Compare commits
63 Commits
8c29c7423c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 10846519e3 | |||
| 991a9b104e | |||
| 5349b3c37f | |||
| 2479406d3d | |||
| a0a20d7270 | |||
| f72a4d5a5b | |||
| 803c1373e0 | |||
| 25d797dff0 | |||
| 986001814c | |||
| ccc66f7f0d | |||
| b51d79c5a1 | |||
| 8e1751ef5d | |||
| a2a3b0a842 | |||
| a90e046e03 | |||
| e7d6d5262d | |||
| 5bcf3e8198 | |||
| e5f81c6720 | |||
| 237515b812 | |||
| 42ed981d16 | |||
| b3b5b6260b | |||
| f8a30e01d7 | |||
| 6fe0780113 | |||
| bcd01a5d3e | |||
| f9496fe208 | |||
| 8cd89e48a2 | |||
| 614c26edbc | |||
| 9839389fc9 | |||
| ec01a2ae10 | |||
| c1616ac542 | |||
| 74e43066b6 | |||
| 975bc4ee61 | |||
| 9c3a932386 | |||
| 4d938c5266 | |||
| e936b10e44 | |||
| 49a85d73ee | |||
| 15f8200b1d | |||
| 2cf46b6f28 | |||
| 81fef0c0f8 | |||
| 1810db09b9 | |||
| 012ec02145 | |||
| c91df27dfa | |||
| 6f43fa4c3b | |||
| 6a576136af | |||
| 8a95857010 | |||
| 3d96ac368a | |||
| dc2088fe22 | |||
| 4f581ce3b7 | |||
| 8819837b29 | |||
| c0d890b4de | |||
| d1e0b0bba4 | |||
| 3523b38e0b | |||
| 33f128d5c6 | |||
| 5f48208aab | |||
| 19d523213b | |||
| 8f1e0459fc | |||
| bd028d09e6 | |||
| 2e0dc90220 | |||
| 1d193a9eab | |||
|
|
67aeac2238 | ||
| 996e74a100 | |||
| a46312f78e | |||
| ac7a599bd5 | |||
| b1759eac10 |
633
.drone.yml
Normal file
@@ -0,0 +1,633 @@
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: smartsoltech-ci
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
services:
|
||||
- name: postgres
|
||||
image: postgres:17-alpine
|
||||
environment:
|
||||
POSTGRES_DB: smartsoltech_test
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
ports:
|
||||
- 5432
|
||||
|
||||
- name: redis
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- 6379
|
||||
|
||||
steps:
|
||||
- name: code-quality
|
||||
image: python:3.10-slim
|
||||
environment:
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/smartsoltech_test
|
||||
commands:
|
||||
- apt-get update && apt-get install -y git
|
||||
- pip install --upgrade pip
|
||||
- pip install flake8 black isort bandit safety
|
||||
- echo "Checking code quality..."
|
||||
- flake8 smartsoltech/ --max-line-length=88 --exclude=migrations,staticfiles --ignore=E203,W503 || true
|
||||
- echo "Checking code formatting..."
|
||||
- black --check smartsoltech/ --line-length=88 --target-version=py310 || true
|
||||
- echo "Checking imports..."
|
||||
- isort --check-only smartsoltech/ --profile=black || true
|
||||
- echo "Security scan..."
|
||||
- bandit -r smartsoltech/ -x "*/migrations/*,*/staticfiles/*" -ll || true
|
||||
- echo "Checking dependencies..."
|
||||
- safety check --file requirements.txt --ignore=70612 || true
|
||||
|
||||
- name: install-dependencies
|
||||
image: python:3.10-slim
|
||||
commands:
|
||||
- apt-get update && apt-get install -y libpq-dev gcc git curl
|
||||
- pip install --upgrade pip
|
||||
- pip install -r requirements.txt
|
||||
- pip install coverage pytest-django pytest-cov
|
||||
- echo "Dependencies installed"
|
||||
depends_on:
|
||||
- code-quality
|
||||
|
||||
- name: database-tests
|
||||
image: python:3.10-slim
|
||||
environment:
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/smartsoltech_test
|
||||
SECRET_KEY: test-secret-key-for-ci-very-long-and-secure-key-12345
|
||||
DEBUG: "False"
|
||||
ALLOWED_HOSTS: localhost,127.0.0.1,postgres
|
||||
DJANGO_SETTINGS_MODULE: smartsoltech.settings_test
|
||||
commands:
|
||||
- apt-get update && apt-get install -y libpq-dev gcc curl postgresql-client
|
||||
- pip install --upgrade pip
|
||||
- pip install -r requirements.txt
|
||||
- echo "Waiting for PostgreSQL..."
|
||||
- sleep 15
|
||||
- echo "Checking database connection..."
|
||||
- until pg_isready -h postgres -p 5432 -U postgres; do echo "Waiting for postgres..."; sleep 2; done
|
||||
- echo "Checking migrations..."
|
||||
- cd smartsoltech
|
||||
- python manage.py check --settings=smartsoltech.settings_test
|
||||
- python manage.py makemigrations --check --dry-run --settings=smartsoltech.settings_test
|
||||
- python manage.py migrate --settings=smartsoltech.settings_test
|
||||
- echo "Database setup completed"
|
||||
depends_on:
|
||||
- install-dependencies
|
||||
|
||||
- name: unit-tests
|
||||
image: python:3.10-slim
|
||||
environment:
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/smartsoltech_test
|
||||
SECRET_KEY: test-secret-key-for-ci-very-long-and-secure-key-12345
|
||||
DEBUG: "False"
|
||||
ALLOWED_HOSTS: localhost,127.0.0.1,postgres
|
||||
TELEGRAM_BOT_TOKEN: test-token-for-ci
|
||||
DJANGO_SETTINGS_MODULE: smartsoltech.settings_test
|
||||
commands:
|
||||
- apt-get update && apt-get install -y libpq-dev gcc curl postgresql-client
|
||||
- pip install --upgrade pip
|
||||
- pip install -r requirements.txt
|
||||
- echo "Waiting for PostgreSQL..."
|
||||
- until pg_isready -h postgres -p 5432 -U postgres; do echo "Waiting for postgres..."; sleep 2; done
|
||||
- cd smartsoltech
|
||||
- echo "Running unit tests..."
|
||||
- python manage.py test --verbosity=2 --settings=smartsoltech.settings_test --keepdb
|
||||
- echo "Generating coverage report..."
|
||||
- coverage run --source='.' manage.py test --settings=smartsoltech.settings_test --keepdb
|
||||
- coverage report --show-missing
|
||||
- coverage xml
|
||||
- echo "Unit tests completed successfully"
|
||||
depends_on:
|
||||
- database-tests
|
||||
|
||||
- name: build-docker-image
|
||||
image: docker:24-dind
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- echo "Building Docker image..."
|
||||
- docker build -t smartsoltech:latest .
|
||||
- echo "Docker image built successfully"
|
||||
depends_on:
|
||||
- unit-tests
|
||||
|
||||
- name: docker-compose-tests
|
||||
image: docker/compose:latest
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- echo "Running Docker Compose tests..."
|
||||
- apk add --no-cache curl
|
||||
- docker-compose -f docker-compose.test.yml build
|
||||
- docker-compose -f docker-compose.test.yml up --abort-on-container-exit --exit-code-from django_test
|
||||
- docker-compose -f docker-compose.test.yml down -v
|
||||
- echo "Docker tests completed"
|
||||
depends_on:
|
||||
- build-docker-image
|
||||
|
||||
- name: security-scan
|
||||
image: aquasec/trivy:latest
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- echo "Security scanning Docker image..."
|
||||
- |
|
||||
if docker image inspect smartsoltech:latest >/dev/null 2>&1; then
|
||||
echo "Image found, starting security scan..."
|
||||
trivy image --exit-code 0 --severity HIGH,CRITICAL --no-progress smartsoltech:latest
|
||||
else
|
||||
echo "Image smartsoltech:latest not found, scanning base Python image instead..."
|
||||
trivy image --exit-code 0 --severity HIGH,CRITICAL --no-progress python:3.10-slim
|
||||
fi
|
||||
- echo "Security scan completed"
|
||||
depends_on:
|
||||
- docker-compose-tests
|
||||
|
||||
- name: test-production-connectivity
|
||||
image: alpine:latest
|
||||
environment:
|
||||
PROD_HOST:
|
||||
from_secret: production_host
|
||||
commands:
|
||||
- echo "Testing production server connectivity..."
|
||||
- apk add --no-cache curl netcat-openbsd
|
||||
- |
|
||||
if [ -z "$PROD_HOST" ]; then
|
||||
echo "⚠️ Production host not configured, skipping connectivity test"
|
||||
exit 0
|
||||
fi
|
||||
- echo "Testing SSH connectivity to $PROD_HOST..."
|
||||
- |
|
||||
if nc -z $PROD_HOST 22 2>/dev/null; then
|
||||
echo "✅ SSH port 22 is accessible on $PROD_HOST"
|
||||
else
|
||||
echo "⚠️ SSH port 22 is not accessible, but continuing CI"
|
||||
fi
|
||||
- echo "Testing HTTPS connectivity..."
|
||||
- |
|
||||
if curl -f -s --connect-timeout 10 https://smartsoltech.kr/health/ >/dev/null 2>&1; then
|
||||
echo "✅ Production HTTPS service is accessible"
|
||||
else
|
||||
echo "⚠️ Production HTTPS service check failed, but continuing CI"
|
||||
fi
|
||||
- echo "✅ Production connectivity test completed"
|
||||
depends_on:
|
||||
- security-scan
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
- main
|
||||
|
||||
- name: deploy-to-staging
|
||||
image: alpine:latest
|
||||
environment:
|
||||
STAGING_HOST:
|
||||
from_secret: staging_host
|
||||
STAGING_USER:
|
||||
from_secret: staging_user
|
||||
STAGING_KEY:
|
||||
from_secret: staging_key
|
||||
commands:
|
||||
- echo "Checking staging environment configuration..."
|
||||
- apk add --no-cache openssh-client git curl netcat-openbsd
|
||||
- |
|
||||
if [ -z "$STAGING_HOST" ] || [ -z "$STAGING_USER" ]; then
|
||||
echo "⚠️ Staging credentials not configured"
|
||||
echo "ℹ️ Skipping staging deployment - this is normal for development CI"
|
||||
echo "✅ Continuing with integration tests on local environment"
|
||||
exit 0
|
||||
fi
|
||||
- echo "Deploying to staging server:" $STAGING_HOST
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$STAGING_KEY" > ~/.ssh/id_rsa
|
||||
- chmod 600 ~/.ssh/id_rsa
|
||||
- ssh-keyscan -H $STAGING_HOST >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||
- echo "Testing staging server connectivity..."
|
||||
- |
|
||||
if ! nc -z $STAGING_HOST 22 2>/dev/null; then
|
||||
echo "❌ Cannot connect to staging server on port 22"
|
||||
echo "⚠️ Skipping staging deployment due to connectivity issues"
|
||||
exit 0
|
||||
fi
|
||||
- echo "Deploying to staging environment..."
|
||||
- |
|
||||
ssh $STAGING_USER@$STAGING_HOST "cd /opt/smartsoltech-staging &&
|
||||
echo 'Fetching latest changes...' &&
|
||||
git fetch origin &&
|
||||
git reset --hard origin/master &&
|
||||
echo 'Restarting services...' &&
|
||||
docker-compose down --timeout 30 &&
|
||||
docker-compose pull &&
|
||||
docker-compose up -d --build" || {
|
||||
echo "❌ Staging deployment failed"
|
||||
echo "⚠️ Continuing CI pipeline - staging failures are non-critical"
|
||||
}
|
||||
- echo "✅ Staging deployment step completed"
|
||||
depends_on:
|
||||
- test-production-connectivity
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
- main
|
||||
|
||||
- name: integration-tests
|
||||
image: alpine:latest
|
||||
environment:
|
||||
STAGING_HOST:
|
||||
from_secret: staging_host
|
||||
commands:
|
||||
- echo "Starting comprehensive integration tests..."
|
||||
- apk add --no-cache curl jq netcat-openbsd
|
||||
- |
|
||||
# Определяем target для тестирования
|
||||
if [ -n "$STAGING_HOST" ]; then
|
||||
# Проверяем доступность staging сервера
|
||||
if nc -z -w5 $STAGING_HOST 80 2>/dev/null; then
|
||||
export TEST_TARGET="http://$STAGING_HOST"
|
||||
echo "🎯 Testing staging environment: $TEST_TARGET"
|
||||
else
|
||||
echo "⚠️ Staging server not accessible, falling back to local testing"
|
||||
export TEST_TARGET="http://localhost:8000"
|
||||
echo "🏠 Testing local environment: $TEST_TARGET"
|
||||
fi
|
||||
else
|
||||
export TEST_TARGET="http://localhost:8000"
|
||||
echo "🏠 Testing local environment: $TEST_TARGET"
|
||||
fi
|
||||
- echo "Waiting for services to be ready..."
|
||||
- sleep 30
|
||||
- echo "Running endpoint availability tests..."
|
||||
- |
|
||||
test_endpoint() {
|
||||
local url="$1"
|
||||
local description="$2"
|
||||
echo "Testing $description ($url)..."
|
||||
|
||||
local status_code=$(curl -L -o /dev/null -s -w "%{http_code}" -m 10 "$url" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "$status_code" = "200" ]; then
|
||||
echo "✅ $description - OK (HTTP $status_code)"
|
||||
return 0
|
||||
elif [ "$status_code" = "301" ] || [ "$status_code" = "302" ]; then
|
||||
echo "✅ $description - Redirect (HTTP $status_code)"
|
||||
return 0
|
||||
elif [ "$status_code" = "404" ]; then
|
||||
echo "⚠️ $description - Not Found (HTTP $status_code)"
|
||||
return 1
|
||||
elif [ "$status_code" = "000" ]; then
|
||||
echo "❌ $description - Connection Failed"
|
||||
return 1
|
||||
else
|
||||
echo "⚠️ $description - Unexpected status (HTTP $status_code)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
- |
|
||||
# Счетчик ошибок
|
||||
errors=0
|
||||
total_tests=0
|
||||
|
||||
# Основные страницы
|
||||
total_tests=$((total_tests + 1))
|
||||
if ! test_endpoint "$TEST_TARGET/" "Homepage"; then
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
|
||||
total_tests=$((total_tests + 1))
|
||||
if ! test_endpoint "$TEST_TARGET/services/" "Services page"; then
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
|
||||
total_tests=$((total_tests + 1))
|
||||
if ! test_endpoint "$TEST_TARGET/career/" "Career page"; then
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
|
||||
total_tests=$((total_tests + 1))
|
||||
if ! test_endpoint "$TEST_TARGET/contact/" "Contact page"; then
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
|
||||
total_tests=$((total_tests + 1))
|
||||
if ! test_endpoint "$TEST_TARGET/admin/" "Admin panel"; then
|
||||
echo "ℹ️ Admin panel test failed (expected for production)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📊 Integration Test Results:"
|
||||
echo " Total tests: $total_tests"
|
||||
echo " Failures: $errors"
|
||||
if [ $total_tests -gt 0 ]; then
|
||||
success_rate=$(( (total_tests - errors) * 100 / total_tests ))
|
||||
echo " Success rate: ${success_rate}%"
|
||||
fi
|
||||
|
||||
if [ $errors -gt 2 ]; then
|
||||
echo "❌ Too many critical endpoint failures ($errors)"
|
||||
exit 1
|
||||
elif [ $errors -gt 0 ]; then
|
||||
echo "⚠️ Some tests failed but within acceptable limits"
|
||||
else
|
||||
echo "✅ All integration tests passed successfully"
|
||||
fi
|
||||
- echo "✅ Integration testing phase completed"
|
||||
depends_on:
|
||||
- deploy-to-staging
|
||||
|
||||
- name: notify-success
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
from_secret: telegram_webhook_url
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"chat_id": "${TELEGRAM_CHAT_ID:-@smartsoltech_ci}",
|
||||
"text": "✅ *SmartSolTech CI/CD*\n\nBuild completed successfully!\n\n📝 *Commit:* `${DRONE_COMMIT_SHA:0:8}`\n👤 *Author:* ${DRONE_COMMIT_AUTHOR}\n🌿 *Branch:* ${DRONE_BRANCH}\n⏱ *Time:* ${DRONE_BUILD_FINISHED}\n\n🔗 [Details](${DRONE_BUILD_LINK})",
|
||||
"parse_mode": "Markdown"
|
||||
}
|
||||
environment:
|
||||
TELEGRAM_CHAT_ID:
|
||||
from_secret: telegram_chat_id
|
||||
when:
|
||||
status:
|
||||
- success
|
||||
# Отключаем до настройки секретов
|
||||
event:
|
||||
exclude:
|
||||
- '*'
|
||||
depends_on:
|
||||
- integration-tests
|
||||
|
||||
- name: notify-failure
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
from_secret: telegram_webhook_url
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"chat_id": "${TELEGRAM_CHAT_ID:-@smartsoltech_ci}",
|
||||
"text": "❌ *SmartSolTech CI/CD*\n\nBuild failed!\n\n📝 *Commit:* `${DRONE_COMMIT_SHA:0:8}`\n👤 *Author:* ${DRONE_COMMIT_AUTHOR}\n🌿 *Branch:* ${DRONE_BRANCH}\n⏱ *Time:* ${DRONE_BUILD_FINISHED}\n\n🔗 [Logs](${DRONE_BUILD_LINK})",
|
||||
"parse_mode": "Markdown"
|
||||
}
|
||||
environment:
|
||||
TELEGRAM_CHAT_ID:
|
||||
from_secret: telegram_chat_id
|
||||
when:
|
||||
status:
|
||||
- failure
|
||||
# Отключаем до настройки секретов
|
||||
event:
|
||||
exclude:
|
||||
- '*'
|
||||
depends_on:
|
||||
- integration-tests
|
||||
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
host:
|
||||
path: /var/run/docker.sock
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
- main
|
||||
- develop
|
||||
- feature/*
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: production-deploy
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: check-production-server
|
||||
image: alpine:latest
|
||||
environment:
|
||||
PROD_HOST:
|
||||
from_secret: production_host
|
||||
PROD_USER:
|
||||
from_secret: production_user
|
||||
commands:
|
||||
- echo "Checking production server connectivity..."
|
||||
- apk add --no-cache openssh-client curl netcat-openbsd
|
||||
- |
|
||||
if [ -z "$PROD_HOST" ] || [ -z "$PROD_USER" ]; then
|
||||
echo "❌ Production server credentials not configured"
|
||||
exit 1
|
||||
fi
|
||||
- echo "Testing SSH connectivity to $PROD_HOST..."
|
||||
- |
|
||||
if ! nc -z $PROD_HOST 22; then
|
||||
echo "❌ SSH port 22 is not accessible on $PROD_HOST"
|
||||
exit 1
|
||||
fi
|
||||
- echo "Testing HTTPS connectivity..."
|
||||
- |
|
||||
if curl -f -s --connect-timeout 10 https://smartsoltech.kr >/dev/null; then
|
||||
echo "✅ HTTPS service is accessible"
|
||||
else
|
||||
echo "⚠️ HTTPS service check failed, but continuing deployment"
|
||||
fi
|
||||
- echo "✅ Production server checks passed"
|
||||
|
||||
- name: deploy-production
|
||||
image: docker:24-dind
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
path: /var/run/docker.sock
|
||||
environment:
|
||||
PROD_HOST:
|
||||
from_secret: production_host
|
||||
PROD_USER:
|
||||
from_secret: production_user
|
||||
PROD_KEY:
|
||||
from_secret: production_ssh_key
|
||||
commands:
|
||||
- echo "Deploying to production..."
|
||||
- apk add --no-cache openssh-client git curl
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$PROD_KEY" > ~/.ssh/id_rsa
|
||||
- chmod 600 ~/.ssh/id_rsa
|
||||
- ssh-keyscan -H $PROD_HOST >> ~/.ssh/known_hosts
|
||||
- echo "Creating backup before deployment..."
|
||||
- |
|
||||
ssh $PROD_USER@$PROD_HOST "cd /opt/smartsoltech &&
|
||||
echo 'Creating backup...' &&
|
||||
git stash push -m \"Pre-deployment backup \$(date)\" || true &&
|
||||
docker-compose down --timeout 30 || true"
|
||||
- echo "Pulling latest changes..."
|
||||
- |
|
||||
ssh $PROD_USER@$PROD_HOST "cd /opt/smartsoltech &&
|
||||
git fetch origin &&
|
||||
git reset --hard origin/master &&
|
||||
git clean -fd"
|
||||
- echo "Running deployment script..."
|
||||
- |
|
||||
ssh $PROD_USER@$PROD_HOST "cd /opt/smartsoltech &&
|
||||
if [ -f ./bin/update ]; then
|
||||
chmod +x ./bin/update &&
|
||||
./bin/update
|
||||
else
|
||||
echo 'Update script not found, running manual deployment...' &&
|
||||
docker-compose pull &&
|
||||
docker-compose up -d --build
|
||||
fi"
|
||||
- echo "Verifying deployment..."
|
||||
- sleep 30
|
||||
- |
|
||||
for i in 1 2 3; do
|
||||
if curl -f -s --connect-timeout 10 https://smartsoltech.kr >/dev/null; then
|
||||
echo "✅ Deployment verification successful"
|
||||
break
|
||||
else
|
||||
echo "⚠️ Deployment verification attempt $i failed, retrying..."
|
||||
sleep 15
|
||||
fi
|
||||
if [ $i -eq 3 ]; then
|
||||
echo "❌ Deployment verification failed after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
- echo "🎉 Production deployment completed successfully"
|
||||
depends_on:
|
||||
- check-production-server
|
||||
|
||||
- name: notify-production-success
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
from_secret: telegram_webhook_url
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"chat_id": "${TELEGRAM_CHAT_ID}",
|
||||
"text": "🎉 *SmartSolTech Production*\n\n✅ Production deployment completed!\n\n📝 *Commit:* \`${DRONE_COMMIT_SHA:0:8}\`\n👤 *Author:* ${DRONE_COMMIT_AUTHOR}\n🌿 *Branch:* ${DRONE_BRANCH}\n⏱ *Time:* ${DRONE_BUILD_FINISHED}\n\n🌐 [Website](https://smartsoltech.kr)\n🔧 [Admin](https://smartsoltech.kr/admin/)\n📊 [Status Check](https://smartsoltech.kr/health/)",
|
||||
"parse_mode": "Markdown"
|
||||
}
|
||||
environment:
|
||||
TELEGRAM_CHAT_ID:
|
||||
from_secret: telegram_chat_id
|
||||
depends_on:
|
||||
- deploy-production
|
||||
|
||||
- name: notify-production-failure
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
from_secret: telegram_webhook_url
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"chat_id": "${TELEGRAM_CHAT_ID}",
|
||||
"text": "🚨 *SmartSolTech Production*\n\n❌ Production deployment failed!\n\n📝 *Commit:* \`${DRONE_COMMIT_SHA:0:8}\`\n👤 *Author:* ${DRONE_COMMIT_AUTHOR}\n🌿 *Branch:* ${DRONE_BRANCH}\n⏱ *Time:* ${DRONE_BUILD_FINISHED}\n💥 *Step:* ${DRONE_FAILED_STEPS}\n\n🔗 [View Logs](${DRONE_BUILD_LINK})\n🛠 [Rollback Guide](https://smartsoltech.kr/docs/rollback)",
|
||||
"parse_mode": "Markdown"
|
||||
}
|
||||
environment:
|
||||
TELEGRAM_CHAT_ID:
|
||||
from_secret: telegram_chat_id
|
||||
when:
|
||||
status:
|
||||
- failure
|
||||
depends_on:
|
||||
- deploy-production
|
||||
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
host:
|
||||
path: /var/run/docker.sock
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- tag
|
||||
ref:
|
||||
- refs/tags/v*
|
||||
|
||||
depends_on:
|
||||
- smartsoltech-ci
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: maintenance
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: cleanup-docker
|
||||
image: docker:24-dind
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- echo "Docker cleanup..."
|
||||
- docker system prune -af --volumes
|
||||
- docker image prune -af
|
||||
- echo "Docker cleanup completed"
|
||||
|
||||
- name: backup-database
|
||||
image: postgres:17-alpine
|
||||
environment:
|
||||
PGHOST:
|
||||
from_secret: db_host
|
||||
PGUSER:
|
||||
from_secret: db_user
|
||||
PGPASSWORD:
|
||||
from_secret: db_password
|
||||
PGDATABASE:
|
||||
from_secret: db_name
|
||||
BACKUP_PATH:
|
||||
from_secret: backup_path
|
||||
commands:
|
||||
- echo "Creating database backup..."
|
||||
- mkdir -p /backups
|
||||
- pg_dump -h $PGHOST -U $PGUSER -d $PGDATABASE --no-password > /backups/backup_$(date +%Y%m%d_%H%M%S).sql
|
||||
- echo "Database backup created"
|
||||
|
||||
- name: notify-maintenance
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
from_secret: telegram_webhook_url
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"chat_id": "${TELEGRAM_CHAT_ID}",
|
||||
"text": "🛠 *SmartSolTech Maintenance*\n\n✅ Scheduled maintenance completed!\n\n🧹 Docker cleanup\n💾 Database backup\n⏱ *Time:* ${DRONE_BUILD_FINISHED}",
|
||||
"parse_mode": "Markdown"
|
||||
}
|
||||
environment:
|
||||
TELEGRAM_CHAT_ID:
|
||||
from_secret: telegram_chat_id
|
||||
depends_on:
|
||||
- cleanup-docker
|
||||
- backup-database
|
||||
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
host:
|
||||
path: /var/run/docker.sock
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- cron
|
||||
cron:
|
||||
- nightly_maintenance
|
||||
137
.gitignore
vendored
@@ -1,5 +1,134 @@
|
||||
# 🐍 Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# 🧪 Testing
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# 🌐 Django
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
media/
|
||||
staticfiles/
|
||||
smartsoltech/staticfiles/
|
||||
static_root/
|
||||
|
||||
# ⚙️ Environment variables
|
||||
.env
|
||||
__pycache__
|
||||
.venv
|
||||
.history
|
||||
static/qr_codes
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.venv/
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# 📝 IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.history/
|
||||
|
||||
# 📱 OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# 🐳 Docker
|
||||
.dockerignore
|
||||
|
||||
# 📊 Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# 💾 QR codes (generated dynamically)
|
||||
static/qr_codes/*.png
|
||||
smartsoltech/static/qr_codes/*.png
|
||||
!smartsoltech/static/qr_codes/.gitkeep
|
||||
|
||||
# 🗃️ Database
|
||||
*.sqlite3
|
||||
*.db
|
||||
|
||||
# 📧 Email
|
||||
sent_emails/
|
||||
|
||||
# 🎨 CSS/JS builds
|
||||
*.css.map
|
||||
*.js.map
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
# 🔄 Cache
|
||||
.cache/
|
||||
.parcel-cache/
|
||||
|
||||
# 🌍 Translation files
|
||||
*.pot
|
||||
|
||||
# 📋 Temporary files and folders
|
||||
temp/
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# 🚫 Exclude test files from root
|
||||
response_*.json
|
||||
test_*.html
|
||||
*_test.sh
|
||||
endpoint_test.sh
|
||||
loading_screen_fixed.html
|
||||
qr_success_animation_demo.html
|
||||
|
||||
# 📋 Temporary documentation files that moved to docs/
|
||||
BACKUP_SETUP_COMPLETE.md
|
||||
COMMIT_SUMMARY.md
|
||||
SCRIPTS_README.md
|
||||
|
||||
# 🔧 Utils and scripts (keep tracked but ignore development versions)
|
||||
utils/*.log
|
||||
scripts/*.log
|
||||
backups/*.tmp
|
||||
30
Dockerfile.test
Normal file
@@ -0,0 +1,30 @@
|
||||
# Dockerfile для тестирования
|
||||
FROM python:3.10-slim
|
||||
|
||||
# Установка системных зависимостей
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libpq-dev \
|
||||
gcc \
|
||||
curl \
|
||||
postgresql-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Рабочая директория
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем requirements и устанавливаем зависимости
|
||||
COPY requirements.txt .
|
||||
RUN pip install --upgrade pip && pip install -r requirements.txt
|
||||
|
||||
# Копируем код приложения
|
||||
COPY . .
|
||||
|
||||
# Настройки для тестов
|
||||
ENV PYTHONPATH=/app
|
||||
ENV DJANGO_SETTINGS_MODULE=smartsoltech.settings_test
|
||||
ENV SECRET_KEY=test-secret-key-for-ci-very-long-and-secure-key-12345
|
||||
ENV DEBUG=False
|
||||
ENV ALLOWED_HOSTS=localhost,127.0.0.1,postgres,*
|
||||
|
||||
# Команда по умолчанию
|
||||
CMD ["python", "smartsoltech/manage.py", "test", "--settings=smartsoltech.settings_test", "--verbosity=2"]
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2007-present Steven Levithan <http://xregexp.com/>
|
||||
Copyright (c) 2023 SmartSolTech
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,13 +9,13 @@ 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 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.
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
242
README.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# 🚀 SmartSolTech
|
||||
|
||||
[](https://drone.smartsoltech.kr/trevor/smartsoltech_site)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.python.org/downloads/release/python-3100/)
|
||||
[](https://docs.djangoproject.com/en/4.2/)
|
||||
|
||||
Современная веб-платформа для предоставления IT-услуг с интегрированной системой управления заказами и Telegram-ботом.
|
||||
|
||||
## 🛠️ Технологический стек
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white" alt="Python" />
|
||||
<img src="https://img.shields.io/badge/Django-092E20?style=for-the-badge&logo=django&logoColor=white" alt="Django" />
|
||||
<img src="https://img.shields.io/badge/PostgreSQL-316192?style=for-the-badge&logo=postgresql&logoColor=white" alt="PostgreSQL" />
|
||||
<img src="https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white" alt="Docker" />
|
||||
<img src="https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white" alt="Bootstrap" />
|
||||
<img src="https://img.shields.io/badge/JavaScript-F7DF1E?style=for-the-badge&logo=javascript&logoColor=black" alt="JavaScript" />
|
||||
<img src="https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white" alt="Telegram" />
|
||||
<img src="https://img.shields.io/badge/Drone%20CI-212121?style=for-the-badge&logo=drone&logoColor=white" alt="Drone CI" />
|
||||
<img src="https://img.shields.io/badge/HTML5-E34F26?style=for-the-badge&logo=html5&logoColor=white" alt="HTML5" />
|
||||
<img src="https://img.shields.io/badge/CSS3-1572B6?style=for-the-badge&logo=css3&logoColor=white" alt="CSS3" />
|
||||
</p>
|
||||
|
||||
## ✨ Основные возможности
|
||||
|
||||
### 📱 **Веб-платформа**
|
||||
- Современный адаптивный интерфейс на Bootstrap 5
|
||||
- Система подачи заявок на услуги с QR-кодами
|
||||
- Портфолио проектов и услуг
|
||||
- Админ-панель для управления контентом
|
||||
|
||||
### 🤖 **Telegram Bot Integration**
|
||||
- Автоматическое уведомление о новых заказах
|
||||
- Подтверждение заявок через QR-коды
|
||||
- Двусторонняя связь клиент-компания
|
||||
- Real-time статусы заказов
|
||||
|
||||
### 🔧 **DevOps & Автоматизация**
|
||||
- Docker контейнеризация
|
||||
- CI/CD pipeline с Drone
|
||||
- Автоматизированные скрипты развертывания
|
||||
- Система резервного копирования
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### Требования
|
||||
- Docker & Docker Compose
|
||||
- Git
|
||||
- Python 3.10+ (для разработки)
|
||||
|
||||
### Запуск проекта
|
||||
|
||||
```bash
|
||||
# Клонирование репозитория
|
||||
git clone https://github.com/smartsoltech/smartsoltech.kr.git
|
||||
cd smartsoltech.kr
|
||||
|
||||
# Запуск всех сервисов
|
||||
./start
|
||||
|
||||
# Создание суперпользователя (опционально)
|
||||
./cli createsuperuser
|
||||
|
||||
# Проверка статуса
|
||||
./cli status
|
||||
```
|
||||
|
||||
Сайт будет доступен по адресу: http://localhost:8000
|
||||
|
||||
### Основные команды
|
||||
|
||||
```bash
|
||||
.utils/cli shell # Django shell
|
||||
./cli migrate # Применить миграции
|
||||
./update # Полное обновление проекта
|
||||
./stop # Остановка сервисов
|
||||
./logs # Просмотр логов
|
||||
```
|
||||
|
||||
## 📚 Документация
|
||||
|
||||
| Документ | Описание |
|
||||
|----------|----------|
|
||||
| [🛠️ Управление скриптами](docs/SCRIPTS_README.md) | Полное руководство по всем скриптам управления |
|
||||
| [🔧 Настройка бэкапа](docs/BACKUP_SETUP_COMPLETE.md) | Настройка системы резервного копирования |
|
||||
| [📝 История изменений](docs/COMMIT_SUMMARY.md) | Подробная история разработки |
|
||||
| [🚀 Развертывание](docs/DEPLOYMENT.md) | Руководство по развертыванию в продакшн |
|
||||
| [🤖 API документация](docs/API.md) | Документация REST API |
|
||||
|
||||
## 🏗️ Архитектура проекта
|
||||
|
||||
```
|
||||
smartsoltech.kr/
|
||||
├── 📄 README.md # Основная документация
|
||||
├── 📜 LICENSE # Лицензия MIT
|
||||
├── 🔧 .drone.yml # CI/CD pipeline конфигурация
|
||||
├── 📋 .gitignore # Git исключения
|
||||
├── 🐳 Контейнеризация
|
||||
│ ├── Dockerfile # Docker образ
|
||||
│ ├── docker-compose.yml # Оркестрация сервисов
|
||||
│ └── requirements.txt # Python зависимости
|
||||
├── 🛠️ bin/ # Скрипты управления
|
||||
│ ├── cli.sh # CLI для контейнера
|
||||
│ ├── update.sh # Скрипт обновления
|
||||
│ ├── start.sh, stop.sh # Управление сервисами
|
||||
│ ├── logs.sh # Просмотр логов
|
||||
│ └── setup-backup.sh # Настройка backup
|
||||
├── 📚 docs/ # Документация
|
||||
│ ├── SCRIPTS_README.md # Руководство по скриптам
|
||||
│ ├── DEPLOYMENT.md # Развертывание в продакшн
|
||||
│ ├── API.md # API документация
|
||||
│ └── *.md # Другая документация
|
||||
├── 🐍 smartsoltech/ # Django приложение
|
||||
│ ├── web/ # Основное веб-приложение
|
||||
│ ├── comunication/ # Telegram bot & уведомления
|
||||
│ ├── static/ # Статические файлы
|
||||
│ ├── media/ # Загруженные файлы
|
||||
│ └── manage.py # Django управление
|
||||
├── 🎨 frontend/ # Фронтенд ресурсы
|
||||
│ ├── assets/ # CSS, JS, изображения
|
||||
│ └── *.html # HTML шаблоны
|
||||
├── 🔧 patch/ # Патчи и временные файлы
|
||||
└── 🔗 Корневые утилиты # cli, update, start, stop, logs
|
||||
```
|
||||
|
||||
## 🔧 Разработка
|
||||
|
||||
### Локальная разработка
|
||||
|
||||
```bash
|
||||
# Активация виртуального окружения
|
||||
source .venv/bin/activate
|
||||
|
||||
# Установка зависимостей
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Запуск в режиме разработки
|
||||
./cli runserver
|
||||
```
|
||||
|
||||
### Работа с базой данных
|
||||
|
||||
```bash
|
||||
# Подключение к БД
|
||||
./cli dbshell
|
||||
|
||||
# Создание миграций
|
||||
./cli makemigrations
|
||||
|
||||
# Применение миграций
|
||||
./cli migrate
|
||||
```
|
||||
|
||||
### Управление контейнерами
|
||||
|
||||
```bash
|
||||
# Пересборка контейнеров
|
||||
./stop --clean && ./update
|
||||
|
||||
# Логи конкретного сервиса
|
||||
./logs web
|
||||
./logs db
|
||||
|
||||
# Вход в контейнер
|
||||
./cli bash
|
||||
```
|
||||
|
||||
## 🌐 Продакшн развертывание
|
||||
|
||||
### Настройка сервера
|
||||
|
||||
```bash
|
||||
# Полное обновление с резервным репозиторием
|
||||
./update origin backup
|
||||
|
||||
# Настройка backup репозитория
|
||||
./bin/setup-backup.sh
|
||||
|
||||
# Проверка статуса продакшн сервисов
|
||||
./cli status
|
||||
```
|
||||
|
||||
## 📂 Структура проекта
|
||||
|
||||
```
|
||||
smartsoltech/
|
||||
├── 🐍 smartsoltech/ # Основное Django приложение
|
||||
├── 🎨 frontend/ # Статические фронтенд файлы
|
||||
├── 🐳 bin/ # Скрипты развертывания
|
||||
├── 📋 docs/ # Документация проекта
|
||||
├── 🧩 patch/ # Патчи и исправления
|
||||
├── 🛠️ utils/ # Утилиты и инструменты
|
||||
│ ├── start # Запуск проекта
|
||||
│ ├── stop # Остановка сервисов
|
||||
│ ├── update # Обновление проекта
|
||||
│ ├── cli # CLI интерфейс
|
||||
│ ├── logs # Просмотр логов
|
||||
│ └── drone # CI/CD бинарий
|
||||
├── 🐍 scripts/ # Вспомогательные скрипты
|
||||
│ ├── create_hero_banner.py # Создание баннеров
|
||||
│ └── hero_script.py # Скрипты для баннеров
|
||||
├── 💾 backups/ # Резервные копии
|
||||
│ ├── .drone.yml.backup # Бэкап CI конфигурации
|
||||
│ └── original_home_modern.html # Оригинал главной страницы
|
||||
├── 🗂️ temp/ # Временные файлы
|
||||
├── 🐳 docker-compose.yml # Docker конфигурация
|
||||
├── 🚀 .drone.yml # CI/CD конфигурация
|
||||
├── 📄 requirements.txt # Python зависимости
|
||||
└── 📖 README.md # Этот файл
|
||||
```
|
||||
|
||||
### Мониторинг
|
||||
|
||||
- **Веб-сайт**: http://localhost:8000
|
||||
- **Админ-панель**: http://localhost:8000/admin
|
||||
- **PgAdmin**: http://localhost:8080
|
||||
- **Drone CI**: https://drone.smartsoltech.kr
|
||||
|
||||
## 🤝 Участие в разработке
|
||||
|
||||
1. Fork репозитория
|
||||
2. Создайте feature ветку: `git checkout -b feature/amazing-feature`
|
||||
3. Commit изменения: `git commit -m 'Add amazing feature'`
|
||||
4. Push в ветку: `git push origin feature/amazing-feature`
|
||||
5. Создайте Pull Request
|
||||
|
||||
## 📝 Лицензия
|
||||
|
||||
Этот проект распространяется под лицензией MIT. Подробности в файле [LICENSE](LICENSE).
|
||||
|
||||
## 📞 Контакты
|
||||
|
||||
- **Сайт**: [smartsoltech.kr](https://smartsoltech.kr)
|
||||
- **Email**: info@smartsoltech.kr
|
||||
- **Telegram**: [@smartsoltech](https://t.me/smartsoltech)
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
Сделано с ❤️ командой <strong>SmartSolTech</strong>
|
||||
</p>
|
||||
401
backups/.drone.yml.backup
Normal file
@@ -0,0 +1,401 @@
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: smartsoltech-ci
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
# Сервисы для тестирования
|
||||
services:
|
||||
- name: postgres
|
||||
image: postgres:17-alpine
|
||||
environment:
|
||||
POSTGRES_DB: smartsoltech_test
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
ports:
|
||||
- 5432
|
||||
|
||||
- name: redis
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- 6379
|
||||
|
||||
# Этапы сборки
|
||||
steps:
|
||||
# 1. Подготовка и проверка кода
|
||||
- name: code-quality
|
||||
image: python:3.10-slim
|
||||
environment:
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/smartsoltech_test
|
||||
commands:
|
||||
- apt-get update && apt-get install -y git
|
||||
- pip install --upgrade pip
|
||||
- pip install flake8 black isort bandit safety
|
||||
- echo "Checking code quality..."
|
||||
- flake8 smartsoltech/ --max-line-length=88 --exclude=migrations,staticfiles --ignore=E203,W503
|
||||
- echo "Checking code formatting..."
|
||||
- black --check smartsoltech/ --line-length=88 --target-version=py310 || echo "Black formatting check skipped"
|
||||
- echo "Checking imports..."
|
||||
- isort --check-only smartsoltech/ --profile=black || echo "Import sorting check skipped"
|
||||
- echo "Security scan..."
|
||||
- bandit -r smartsoltech/ -x "*/migrations/*,*/staticfiles/*" -ll || echo "Security check completed with warnings"
|
||||
- echo "Checking dependencies..."
|
||||
- safety check --file requirements.txt --ignore=70612 || echo "Dependencies check completed"
|
||||
|
||||
# 2. Установка зависимостей
|
||||
- name: install-dependencies
|
||||
image: python:3.10-slim
|
||||
environment:
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/smartsoltech_test
|
||||
commands:
|
||||
- apt-get update && apt-get install -y libpq-dev gcc git curl
|
||||
- pip install --upgrade pip
|
||||
- pip install -r requirements.txt
|
||||
- pip install coverage pytest-django pytest-cov
|
||||
- echo "Dependencies installed successfully"
|
||||
depends_on:
|
||||
- code-quality
|
||||
|
||||
# 3. Тестирование базы данных
|
||||
- name: database-tests
|
||||
image: python:3.10-slim
|
||||
environment:
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/smartsoltech_test
|
||||
SECRET_KEY: test-secret-key-for-ci-very-long-and-secure-key-12345
|
||||
DEBUG: "False"
|
||||
ALLOWED_HOSTS: localhost,127.0.0.1
|
||||
DJANGO_SETTINGS_MODULE: smartsoltech.settings_test
|
||||
commands:
|
||||
- apt-get update && apt-get install -y libpq-dev gcc curl postgresql-client
|
||||
- pip install --upgrade pip
|
||||
- pip install -r requirements.txt
|
||||
- echo "Waiting for PostgreSQL..."
|
||||
- sleep 15
|
||||
- echo "Checking database connection..."
|
||||
- until pg_isready -h postgres -p 5432 -U postgres; do echo "Waiting for postgres..."; sleep 2; done
|
||||
- echo "Creating test database..."
|
||||
- PGPASSWORD=postgres createdb -h postgres -U postgres smartsoltech_test || echo "Database already exists"
|
||||
- echo "Checking migrations..."
|
||||
- cd smartsoltech
|
||||
- python manage.py check --settings=smartsoltech.settings_test
|
||||
- python manage.py makemigrations --check --dry-run --settings=smartsoltech.settings_test
|
||||
- python manage.py migrate --settings=smartsoltech.settings_test
|
||||
- echo "Database setup completed"
|
||||
depends_on:
|
||||
- install-dependencies
|
||||
|
||||
# 4. Модульные тесты
|
||||
- name: unit-tests
|
||||
image: python:3.10-slim
|
||||
environment:
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/smartsoltech_test
|
||||
SECRET_KEY: test-secret-key-for-ci-very-long-and-secure-key-12345
|
||||
DEBUG: "False"
|
||||
ALLOWED_HOSTS: localhost,127.0.0.1
|
||||
TELEGRAM_BOT_TOKEN: test-token-for-ci
|
||||
DJANGO_SETTINGS_MODULE: smartsoltech.settings_test
|
||||
commands:
|
||||
- apt-get update && apt-get install -y libpq-dev gcc curl postgresql-client
|
||||
- pip install --upgrade pip
|
||||
- pip install -r requirements.txt
|
||||
- echo "Waiting for PostgreSQL..."
|
||||
- until pg_isready -h postgres -p 5432 -U postgres; do echo "Waiting for postgres..."; sleep 2; done
|
||||
- cd smartsoltech
|
||||
- echo "Running unit tests..."
|
||||
- python manage.py test --verbosity=2 --settings=smartsoltech.settings_test --keepdb
|
||||
- echo "Generating coverage report..."
|
||||
- coverage run --source='.' manage.py test --settings=smartsoltech.settings_test --keepdb
|
||||
- coverage report --show-missing
|
||||
- coverage xml
|
||||
- echo "Unit tests completed successfully"
|
||||
depends_on:
|
||||
- database-tests
|
||||
|
||||
# 5. Интеграционные тесты
|
||||
- name: integration-tests
|
||||
image: python:3.10-slim
|
||||
environment:
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/smartsoltech_test
|
||||
SECRET_KEY: test-secret-key-for-ci-very-long-and-secure-key-12345
|
||||
DEBUG: "False"
|
||||
ALLOWED_HOSTS: localhost,127.0.0.1
|
||||
TELEGRAM_BOT_TOKEN: test-token-for-ci
|
||||
DJANGO_SETTINGS_MODULE: smartsoltech.settings_test
|
||||
commands:
|
||||
- apt-get update && apt-get install -y libpq-dev gcc curl postgresql-client
|
||||
- pip install --upgrade pip
|
||||
- pip install -r requirements.txt
|
||||
- echo "Waiting for PostgreSQL..."
|
||||
- until pg_isready -h postgres -p 5432 -U postgres; do echo "Waiting for postgres..."; sleep 2; done
|
||||
- cd smartsoltech
|
||||
- python manage.py migrate --settings=smartsoltech.settings_test
|
||||
- python manage.py collectstatic --noinput --settings=smartsoltech.settings_test
|
||||
- echo "Running integration tests..."
|
||||
- python manage.py test web.tests --verbosity=2 --settings=smartsoltech.settings_test --keepdb || echo "Integration tests completed"
|
||||
- echo "Integration tests completed"
|
||||
depends_on:
|
||||
- unit-tests
|
||||
|
||||
# 6. Сборка Docker образа
|
||||
- name: build-docker-image
|
||||
image: docker:24-dind
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- echo "Building Docker image..."
|
||||
- docker build -t smartsoltech:${DRONE_COMMIT_SHA:0:8} .
|
||||
- docker tag smartsoltech:${DRONE_COMMIT_SHA:0:8} smartsoltech:latest
|
||||
- echo "Docker image built successfully: smartsoltech:${DRONE_COMMIT_SHA:0:8}"
|
||||
depends_on:
|
||||
- integration-tests
|
||||
|
||||
# 7. Тестирование через Docker Compose
|
||||
- name: docker-compose-tests
|
||||
image: docker/compose:latest
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- echo "Running tests with Docker Compose..."
|
||||
- apk add --no-cache curl
|
||||
- docker-compose -f docker-compose.test.yml build
|
||||
- docker-compose -f docker-compose.test.yml up --abort-on-container-exit --exit-code-from django_test
|
||||
- echo "Cleaning up test containers..."
|
||||
- docker-compose -f docker-compose.test.yml down -v
|
||||
- echo "Docker Compose tests completed"
|
||||
depends_on:
|
||||
- build-docker-image
|
||||
|
||||
# 8. Проверка безопасности образа
|
||||
- name: security-scan
|
||||
image: aquasec/trivy:latest
|
||||
commands:
|
||||
- echo "Security scanning Docker image..."
|
||||
- trivy image --exit-code 0 --severity HIGH,CRITICAL --no-progress smartsoltech:latest
|
||||
- echo "Security scan completed"
|
||||
depends_on:
|
||||
- docker-compose-tests
|
||||
|
||||
# 9. Уведомления об успехе
|
||||
- name: notify-success
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
from_secret: telegram_webhook_url
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"chat_id": "${TELEGRAM_CHAT_ID}",
|
||||
"text": "✅ *SmartSolTech CI/CD*\n\n🎉 Сборка успешно завершена!\n\n📝 *Коммит:* `${DRONE_COMMIT_SHA:0:8}`\n👤 *Автор:* ${DRONE_COMMIT_AUTHOR}\n🌿 *Ветка:* ${DRONE_BRANCH}\n⏱ *Время:* ${DRONE_BUILD_FINISHED}\n\n🔗 [Подробности](${DRONE_BUILD_LINK})",
|
||||
"parse_mode": "Markdown"
|
||||
}
|
||||
environment:
|
||||
TELEGRAM_CHAT_ID:
|
||||
from_secret: telegram_chat_id
|
||||
when:
|
||||
status:
|
||||
- success
|
||||
depends_on:
|
||||
- security-scan
|
||||
|
||||
- name: notify-failure
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
from_secret: telegram_webhook_url
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"chat_id": "${TELEGRAM_CHAT_ID}",
|
||||
"text": "❌ *SmartSolTech CI/CD*\n\n🚨 Сборка провалена!\n\n📝 *Коммит:* `${DRONE_COMMIT_SHA:0:8}`\n👤 *Автор:* ${DRONE_COMMIT_AUTHOR}\n🌿 *Ветка:* ${DRONE_BRANCH}\n⏱ *Время:* ${DRONE_BUILD_FINISHED}\n\n🔗 [Логи](${DRONE_BUILD_LINK})",
|
||||
"parse_mode": "Markdown"
|
||||
}
|
||||
environment:
|
||||
TELEGRAM_CHAT_ID:
|
||||
from_secret: telegram_chat_id
|
||||
when:
|
||||
status:
|
||||
- failure
|
||||
depends_on:
|
||||
- security-scan
|
||||
|
||||
# Volumes для Docker in Docker
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
host:
|
||||
path: /var/run/docker.sock
|
||||
|
||||
# Триггеры
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
- main
|
||||
- develop
|
||||
- feature/*
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
---
|
||||
# Production deployment pipeline
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: production-deploy
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: deploy-production
|
||||
image: docker:24-dind
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
path: /var/run/docker.sock
|
||||
environment:
|
||||
PROD_HOST:
|
||||
from_secret: production_host
|
||||
PROD_USER:
|
||||
from_secret: production_user
|
||||
PROD_KEY:
|
||||
from_secret: production_ssh_key
|
||||
commands:
|
||||
- echo "Deploying to production..."
|
||||
- apk add --no-cache openssh-client git
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$PROD_KEY" > ~/.ssh/id_rsa
|
||||
- chmod 600 ~/.ssh/id_rsa
|
||||
- ssh-keyscan -H $PROD_HOST >> ~/.ssh/known_hosts
|
||||
- ssh $PROD_USER@$PROD_HOST "cd /opt/smartsoltech && git pull origin master && ./bin/update"
|
||||
- echo "Production deployment completed"
|
||||
|
||||
- name: notify-production-success
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
from_secret: telegram_webhook_url
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"chat_id": "${TELEGRAM_CHAT_ID}",
|
||||
"text": "🎉 *SmartSolTech Production*\n\n✅ Развертывание в продакшн успешно завершено!\n\n📝 *Версия:* `${DRONE_TAG}`\n👤 *Автор:* ${DRONE_COMMIT_AUTHOR}\n⏱ *Время:* ${DRONE_BUILD_FINISHED}\n\n🌐 [Сайт](https://smartsoltech.kr)",
|
||||
"parse_mode": "Markdown"
|
||||
}
|
||||
environment:
|
||||
TELEGRAM_CHAT_ID:
|
||||
from_secret: telegram_chat_id
|
||||
depends_on:
|
||||
- deploy-production
|
||||
|
||||
- name: notify-production-failure
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
from_secret: telegram_webhook_url
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"chat_id": "${TELEGRAM_CHAT_ID}",
|
||||
"text": "🚨 *SmartSolTech Production*\n\n❌ Развертывание в продакшн провалено!\n\n📝 *Версия:* `${DRONE_TAG}`\n👤 *Автор:* ${DRONE_COMMIT_AUTHOR}\n⏱ *Время:* ${DRONE_BUILD_FINISHED}\n\n🔗 [Логи](${DRONE_BUILD_LINK})",
|
||||
"parse_mode": "Markdown"
|
||||
}
|
||||
environment:
|
||||
TELEGRAM_CHAT_ID:
|
||||
from_secret: telegram_chat_id
|
||||
when:
|
||||
status:
|
||||
- failure
|
||||
depends_on:
|
||||
- deploy-production
|
||||
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
host:
|
||||
path: /var/run/docker.sock
|
||||
|
||||
# Триггер только для тегов (релизов)
|
||||
trigger:
|
||||
event:
|
||||
- tag
|
||||
ref:
|
||||
- refs/tags/v*
|
||||
|
||||
depends_on:
|
||||
- smartsoltech-ci
|
||||
|
||||
---
|
||||
# Scheduled maintenance pipeline
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: maintenance
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: cleanup-docker
|
||||
image: docker:24-dind
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- echo "Docker cleanup..."
|
||||
- docker system prune -af --volumes
|
||||
- docker image prune -af
|
||||
- echo "Docker cleanup completed"
|
||||
|
||||
- name: backup-database
|
||||
image: postgres:17-alpine
|
||||
environment:
|
||||
PGHOST:
|
||||
from_secret: db_host
|
||||
PGUSER:
|
||||
from_secret: db_user
|
||||
PGPASSWORD:
|
||||
from_secret: db_password
|
||||
PGDATABASE:
|
||||
from_secret: db_name
|
||||
BACKUP_PATH:
|
||||
from_secret: backup_path
|
||||
commands:
|
||||
- echo "Creating database backup..."
|
||||
- mkdir -p /backups
|
||||
- pg_dump -h $PGHOST -U $PGUSER -d $PGDATABASE --no-password > /backups/backup_$(date +%Y%m%d_%H%M%S).sql
|
||||
- echo "Database backup created"
|
||||
|
||||
- name: notify-maintenance
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
from_secret: telegram_webhook_url
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"chat_id": "${TELEGRAM_CHAT_ID}",
|
||||
"text": "🛠 *SmartSolTech Maintenance*\n\n✅ Плановое обслуживание выполнено!\n\n🧹 Очистка Docker\n💾 Резервное копирование БД\n⏱ *Время:* ${DRONE_BUILD_FINISHED}",
|
||||
"parse_mode": "Markdown"
|
||||
}
|
||||
environment:
|
||||
TELEGRAM_CHAT_ID:
|
||||
from_secret: telegram_chat_id
|
||||
depends_on:
|
||||
- cleanup-docker
|
||||
- backup-database
|
||||
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
host:
|
||||
path: /var/run/docker.sock
|
||||
|
||||
# Триггер по расписанию (каждую ночь в 2:00)
|
||||
trigger:
|
||||
event:
|
||||
- cron
|
||||
cron:
|
||||
- nightly_maintenance
|
||||
19
backups/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# 💾 Backups
|
||||
|
||||
Папка для хранения резервных копий конфигурационных файлов и важных данных.
|
||||
|
||||
## Содержимое:
|
||||
|
||||
- `.drone.yml.backup` - Резервная копия конфигурации CI/CD
|
||||
- `original_home_modern.html` - Оригинальная версия главной страницы
|
||||
|
||||
## Правила:
|
||||
|
||||
1. Все файлы в этой папке не должны влиять на работу проекта
|
||||
2. Файлы служат для восстановления при необходимости
|
||||
3. Регулярно очищайте старые файлы
|
||||
4. Добавляйте дату к именам файлов при создании бэкапов
|
||||
|
||||
## Автоматические бэкапы:
|
||||
|
||||
Система CI/CD автоматически создает бэкапы перед деплоем в продакшн.
|
||||
361
backups/original_home_modern.html
Normal file
@@ -0,0 +1,361 @@
|
||||
{% extends 'web/base_modern.html' %}
|
||||
{% load static %}
|
||||
{% block title %}SmartSolTech - Современные IT-решения для вашего бизнеса{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-modern" id="home">
|
||||
<div class="container-modern">
|
||||
<div class="row align-items-center min-vh-100">
|
||||
<div class="col-lg-6">
|
||||
<div class="animate-fade-in-up">
|
||||
<h1 class="display-3 fw-bold mb-4">
|
||||
Создаем <span class="text-gradient">будущее</span> вашего бизнеса
|
||||
</h1>
|
||||
<p class="lead mb-4 text-muted">
|
||||
Мы разрабатываем современные веб-приложения, мобильные решения и системы автоматизации,
|
||||
которые помогают компаниям расти и быть конкурентоспособными.
|
||||
</p>
|
||||
<div class="d-flex flex-wrap gap-3 mb-5">
|
||||
<a href="{% url 'services' %}" class="btn btn-primary-modern btn-lg">
|
||||
<i class="fas fa-rocket me-2"></i>
|
||||
Начать проект
|
||||
</a>
|
||||
<a href="{% url 'about' %}" class="btn btn-secondary-modern btn-lg">
|
||||
<i class="fas fa-play-circle me-2"></i>
|
||||
Узнать больше
|
||||
</a>
|
||||
</div>
|
||||
<div class="row text-center">
|
||||
<div class="col-4">
|
||||
<div class="stat-item">
|
||||
<h3 class="text-gradient fw-bold mb-1">50+</h3>
|
||||
<p class="small text-muted mb-0">Проектов</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="stat-item">
|
||||
<h3 class="text-gradient fw-bold mb-1">3+</h3>
|
||||
<p class="small text-muted mb-0">Лет опыта</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="stat-item">
|
||||
<h3 class="text-gradient fw-bold mb-1">24/7</h3>
|
||||
<p class="small text-muted mb-0">Поддержка</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="text-center animate-float">
|
||||
<div class="position-relative">
|
||||
<!-- 3D Graphic Placeholder -->
|
||||
<div class="hero-graphic p-5">
|
||||
<div class="position-relative">
|
||||
<!-- Code Window -->
|
||||
<div class="code-window bg-dark rounded-4 p-4 mb-4 shadow-lg"
|
||||
style="transform: rotate(-5deg); max-width: 400px;">
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<div class="rounded-circle bg-danger" style="width: 12px; height: 12px;"></div>
|
||||
<div class="rounded-circle bg-warning" style="width: 12px; height: 12px;"></div>
|
||||
<div class="rounded-circle bg-success" style="width: 12px; height: 12px;"></div>
|
||||
</div>
|
||||
<div class="text-light font-monospace small">
|
||||
<div class="text-info">def create_future():</div>
|
||||
<div class="ms-3 text-success">return innovation + passion</div>
|
||||
<div class="text-warning">// SmartSolTech</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile App Preview -->
|
||||
<div class="mobile-preview bg-light rounded-4 p-3 shadow-lg position-absolute"
|
||||
style="transform: rotate(10deg); top: 50px; right: 50px; width: 200px;">
|
||||
<div class="bg-gradient rounded-3 p-3 text-white text-center">
|
||||
<i class="fas fa-mobile-alt fa-3x mb-2"></i>
|
||||
<h6 class="mb-1">Мобильные</h6>
|
||||
<p class="small mb-0 opacity-75">приложения</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating Icons -->
|
||||
<div class="floating-icon position-absolute"
|
||||
style="top: 20px; left: 20px; animation: float 2s ease-in-out infinite;">
|
||||
<div class="bg-primary rounded-3 p-3 text-white shadow">
|
||||
<i class="fab fa-react fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="floating-icon position-absolute"
|
||||
style="bottom: 100px; left: 100px; animation: float 3s ease-in-out infinite reverse;">
|
||||
<div class="bg-success rounded-3 p-3 text-white shadow">
|
||||
<i class="fab fa-python fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Services Preview Section -->
|
||||
<section class="section-padding bg-light">
|
||||
<div class="container-modern">
|
||||
<div class="text-center mb-5">
|
||||
<h2 class="display-5 fw-bold mb-3">
|
||||
Полный спектр <span class="text-gradient">IT-услуг</span>
|
||||
</h2>
|
||||
<p class="lead text-muted max-width-600 mx-auto">
|
||||
От идеи до реализации - мы предоставляем комплексные решения для вашего цифрового успеха
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="services-grid">
|
||||
{% for service in services %}
|
||||
<div class="service-card">
|
||||
<div class="service-icon">
|
||||
<i class="fas fa-{% cycle 'code' 'mobile-alt' 'paint-brush' 'server' 'chart-line' 'shield-alt' %}"></i>
|
||||
</div>
|
||||
<h4 class="mb-3">{{ service.name }}</h4>
|
||||
<p class="text-muted mb-4">{{ service.description|truncatewords:20 }}</p>
|
||||
<a href="{% url 'service_detail' service.pk %}" class="btn btn-outline-primary">
|
||||
Подробнее <i class="fas fa-arrow-right ms-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-5">
|
||||
<a href="{% url 'services' %}" class="btn btn-primary-modern btn-lg">
|
||||
<i class="fas fa-th-large me-2"></i>
|
||||
Все услуги
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Why Choose Us Section -->
|
||||
<section class="section-padding">
|
||||
<div class="container-modern">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="pe-lg-5">
|
||||
<h2 class="display-6 fw-bold mb-4">
|
||||
Ваш надежный <span class="text-gradient">IT-партнер</span>
|
||||
</h2>
|
||||
<p class="text-muted mb-4">
|
||||
Мы не просто выполняем проекты - мы создаем долгосрочные партнерские отношения
|
||||
и помогаем бизнесу расти с помощью технологий.
|
||||
</p>
|
||||
|
||||
<div class="feature-list">
|
||||
<div class="d-flex align-items-start mb-4">
|
||||
<div class="feature-icon bg-primary rounded-3 p-2 me-3 text-white">
|
||||
<i class="fas fa-rocket"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-2">Быстрая разработка</h5>
|
||||
<p class="text-muted mb-0">Agile-методология и современные инструменты для быстрой доставки результата</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-start mb-4">
|
||||
<div class="feature-icon bg-success rounded-3 p-2 me-3 text-white">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-2">Высокое качество</h5>
|
||||
<p class="text-muted mb-0">Тщательное тестирование и code review обеспечивают надежность решений</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-start mb-4">
|
||||
<div class="feature-icon bg-warning rounded-3 p-2 me-3 text-white">
|
||||
<i class="fas fa-headset"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-2">24/7 Поддержка</h5>
|
||||
<p class="text-muted mb-0">Постоянная техническая поддержка и сопровождение проектов</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="position-relative">
|
||||
<!-- Process Steps -->
|
||||
<div class="process-steps">
|
||||
<div class="step-card active bg-white rounded-4 p-4 shadow mb-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="step-number bg-primary text-white rounded-circle me-3"
|
||||
style="width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;">
|
||||
1
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-1">Анализ требований</h6>
|
||||
<p class="small text-muted mb-0">Детальное изучение ваших потребностей</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-card bg-white rounded-4 p-4 shadow mb-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="step-number bg-secondary text-white rounded-circle me-3"
|
||||
style="width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;">
|
||||
2
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-1">Проектирование</h6>
|
||||
<p class="small text-muted mb-0">Создание архитектуры и дизайна</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-card bg-white rounded-4 p-4 shadow mb-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="step-number bg-success text-white rounded-circle me-3"
|
||||
style="width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;">
|
||||
3
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-1">Разработка</h6>
|
||||
<p class="small text-muted mb-0">Программирование и тестирование</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-card bg-white rounded-4 p-4 shadow">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="step-number bg-warning text-white rounded-circle me-3"
|
||||
style="width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;">
|
||||
4
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-1">Запуск и поддержка</h6>
|
||||
<p class="small text-muted mb-0">Деплой и техническая поддержка</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="section-padding bg-gradient text-white">
|
||||
<div class="container-modern text-center">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<h2 class="display-6 fw-bold mb-4">
|
||||
Готовы начать свой проект?
|
||||
</h2>
|
||||
<p class="lead mb-5 opacity-90">
|
||||
Свяжитесь с нами сегодня и получите бесплатную консультацию по вашему проекту
|
||||
</p>
|
||||
<div class="d-flex flex-wrap gap-3 justify-content-center">
|
||||
<a href="{% url 'services' %}" class="btn btn-light btn-lg text-primary">
|
||||
<i class="fas fa-comments me-2"></i>
|
||||
Получить консультацию
|
||||
</a>
|
||||
<a href="tel:+82-10-XXXX-XXXX" class="btn btn-outline-light btn-lg">
|
||||
<i class="fas fa-phone me-2"></i>
|
||||
Позвонить сейчас
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Animate elements on scroll
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(function(entries) {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('animate-fade-in-up');
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// Observe service cards
|
||||
document.querySelectorAll('.service-card, .step-card').forEach(card => {
|
||||
observer.observe(card);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.max-width-600 {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.hero-graphic {
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.code-window {
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.floating-icon {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.step-card {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.6s ease;
|
||||
}
|
||||
|
||||
.step-card.animate-fade-in-up {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero-graphic {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.code-window {
|
||||
transform: rotate(0deg) !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.mobile-preview {
|
||||
position: relative !important;
|
||||
transform: rotate(0deg) !important;
|
||||
margin-top: 1rem;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.floating-icon {
|
||||
position: relative !important;
|
||||
display: inline-block;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
207
bin/cli.sh
Executable file
@@ -0,0 +1,207 @@
|
||||
#!/bin/bash
|
||||
|
||||
# =============================================================================
|
||||
# SmartSolTech - CLI для выполнения команд в контейнере веб-приложения
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Цвета для вывода
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
RED='\033[0;31m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||
}
|
||||
|
||||
success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
info() {
|
||||
echo -e "${CYAN}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
# Имя контейнера Django
|
||||
CONTAINER_NAME="django_app"
|
||||
|
||||
# Проверка что контейнер запущен
|
||||
check_container() {
|
||||
if ! docker ps --format "table {{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then
|
||||
error "Контейнер $CONTAINER_NAME не запущен"
|
||||
warning "Запустите сервисы командой: ./bin/start.sh"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Выполнение команды в контейнере
|
||||
run_in_container() {
|
||||
local cmd="$*"
|
||||
log "Выполнение в контейнере: $cmd"
|
||||
docker exec -it $CONTAINER_NAME $cmd
|
||||
}
|
||||
|
||||
# Выполнение Django команды
|
||||
run_django_command() {
|
||||
local django_cmd="$*"
|
||||
run_in_container python smartsoltech/manage.py $django_cmd
|
||||
}
|
||||
|
||||
# Выполнение команды без интерактивности
|
||||
run_in_container_quiet() {
|
||||
local cmd="$*"
|
||||
docker exec $CONTAINER_NAME $cmd
|
||||
}
|
||||
|
||||
# Django команды без интерактивности
|
||||
run_django_command_quiet() {
|
||||
local django_cmd="$*"
|
||||
run_in_container_quiet python smartsoltech/manage.py $django_cmd
|
||||
}
|
||||
|
||||
# Показать справку
|
||||
show_help() {
|
||||
echo "SmartSolTech CLI - Выполнение команд в контейнере веб-приложения"
|
||||
echo ""
|
||||
echo "Использование:"
|
||||
echo " $0 <команда> [аргументы...]"
|
||||
echo ""
|
||||
echo "Django команды:"
|
||||
echo " $0 shell # Django shell"
|
||||
echo " $0 dbshell # Database shell"
|
||||
echo " $0 migrate # Выполнить миграции"
|
||||
echo " $0 makemigrations # Создать миграции"
|
||||
echo " $0 collectstatic # Собрать статические файлы"
|
||||
echo " $0 createsuperuser # Создать суперпользователя"
|
||||
echo " $0 check # Проверка проекта"
|
||||
echo " $0 runserver # Запуск dev сервера"
|
||||
echo ""
|
||||
echo "Системные команды:"
|
||||
echo " $0 bash # Bash оболочка в контейнере"
|
||||
echo " $0 sh # Sh оболочка в контейнере"
|
||||
echo " $0 ps # Список процессов в контейнере"
|
||||
echo " $0 logs [lines] # Логи приложения (по умолчанию 50 строк)"
|
||||
echo ""
|
||||
echo "Пользовательские команды:"
|
||||
echo " $0 manage <django_command> # Произвольная Django команда"
|
||||
echo " $0 exec <system_command> # Произвольная системная команда"
|
||||
echo ""
|
||||
echo "Специальные команды:"
|
||||
echo " $0 status # Статус контейнеров"
|
||||
echo " $0 restart # Перезапуск веб-контейнера"
|
||||
echo " $0 --help # Показать эту справку"
|
||||
echo ""
|
||||
echo "Примеры:"
|
||||
echo " $0 shell # Django shell"
|
||||
echo " $0 manage showmigrations # Показать статус миграций"
|
||||
echo " $0 exec cat /app/requirements.txt # Показать зависимости"
|
||||
echo " $0 logs 100 # Последние 100 строк логов"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Проверка параметров и выполнение команд
|
||||
case "${1:-}" in
|
||||
--help|-h|help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
|
||||
# Django команды
|
||||
shell|dbshell|migrate|makemigrations|collectstatic|createsuperuser|check|runserver)
|
||||
check_container
|
||||
run_django_command "$@"
|
||||
;;
|
||||
|
||||
# Системные команды
|
||||
bash|sh)
|
||||
check_container
|
||||
run_in_container "$@"
|
||||
;;
|
||||
|
||||
ps)
|
||||
check_container
|
||||
run_in_container_quiet ps aux
|
||||
;;
|
||||
|
||||
logs)
|
||||
check_container
|
||||
lines="${2:-50}"
|
||||
info "Показываем последние $lines строк логов..."
|
||||
docker logs --tail=$lines $CONTAINER_NAME
|
||||
;;
|
||||
|
||||
# Пользовательские команды
|
||||
manage)
|
||||
check_container
|
||||
shift # убираем 'manage' из аргументов
|
||||
run_django_command "$@"
|
||||
;;
|
||||
|
||||
exec)
|
||||
check_container
|
||||
shift # убираем 'exec' из аргументов
|
||||
run_in_container "$@"
|
||||
;;
|
||||
|
||||
# Специальные команды
|
||||
status)
|
||||
echo ""
|
||||
info "Статус контейнеров:"
|
||||
docker-compose ps
|
||||
echo ""
|
||||
if docker ps --format "table {{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then
|
||||
success "Веб-контейнер $CONTAINER_NAME запущен"
|
||||
info "Процессы в контейнере:"
|
||||
docker exec $CONTAINER_NAME ps aux | head -10
|
||||
else
|
||||
warning "Веб-контейнер $CONTAINER_NAME не запущен"
|
||||
fi
|
||||
;;
|
||||
|
||||
restart)
|
||||
log "Перезапуск веб-контейнера..."
|
||||
docker-compose restart web
|
||||
success "Веб-контейнер перезапущен"
|
||||
;;
|
||||
|
||||
"")
|
||||
error "Не указана команда"
|
||||
echo ""
|
||||
echo "Используйте '$0 --help' для справки"
|
||||
echo ""
|
||||
echo "Быстрые команды:"
|
||||
echo " $0 shell # Django shell"
|
||||
echo " $0 bash # Bash в контейнере"
|
||||
echo " $0 status # Статус сервисов"
|
||||
exit 1
|
||||
;;
|
||||
|
||||
*)
|
||||
# Пробуем выполнить как Django команду
|
||||
check_container
|
||||
log "Попытка выполнить как Django команду: $*"
|
||||
if run_django_command_quiet help "$1" >/dev/null 2>&1; then
|
||||
run_django_command "$@"
|
||||
else
|
||||
error "Неизвестная команда: $1"
|
||||
echo ""
|
||||
echo "Попробуйте:"
|
||||
echo " $0 --help # Полная справка"
|
||||
echo " $0 manage help # Список Django команд"
|
||||
echo " $0 exec $* # Выполнить как системную команду"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
45
bin/demo.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
|
||||
# =============================================================================
|
||||
# SmartSolTech - Пример использования скриптов обновления
|
||||
# =============================================================================
|
||||
|
||||
echo "🚀 Демонстрация скриптов управления SmartSolTech"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
|
||||
echo "📋 Доступные удаленные репозитории:"
|
||||
git remote -v
|
||||
echo ""
|
||||
|
||||
echo "📊 Текущий статус проекта:"
|
||||
./update.sh --status
|
||||
echo ""
|
||||
|
||||
echo "💡 Примеры команд обновления:"
|
||||
echo ""
|
||||
echo " # Стандартное обновление из основного репозитория"
|
||||
echo " ./update.sh"
|
||||
echo ""
|
||||
echo " # Обновление из резервного репозитория"
|
||||
echo " ./update.sh backup"
|
||||
echo ""
|
||||
echo " # Обновление из основного с созданием бэкапа в резервном"
|
||||
echo " ./update.sh origin backup"
|
||||
echo ""
|
||||
echo " # Обновление из резервного с бэкапом в основной"
|
||||
echo " ./update.sh backup origin"
|
||||
echo ""
|
||||
|
||||
echo "🛠️ Другие полезные команды:"
|
||||
echo ""
|
||||
echo " ./start.sh # Быстрый запуск сервисов"
|
||||
echo " ./stop.sh # Остановка сервисов"
|
||||
echo " ./stop.sh --clean # Полная очистка"
|
||||
echo " ./logs.sh # Просмотр логов"
|
||||
echo " ./logs.sh web # Логи веб-сервера"
|
||||
echo ""
|
||||
|
||||
echo "📚 Полная документация: cat SCRIPTS_README.md"
|
||||
echo ""
|
||||
echo "✅ Готово!"
|
||||
85
bin/logs.sh
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
|
||||
# =============================================================================
|
||||
# SmartSolTech - Скрипт для отображения логов
|
||||
# =============================================================================
|
||||
|
||||
# Цвета для вывода
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||
}
|
||||
|
||||
success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
# Проверка docker-compose.yml
|
||||
if [ ! -f "docker-compose.yml" ]; then
|
||||
error "docker-compose.yml не найден. Запустите скрипт из корня проекта."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Обработка параметров
|
||||
case "${1:-}" in
|
||||
--help|-h)
|
||||
echo "SmartSolTech - Скрипт для отображения логов"
|
||||
echo ""
|
||||
echo "Использование:"
|
||||
echo " $0 - Показать все логи в реальном времени"
|
||||
echo " $0 web - Показать только логи веб-сервера"
|
||||
echo " $0 db - Показать только логи базы данных"
|
||||
echo " $0 pgadmin - Показать только логи PgAdmin"
|
||||
echo " $0 --tail 50 - Показать последние 50 строк"
|
||||
echo " $0 --help - Показать эту справку"
|
||||
echo ""
|
||||
exit 0
|
||||
;;
|
||||
--tail)
|
||||
if [ -z "$2" ]; then
|
||||
error "Укажите количество строк для --tail"
|
||||
exit 1
|
||||
fi
|
||||
log "Показываем последние $2 строк логов..."
|
||||
docker-compose logs --tail=$2
|
||||
;;
|
||||
web|db|pgadmin)
|
||||
log "Показываем логи сервиса $1..."
|
||||
warning "Для выхода нажмите Ctrl+C"
|
||||
sleep 2
|
||||
docker-compose logs -f $1
|
||||
;;
|
||||
"")
|
||||
echo ""
|
||||
echo "📋 SmartSolTech - Логи сервисов"
|
||||
echo "==============================="
|
||||
echo ""
|
||||
|
||||
log "Статус сервисов:"
|
||||
docker-compose ps
|
||||
echo ""
|
||||
|
||||
log "Показываем все логи в реальном времени..."
|
||||
warning "Для выхода нажмите Ctrl+C"
|
||||
sleep 2
|
||||
docker-compose logs -f
|
||||
;;
|
||||
*)
|
||||
error "Неизвестный параметр: $1"
|
||||
echo "Используйте --help для справки"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
157
bin/setup-backup.sh
Executable file
@@ -0,0 +1,157 @@
|
||||
#!/bin/bash
|
||||
|
||||
# =============================================================================
|
||||
# SmartSolTech - Настройка резервного репозитория
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Цвета для вывода
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||
}
|
||||
|
||||
success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "🔧 SmartSolTech - Настройка backup репозитория"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
|
||||
# Проверка что мы в Git репозитории
|
||||
if [ ! -d ".git" ]; then
|
||||
error "Не найден Git репозиторий. Запустите скрипт из корня проекта."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Показать текущие репозитории
|
||||
log "Текущие удаленные репозитории:"
|
||||
git remote -v
|
||||
echo ""
|
||||
|
||||
# Функция добавления нового backup репозитория
|
||||
add_backup_repo() {
|
||||
local repo_url="$1"
|
||||
local repo_name="${2:-backup}"
|
||||
|
||||
log "Добавление backup репозитория..."
|
||||
|
||||
# Проверяем не существует ли уже такой remote
|
||||
if git remote | grep -q "^${repo_name}$"; then
|
||||
warning "Репозиторий $repo_name уже существует"
|
||||
git remote -v | grep "^${repo_name}"
|
||||
read -p "Заменить? (y/N): " replace_choice
|
||||
if [[ $replace_choice =~ ^[Yy]$ ]]; then
|
||||
git remote remove $repo_name
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Добавляем новый remote
|
||||
if git remote add $repo_name "$repo_url"; then
|
||||
success "Backup репозиторий $repo_name добавлен"
|
||||
|
||||
# Проверяем подключение
|
||||
log "Проверка подключения..."
|
||||
if git ls-remote $repo_name > /dev/null 2>&1; then
|
||||
success "Подключение к $repo_name работает"
|
||||
|
||||
# Предлагаем сделать первый push
|
||||
read -p "Отправить текущее состояние в backup? (y/N): " push_choice
|
||||
if [[ $push_choice =~ ^[Yy]$ ]]; then
|
||||
git push $repo_name master
|
||||
success "Backup создан в $repo_name"
|
||||
fi
|
||||
else
|
||||
error "Не удалось подключиться к $repo_name"
|
||||
git remote remove $repo_name
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
error "Не удалось добавить backup репозиторий"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Обработка параметров
|
||||
case "${1:-}" in
|
||||
--help|-h)
|
||||
echo "Использование:"
|
||||
echo " $0 # Интерактивная настройка"
|
||||
echo " $0 <URL> # Добавить backup репозиторий"
|
||||
echo " $0 <URL> <name> # Добавить с именем"
|
||||
echo " $0 --remove <name> # Удалить backup репозиторий"
|
||||
echo " $0 --list # Показать все репозитории"
|
||||
echo ""
|
||||
echo "Примеры:"
|
||||
echo " $0 git@github.com:user/backup.git"
|
||||
echo " $0 git@server.com:backup.git mybkp"
|
||||
echo " $0 --remove backup"
|
||||
echo ""
|
||||
exit 0
|
||||
;;
|
||||
--list)
|
||||
log "Все удаленные репозитории:"
|
||||
git remote -v
|
||||
exit 0
|
||||
;;
|
||||
--remove)
|
||||
if [ -z "$2" ]; then
|
||||
error "Укажите имя репозитория для удаления"
|
||||
exit 1
|
||||
fi
|
||||
log "Удаление репозитория $2..."
|
||||
if git remote remove "$2"; then
|
||||
success "Репозиторий $2 удален"
|
||||
else
|
||||
error "Не удалось удалить репозиторий $2"
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
"")
|
||||
# Интерактивный режим
|
||||
echo "💡 Примеры URL репозиториев:"
|
||||
echo " git@github.com:username/repo-backup.git"
|
||||
echo " ssh://git@server.com:2222/user/backup.git"
|
||||
echo " https://github.com/username/repo-backup.git"
|
||||
echo ""
|
||||
|
||||
read -p "Введите URL backup репозитория: " repo_url
|
||||
if [ -z "$repo_url" ]; then
|
||||
warning "URL не указан, выход"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
read -p "Введите имя (backup): " repo_name
|
||||
repo_name="${repo_name:-backup}"
|
||||
|
||||
add_backup_repo "$repo_url" "$repo_name"
|
||||
;;
|
||||
*)
|
||||
# URL передан как параметр
|
||||
add_backup_repo "$1" "$2"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
log "Обновленный список репозиториев:"
|
||||
git remote -v
|
||||
echo ""
|
||||
success "Готово!"
|
||||
59
bin/setup-docker-compose-compatibility.sh
Normal file
@@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Скрипт для настройки совместимости docker-compose с Docker Compose v2
|
||||
# Запускать на продакшн сервере с правами sudo
|
||||
|
||||
echo "🐳 Setting up docker-compose compatibility..."
|
||||
|
||||
# Проверяем, есть ли уже docker-compose
|
||||
if command -v docker-compose >/dev/null 2>&1; then
|
||||
echo "✅ docker-compose уже доступен:"
|
||||
docker-compose --version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Проверяем наличие docker compose v2
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
echo "❌ Docker Compose v2 не найден. Установите Docker сначала."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📦 Docker Compose v2 обнаружен:"
|
||||
docker compose version
|
||||
|
||||
# Пытаемся найти путь к docker-compose plugin
|
||||
COMPOSE_PLUGIN_PATH=""
|
||||
for path in "/usr/libexec/docker/cli-plugins/docker-compose" "/usr/local/lib/docker/cli-plugins/docker-compose" "/opt/docker/cli-plugins/docker-compose"; do
|
||||
if [ -f "$path" ]; then
|
||||
COMPOSE_PLUGIN_PATH="$path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Если найден plugin, создаем symlink
|
||||
if [ -n "$COMPOSE_PLUGIN_PATH" ]; then
|
||||
echo "🔗 Создаем symlink из $COMPOSE_PLUGIN_PATH"
|
||||
sudo ln -sf "$COMPOSE_PLUGIN_PATH" /usr/local/bin/docker-compose
|
||||
else
|
||||
# Создаем wrapper скрипт
|
||||
echo "📝 Создаем wrapper скрипт..."
|
||||
sudo tee /usr/local/bin/docker-compose > /dev/null << 'EOF'
|
||||
#!/bin/bash
|
||||
# Docker Compose v1 compatibility wrapper
|
||||
exec docker compose "$@"
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Делаем исполняемым
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
# Проверяем результат
|
||||
if command -v docker-compose >/dev/null 2>&1; then
|
||||
echo "✅ Успешно! docker-compose теперь доступен:"
|
||||
docker-compose --version
|
||||
else
|
||||
echo "❌ Что-то пошло не так. Проверьте настройки PATH."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🎉 Настройка завершена!"
|
||||
67
bin/start.sh
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
|
||||
# =============================================================================
|
||||
# SmartSolTech - Быстрый скрипт запуска разработки
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Цвета для вывода
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||
}
|
||||
|
||||
success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "🚀 SmartSolTech - Быстрый запуск"
|
||||
echo "==============================="
|
||||
echo ""
|
||||
|
||||
# Проверка docker-compose.yml
|
||||
if [ ! -f "docker-compose.yml" ]; then
|
||||
echo "❌ docker-compose.yml не найден. Запустите скрипт из корня проекта."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Запуск контейнеров
|
||||
log "Запуск сервисов..."
|
||||
docker-compose up -d
|
||||
|
||||
# Ожидание готовности
|
||||
log "Ожидание готовности сервисов..."
|
||||
sleep 10
|
||||
|
||||
# Статус
|
||||
echo ""
|
||||
log "Статус сервисов:"
|
||||
docker-compose ps
|
||||
|
||||
echo ""
|
||||
success "Сервисы запущены!"
|
||||
echo ""
|
||||
echo "📊 Доступные ресурсы:"
|
||||
echo " • Веб-сайт: http://localhost:8000"
|
||||
echo " • Админка: http://localhost:8000/admin"
|
||||
echo " • PgAdmin: http://localhost:8080"
|
||||
echo ""
|
||||
|
||||
# Предложение показать логи
|
||||
read -p "Показать логи в реальном времени? (y/N): " show_logs_choice
|
||||
if [[ $show_logs_choice =~ ^[Yy]$ ]]; then
|
||||
echo ""
|
||||
warning "Для выхода из логов нажмите Ctrl+C"
|
||||
sleep 2
|
||||
docker-compose logs -f
|
||||
fi
|
||||
91
bin/stop.sh
Executable file
@@ -0,0 +1,91 @@
|
||||
#!/bin/bash
|
||||
|
||||
# =============================================================================
|
||||
# SmartSolTech - Скрипт остановки сервисов
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Цвета для вывода
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||
}
|
||||
|
||||
success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "🛑 SmartSolTech - Остановка сервисов"
|
||||
echo "==================================="
|
||||
echo ""
|
||||
|
||||
# Проверка docker-compose.yml
|
||||
if [ ! -f "docker-compose.yml" ]; then
|
||||
error "docker-compose.yml не найден. Запустите скрипт из корня проекта."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Показать текущий статус
|
||||
log "Текущий статус сервисов:"
|
||||
docker-compose ps
|
||||
|
||||
echo ""
|
||||
|
||||
# Остановка сервисов
|
||||
log "Остановка сервисов..."
|
||||
|
||||
# Проверяем параметры
|
||||
case "${1:-}" in
|
||||
--remove|-r)
|
||||
log "Удаление контейнеров и волюмов..."
|
||||
docker-compose down -v --remove-orphans
|
||||
success "Сервисы остановлены, контейнеры и волюмы удалены"
|
||||
;;
|
||||
--clean|-c)
|
||||
log "Полная очистка (контейнеры, образы, волюмы)..."
|
||||
docker-compose down -v --rmi all --remove-orphans
|
||||
success "Сервисы остановлены, все ресурсы очищены"
|
||||
;;
|
||||
--help|-h)
|
||||
echo "SmartSolTech - Скрипт остановки сервисов"
|
||||
echo ""
|
||||
echo "Использование:"
|
||||
echo " $0 - Простая остановка контейнеров"
|
||||
echo " $0 --remove - Остановка + удаление контейнеров и волюмов"
|
||||
echo " $0 --clean - Полная очистка (контейнеры + образы + волюмы)"
|
||||
echo " $0 --help - Показать эту справку"
|
||||
echo ""
|
||||
exit 0
|
||||
;;
|
||||
"")
|
||||
docker-compose down
|
||||
success "Сервисы остановлены"
|
||||
;;
|
||||
*)
|
||||
error "Неизвестный параметр: $1"
|
||||
echo "Используйте --help для справки"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
log "Итоговый статус:"
|
||||
docker-compose ps
|
||||
|
||||
echo ""
|
||||
success "Готово!"
|
||||
370
bin/update.sh
Executable file
@@ -0,0 +1,370 @@
|
||||
#!/bin/bash
|
||||
|
||||
# =============================================================================
|
||||
# SmartSolTech - Скрипт автоматического обновления
|
||||
# =============================================================================
|
||||
|
||||
set -e # Выход при любой ошибке
|
||||
|
||||
# Цвета для вывода
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Функция для логирования
|
||||
log() {
|
||||
echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"
|
||||
}
|
||||
|
||||
success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
# Функция проверки зависимостей
|
||||
check_dependencies() {
|
||||
log "Проверка зависимостей..."
|
||||
|
||||
if ! command -v git &> /dev/null; then
|
||||
error "Git не установлен"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker &> /dev/null; then
|
||||
error "Docker не установлен"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
error "Docker Compose не установлен"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
success "Все зависимости найдены"
|
||||
}
|
||||
|
||||
# Функция очистки staticfiles
|
||||
cleanup_staticfiles() {
|
||||
log "Очистка staticfiles..."
|
||||
|
||||
if [ -d "smartsoltech/staticfiles" ]; then
|
||||
warning "Найдена папка staticfiles, удаляем..."
|
||||
chmod -R 755 smartsoltech/staticfiles 2>/dev/null || true
|
||||
rm -rf smartsoltech/staticfiles
|
||||
success "Staticfiles очищены"
|
||||
fi
|
||||
}
|
||||
|
||||
# Функция сохранения локальных изменений
|
||||
save_local_changes() {
|
||||
log "Проверка локальных изменений..."
|
||||
|
||||
# Проверяем есть ли изменения в рабочей директории или индексе
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
warning "Обнаружены локальные изменения, сохраняем в коммит..."
|
||||
|
||||
# Добавляем все изменения
|
||||
git add .
|
||||
|
||||
# Создаем коммит с временной меткой
|
||||
local commit_msg="Auto commit before update $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
if git commit -m "$commit_msg"; then
|
||||
success "Локальные изменения сохранены в коммит"
|
||||
log "Коммит: $commit_msg"
|
||||
else
|
||||
warning "Не удалось создать коммит (возможно нет изменений для коммита)"
|
||||
fi
|
||||
else
|
||||
log "Локальных изменений не обнаружено"
|
||||
fi
|
||||
}
|
||||
update_code() {
|
||||
local remote_name="${1:-origin}"
|
||||
log "Обновление кода из репозитория $remote_name..."
|
||||
|
||||
# Проверяем существование удаленного репозитория
|
||||
if ! git remote | grep -q "^${remote_name}$"; then
|
||||
error "Удаленный репозиторий '$remote_name' не найден"
|
||||
log "Доступные репозитории:"
|
||||
git remote -v
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Настраиваем стратегию pull если не настроено
|
||||
if [ -z "$(git config pull.rebase 2>/dev/null)" ]; then
|
||||
log "Настраиваем стратегию Git pull..."
|
||||
git config pull.rebase false
|
||||
fi
|
||||
|
||||
# Получаем обновления
|
||||
log "Получение обновлений из $remote_name..."
|
||||
git fetch $remote_name
|
||||
|
||||
# Обновляем текущую ветку
|
||||
local current_branch
|
||||
current_branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
# Пробуем обновить с обработкой конфликтов
|
||||
if ! git pull $remote_name $current_branch; then
|
||||
error "Не удалось обновить код. Возможно есть конфликты."
|
||||
log "Попробуйте выполнить команды вручную:"
|
||||
log " git status"
|
||||
log " git merge --abort # если нужно отменить"
|
||||
log " git pull $remote_name $current_branch"
|
||||
return 1
|
||||
fi
|
||||
|
||||
success "Код обновлен с ветки $current_branch из $remote_name"
|
||||
}
|
||||
|
||||
# Функция остановки контейнеров
|
||||
stop_containers() {
|
||||
log "Остановка контейнеров..."
|
||||
|
||||
if docker-compose ps | grep -q "Up"; then
|
||||
docker-compose down
|
||||
success "Контейнеры остановлены"
|
||||
else
|
||||
warning "Контейнеры уже остановлены"
|
||||
fi
|
||||
}
|
||||
|
||||
# Функция сборки образов
|
||||
build_images() {
|
||||
log "Сборка Docker образов..."
|
||||
|
||||
# Принудительная пересборка
|
||||
docker-compose build --no-cache
|
||||
|
||||
success "Образы собраны"
|
||||
}
|
||||
|
||||
# Функция запуска контейнеров
|
||||
start_containers() {
|
||||
log "Запуск контейнеров..."
|
||||
|
||||
# Запуск в фоновом режиме
|
||||
docker-compose up -d
|
||||
|
||||
# Ожидание готовности
|
||||
log "Ожидание готовности сервисов..."
|
||||
sleep 10
|
||||
|
||||
# Проверка статуса
|
||||
if docker-compose ps | grep -q "Exit"; then
|
||||
error "Некоторые контейнеры завершились с ошибкой"
|
||||
docker-compose ps
|
||||
return 1
|
||||
fi
|
||||
|
||||
success "Контейнеры запущены"
|
||||
}
|
||||
|
||||
# Функция выполнения миграций
|
||||
run_migrations() {
|
||||
log "Выполнение миграций Django..."
|
||||
|
||||
# Ожидание готовности БД
|
||||
log "Ожидание готовности базы данных..."
|
||||
sleep 15
|
||||
|
||||
# Выполнение миграций
|
||||
docker-compose exec -T web python smartsoltech/manage.py migrate
|
||||
|
||||
success "Миграции выполнены"
|
||||
}
|
||||
|
||||
# Функция сбора статических файлов
|
||||
collect_static() {
|
||||
log "Сбор статических файлов..."
|
||||
|
||||
docker-compose exec -T web python smartsoltech/manage.py collectstatic --noinput
|
||||
|
||||
success "Статические файлы собраны"
|
||||
}
|
||||
|
||||
# Функция проверки состояния сервисов
|
||||
health_check() {
|
||||
log "Проверка состояния сервисов..."
|
||||
|
||||
# Проверка веб-сервера
|
||||
local max_attempts=30
|
||||
local attempt=1
|
||||
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if curl -sf http://localhost:8000/ > /dev/null 2>&1; then
|
||||
success "Веб-сервер доступен"
|
||||
break
|
||||
fi
|
||||
|
||||
if [ $attempt -eq $max_attempts ]; then
|
||||
error "Веб-сервер недоступен после $max_attempts попыток"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "Попытка $attempt/$max_attempts - ожидание веб-сервера..."
|
||||
sleep 5
|
||||
((attempt++))
|
||||
done
|
||||
|
||||
# Показать статус контейнеров
|
||||
echo ""
|
||||
log "Статус контейнеров:"
|
||||
docker-compose ps
|
||||
|
||||
success "Проверка здоровья завершена"
|
||||
}
|
||||
|
||||
# Функция отображения логов
|
||||
show_logs() {
|
||||
log "Последние логи сервисов:"
|
||||
echo ""
|
||||
docker-compose logs --tail=20 web
|
||||
}
|
||||
|
||||
# Функция бэкапа в удаленный репозиторий
|
||||
backup_to_remote() {
|
||||
local backup_remote="${1:-backup}"
|
||||
|
||||
# Пропускаем если это тот же репозиторий что используется для обновления
|
||||
local update_remote="${2:-origin}"
|
||||
if [ "$backup_remote" = "$update_remote" ]; then
|
||||
log "Пропускаем бэкап - используется тот же репозиторий для обновления"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Создание бэкапа в удаленном репозитории $backup_remote..."
|
||||
|
||||
if git remote | grep -q "^${backup_remote}$"; then
|
||||
# Пушим текущее состояние в backup (изменения уже сохранены в коммит)
|
||||
if git push $backup_remote master; then
|
||||
success "Бэкап создан в удаленном репозитории $backup_remote"
|
||||
else
|
||||
warning "Не удалось создать бэкап в $backup_remote"
|
||||
fi
|
||||
else
|
||||
warning "$backup_remote репозиторий не настроен, пропускаем бэкап"
|
||||
fi
|
||||
}
|
||||
|
||||
# Главная функция
|
||||
main() {
|
||||
local remote_source="${1:-origin}"
|
||||
local backup_remote="${2:-backup}"
|
||||
|
||||
echo ""
|
||||
echo "🚀 SmartSolTech - Автоматическое обновление"
|
||||
echo "=========================================="
|
||||
echo "📡 Источник: $remote_source"
|
||||
if git remote | grep -q "^${backup_remote}$"; then
|
||||
echo "💾 Бэкап: $backup_remote"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Проверка что мы в правильной директории
|
||||
if [ ! -f "docker-compose.yml" ]; then
|
||||
error "docker-compose.yml не найден. Запустите скрипт из корня проекта."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local start_time
|
||||
start_time=$(date +%s)
|
||||
|
||||
# Выполняем все этапы
|
||||
check_dependencies
|
||||
save_local_changes
|
||||
backup_to_remote "$backup_remote" "$remote_source"
|
||||
cleanup_staticfiles
|
||||
update_code "$remote_source"
|
||||
stop_containers
|
||||
build_images
|
||||
start_containers
|
||||
run_migrations
|
||||
collect_static
|
||||
health_check
|
||||
|
||||
local end_time
|
||||
end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
|
||||
echo ""
|
||||
echo "🎉 Обновление завершено успешно!"
|
||||
echo "⏱️ Время выполнения: ${duration} секунд"
|
||||
echo "📡 Источник обновления: $remote_source"
|
||||
echo ""
|
||||
echo "📊 Полезная информация:"
|
||||
echo " • Веб-сайт: http://localhost:8000"
|
||||
echo " • Админка: http://localhost:8000/admin"
|
||||
echo " • PgAdmin: http://localhost:8080"
|
||||
echo ""
|
||||
|
||||
# Предложение показать логи
|
||||
read -p "Показать логи сервисов? (y/N): " show_logs_choice
|
||||
if [[ $show_logs_choice =~ ^[Yy]$ ]]; then
|
||||
show_logs
|
||||
fi
|
||||
|
||||
success "Готово!"
|
||||
}
|
||||
|
||||
# Обработка прерываний
|
||||
trap 'echo ""; error "Обновление прервано пользователем"; exit 130' INT TERM
|
||||
|
||||
# Обработка ошибок
|
||||
error_handler() {
|
||||
local line_no=$1
|
||||
error "Ошибка на строке $line_no"
|
||||
echo ""
|
||||
echo "Для диагностики можете выполнить:"
|
||||
echo " docker-compose logs"
|
||||
echo " docker-compose ps"
|
||||
exit 1
|
||||
}
|
||||
|
||||
trap 'error_handler $LINENO' ERR
|
||||
|
||||
# Запуск с параметрами
|
||||
case "${1:-}" in
|
||||
--help|-h)
|
||||
echo "SmartSolTech - Скрипт автоматического обновления"
|
||||
echo ""
|
||||
echo "Использование:"
|
||||
echo " $0 - Полное обновление из origin (по умолчанию)"
|
||||
echo " $0 origin - Обновление из origin репозитория"
|
||||
echo " $0 backup - Обновление из backup репозитория"
|
||||
echo " $0 origin backup - Обновление из origin с бэкапом в backup"
|
||||
echo " $0 backup origin - Обновление из backup с бэкапом в origin"
|
||||
echo " $0 --help - Показать эту справку"
|
||||
echo " $0 --logs - Показать логи без обновления"
|
||||
echo " $0 --status - Показать статус без обновления"
|
||||
echo ""
|
||||
echo "Примеры:"
|
||||
echo " $0 # обновление из origin"
|
||||
echo " $0 backup # обновление из backup репозитория"
|
||||
echo " $0 origin backup # обновление из origin, бэкап в backup"
|
||||
echo ""
|
||||
exit 0
|
||||
;;
|
||||
--logs)
|
||||
show_logs
|
||||
exit 0
|
||||
;;
|
||||
--status)
|
||||
docker-compose ps
|
||||
health_check
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
# Передаем все параметры в main
|
||||
main "$@"
|
||||
;;
|
||||
esac
|
||||
45
create_test_data.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import django
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'smartsoltech.settings')
|
||||
django.setup()
|
||||
|
||||
from web.models import ProjectCategory, Project
|
||||
|
||||
# Создаём категорию
|
||||
cat, created = ProjectCategory.objects.get_or_create(
|
||||
slug='web-development',
|
||||
defaults={
|
||||
'name': 'Веб-разработка',
|
||||
'description': 'Разработка современных веб-приложений',
|
||||
'icon': 'fas fa-laptop-code',
|
||||
'order': 1,
|
||||
'is_active': True
|
||||
}
|
||||
)
|
||||
print(f"{'Создана' if created else 'Найдена'} категория: {cat.name}")
|
||||
|
||||
# Обновляем первый проект
|
||||
project = Project.objects.first()
|
||||
if project:
|
||||
project.short_description = 'Корпоративный сайт SmartSolTech с современным дизайном'
|
||||
project.description = '<h2>О проекте</h2><p>Разработка корпоративного сайта с использованием Django и современного дизайна.</p><h3>Особенности</h3><ul><li>Адаптивный дизайн</li><li>Админ-панель</li><li>Интеграция с Telegram</li></ul>'
|
||||
if not project.slug:
|
||||
project.slug = 'smartsoltech-website'
|
||||
project.technologies = 'Python, Django, PostgreSQL, Bootstrap, JavaScript'
|
||||
project.duration = '3 месяца'
|
||||
project.team_size = 4
|
||||
project.is_featured = True
|
||||
project.display_order = 1
|
||||
project.save()
|
||||
project.categories.add(cat)
|
||||
print(f"Обновлён проект: {project.name}")
|
||||
print(f"URL: /project/{project.pk}/")
|
||||
else:
|
||||
print("Проектов не найдено")
|
||||
|
||||
print("\n=== Статистика ===")
|
||||
print(f"Категорий: {ProjectCategory.objects.count()}")
|
||||
print(f"Проектов: {Project.objects.count()}")
|
||||
print(f"Завершённых проектов: {Project.objects.filter(status='completed').count()}")
|
||||
192
create_test_projects.py
Normal file
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
from datetime import datetime, date
|
||||
|
||||
# Настройка Django
|
||||
sys.path.append('/app/smartsoltech')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'smartsoltech.settings')
|
||||
django.setup()
|
||||
|
||||
from web.models import Project, Category, Client, Service, Order
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
def create_test_projects():
|
||||
# Создаем или получаем категории
|
||||
categories = []
|
||||
|
||||
cat_web, _ = Category.objects.get_or_create(
|
||||
slug='web-development',
|
||||
defaults={
|
||||
'name': 'Веб-разработка',
|
||||
'icon': 'fas fa-code',
|
||||
'order': 1,
|
||||
'description': 'Создание веб-сайтов и приложений'
|
||||
}
|
||||
)
|
||||
categories.append(cat_web)
|
||||
|
||||
cat_mobile, _ = Category.objects.get_or_create(
|
||||
slug='mobile-apps',
|
||||
defaults={
|
||||
'name': 'Мобильные приложения',
|
||||
'icon': 'fas fa-mobile-alt',
|
||||
'order': 2,
|
||||
'description': 'Разработка iOS и Android приложений'
|
||||
}
|
||||
)
|
||||
categories.append(cat_mobile)
|
||||
|
||||
cat_design, _ = Category.objects.get_or_create(
|
||||
slug='design',
|
||||
defaults={
|
||||
'name': 'Дизайн',
|
||||
'icon': 'fas fa-palette',
|
||||
'order': 3,
|
||||
'description': 'UI/UX дизайн и брендинг'
|
||||
}
|
||||
)
|
||||
categories.append(cat_design)
|
||||
|
||||
cat_analytics, _ = Category.objects.get_or_create(
|
||||
slug='analytics',
|
||||
defaults={
|
||||
'name': 'Аналитика',
|
||||
'icon': 'fas fa-chart-bar',
|
||||
'order': 4,
|
||||
'description': 'Системы аналитики и отчетности'
|
||||
}
|
||||
)
|
||||
categories.append(cat_analytics)
|
||||
|
||||
cat_ecommerce, _ = Category.objects.get_or_create(
|
||||
slug='ecommerce',
|
||||
defaults={
|
||||
'name': 'E-commerce',
|
||||
'icon': 'fas fa-shopping-cart',
|
||||
'order': 5,
|
||||
'description': 'Интернет-магазины и торговые платформы'
|
||||
}
|
||||
)
|
||||
categories.append(cat_ecommerce)
|
||||
|
||||
# Создаем или получаем тестового клиента
|
||||
client, _ = Client.objects.get_or_create(
|
||||
email='test@example.com',
|
||||
defaults={
|
||||
'first_name': 'Тестовый',
|
||||
'last_name': 'Клиент',
|
||||
'phone_number': '+7-900-000-0000'
|
||||
}
|
||||
)
|
||||
|
||||
# Создаем или получаем тестовую услугу
|
||||
service, _ = Service.objects.get_or_create(
|
||||
name='Разработка сайта',
|
||||
defaults={
|
||||
'description': 'Профессиональная разработка веб-сайтов',
|
||||
'price': 100000.00,
|
||||
'category': cat_web
|
||||
}
|
||||
)
|
||||
|
||||
# Тестовые данные проектов
|
||||
test_projects = [
|
||||
{
|
||||
'name': 'Корпоративный портал TechCorp',
|
||||
'short_description': 'Современный корпоративный портал с системой управления документами, интеграцией с CRM и модулем HR.',
|
||||
'description': '<p>Разработан комплексный корпоративный портал для компании TechCorp, включающий в себя систему управления документами, интеграцию с CRM-системой и модуль управления персоналом.</p><p>Основные функции: документооборот, календарь событий, внутренние новости, система заявок, интеграция с почтовыми сервисами.</p>',
|
||||
'technologies': 'Django, PostgreSQL, Redis, Celery, Docker, React.js',
|
||||
'duration': '4 месяца',
|
||||
'team_size': 5,
|
||||
'views_count': 1245,
|
||||
'likes_count': 89,
|
||||
'completion_date': date(2024, 8, 15),
|
||||
'categories': [cat_web, cat_analytics],
|
||||
'is_featured': True
|
||||
},
|
||||
{
|
||||
'name': 'Мобильное приложение FoodDelivery',
|
||||
'short_description': 'Cross-platform приложение для доставки еды с геолокацией, онлайн-платежами и системой рейтингов.',
|
||||
'description': '<p>Создано мобильное приложение для службы доставки еды с поддержкой iOS и Android платформ.</p><p>Функционал включает: поиск ресторанов по геолокации, онлайн-заказы, интеграцию с платежными системами, отслеживание курьера в реальном времени, система рейтингов и отзывов.</p>',
|
||||
'technologies': 'React Native, Node.js, MongoDB, Socket.io, Stripe API',
|
||||
'duration': '6 месяцев',
|
||||
'team_size': 4,
|
||||
'views_count': 892,
|
||||
'likes_count': 156,
|
||||
'completion_date': date(2024, 10, 20),
|
||||
'categories': [cat_mobile, cat_ecommerce],
|
||||
'is_featured': False
|
||||
},
|
||||
{
|
||||
'name': 'Аналитическая панель SmartMetrics',
|
||||
'short_description': 'Интерактивная панель управления с визуализацией данных, машинным обучением и предиктивной аналитикой.',
|
||||
'description': '<p>Разработана комплексная система аналитики для обработки больших данных с возможностями машинного обучения.</p><p>Включает: интерактивные дашборды, автоматизированные отчеты, прогнозирование трендов, интеграция с различными источниками данных, алгоритмы машинного обучения.</p>',
|
||||
'technologies': 'Python, Django, PostgreSQL, Redis, TensorFlow, D3.js, Pandas',
|
||||
'duration': '5 месяцев',
|
||||
'team_size': 6,
|
||||
'views_count': 673,
|
||||
'likes_count': 124,
|
||||
'completion_date': date(2024, 7, 10),
|
||||
'categories': [cat_analytics, cat_web],
|
||||
'is_featured': True
|
||||
},
|
||||
{
|
||||
'name': 'E-commerce платформа ShopMaster',
|
||||
'short_description': 'Полнофункциональная платформа интернет-торговли с многопользовательскими магазинами и системой управления.',
|
||||
'description': '<p>Создана масштабируемая e-commerce платформа, поддерживающая множественные магазины на одной основе.</p><p>Возможности: многопользовательская архитектура, система платежей, управление складом, программы лояльности, мобильная оптимизация, SEO инструменты.</p>',
|
||||
'technologies': 'Laravel, MySQL, Redis, Elasticsearch, Vue.js, Stripe, PayPal',
|
||||
'duration': '8 месяцев',
|
||||
'team_size': 7,
|
||||
'views_count': 1567,
|
||||
'likes_count': 203,
|
||||
'completion_date': date(2024, 11, 5),
|
||||
'categories': [cat_ecommerce, cat_web, cat_mobile],
|
||||
'is_featured': True
|
||||
},
|
||||
{
|
||||
'name': 'Дизайн-система BrandKit',
|
||||
'short_description': 'Комплексная дизайн-система для финтех стартапа с фирменным стилем, UI-компонентами и брендбуком.',
|
||||
'description': '<p>Разработана полная дизайн-система для финтех компании, включающая создание фирменного стиля, UI-компонентов и подробного брендбука.</p><p>Результат: логотип и фирменный стиль, библиотека UI-компонентов, руководство по использованию бренда, адаптация для различных платформ.</p>',
|
||||
'technologies': 'Figma, Adobe Creative Suite, Principle, Sketch, InVision',
|
||||
'duration': '3 месяца',
|
||||
'team_size': 3,
|
||||
'views_count': 445,
|
||||
'likes_count': 78,
|
||||
'completion_date': date(2024, 9, 30),
|
||||
'categories': [cat_design],
|
||||
'is_featured': False
|
||||
}
|
||||
]
|
||||
|
||||
print(f"Текущее количество проектов: {Project.objects.count()}")
|
||||
|
||||
# Создаем проекты
|
||||
for i, project_data in enumerate(test_projects):
|
||||
categories_to_add = project_data.pop('categories')
|
||||
|
||||
project, created = Project.objects.get_or_create(
|
||||
name=project_data['name'],
|
||||
defaults={
|
||||
**project_data,
|
||||
'client': client,
|
||||
'service': service,
|
||||
'status': 'completed',
|
||||
'display_order': i + 1
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
# Добавляем категории
|
||||
project.categories.set(categories_to_add)
|
||||
print(f"✅ Создан проект: {project.name}")
|
||||
else:
|
||||
print(f"⚠️ Проект уже существует: {project.name}")
|
||||
|
||||
print(f"\nИтого проектов в базе: {Project.objects.count()}")
|
||||
print(f"Завершенных проектов: {Project.objects.filter(status='completed').count()}")
|
||||
print(f"Избранных проектов: {Project.objects.filter(is_featured=True).count()}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
create_test_projects()
|
||||
55
docker-compose.test.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Тестовая база данных
|
||||
postgres_test:
|
||||
image: postgres:17-alpine
|
||||
container_name: postgres_test
|
||||
environment:
|
||||
POSTGRES_DB: smartsoltech_test
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
ports:
|
||||
- "5433:5432"
|
||||
networks:
|
||||
- test_network
|
||||
volumes:
|
||||
- test_pgdata:/var/lib/postgresql/data
|
||||
|
||||
# Тестовое Django приложение
|
||||
django_test:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.test
|
||||
container_name: django_test
|
||||
environment:
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres_test:5432/smartsoltech_test
|
||||
SECRET_KEY: test-secret-key-for-ci-very-long-and-secure-key-12345
|
||||
DEBUG: "False"
|
||||
ALLOWED_HOSTS: localhost,127.0.0.1,postgres_test,*
|
||||
DJANGO_SETTINGS_MODULE: smartsoltech.settings_test
|
||||
TELEGRAM_BOT_TOKEN: test-token-for-ci
|
||||
depends_on:
|
||||
- postgres_test
|
||||
networks:
|
||||
- test_network
|
||||
command: >
|
||||
sh -c "
|
||||
echo 'Ожидание готовности PostgreSQL...' &&
|
||||
until pg_isready -h postgres_test -p 5432 -U postgres; do
|
||||
echo 'Waiting for postgres_test...' && sleep 2;
|
||||
done &&
|
||||
echo 'Запуск миграций...' &&
|
||||
cd smartsoltech &&
|
||||
python manage.py migrate --settings=smartsoltech.settings_test &&
|
||||
echo 'Запуск тестов...' &&
|
||||
python manage.py test --settings=smartsoltech.settings_test --verbosity=2
|
||||
"
|
||||
|
||||
volumes:
|
||||
test_pgdata:
|
||||
|
||||
networks:
|
||||
test_network:
|
||||
driver: bridge
|
||||
542
docs/API.md
Normal file
@@ -0,0 +1,542 @@
|
||||
# 🤖 SmartSolTech API Documentation
|
||||
|
||||
## 📡 API Endpoints Overview
|
||||
|
||||
SmartSolTech предоставляет RESTful API для взаимодействия с системой управления заказами и интеграции с внешними сервисами.
|
||||
|
||||
### Base URL
|
||||
```
|
||||
Production: https://smartsoltech.kr/api/
|
||||
Development: http://localhost:8000/api/
|
||||
```
|
||||
|
||||
## 🔐 Аутентификация
|
||||
|
||||
### API Token Authentication
|
||||
|
||||
```http
|
||||
Authorization: Token your-api-token-here
|
||||
```
|
||||
|
||||
**Получение токена:**
|
||||
```bash
|
||||
# Через CLI
|
||||
./cli manage drf_create_token <username>
|
||||
|
||||
# Через Django shell
|
||||
./cli shell
|
||||
>>> from django.contrib.auth.models import User
|
||||
>>> from rest_framework.authtoken.models import Token
|
||||
>>> user = User.objects.get(username='admin')
|
||||
>>> token = Token.objects.create(user=user)
|
||||
>>> print(token.key)
|
||||
```
|
||||
|
||||
## 📋 Service Requests API
|
||||
|
||||
### Создание заявки на услугу
|
||||
|
||||
**POST** `/api/service-requests/`
|
||||
|
||||
```json
|
||||
{
|
||||
"client_name": "Иван Иванов",
|
||||
"client_email": "ivan@example.com",
|
||||
"client_phone": "+7 900 123-45-67",
|
||||
"service_type": "web_development",
|
||||
"description": "Разработка корпоративного сайта",
|
||||
"budget_range": "50000-100000",
|
||||
"preferred_contact": "email"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": 15,
|
||||
"client_name": "Иван Иванов",
|
||||
"client_email": "ivan@example.com",
|
||||
"client_phone": "+7 900 123-45-67",
|
||||
"service_type": "web_development",
|
||||
"description": "Разработка корпоративного сайта",
|
||||
"budget_range": "50000-100000",
|
||||
"preferred_contact": "email",
|
||||
"status": "pending",
|
||||
"qr_code_url": "/static/qr_codes/request_15.png",
|
||||
"chat_id": null,
|
||||
"created_at": "2023-11-25T10:30:00Z",
|
||||
"updated_at": "2023-11-25T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Получение списка заявок
|
||||
|
||||
**GET** `/api/service-requests/`
|
||||
|
||||
**Query Parameters:**
|
||||
- `status` - Фильтр по статусу (`pending`, `confirmed`, `in_progress`, `completed`, `cancelled`)
|
||||
- `service_type` - Фильтр по типу услуги
|
||||
- `created_after` - Заявки после определенной даты (ISO format)
|
||||
- `page` - Номер страницы
|
||||
- `page_size` - Количество элементов на странице
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Token your-token" \
|
||||
"https://smartsoltech.kr/api/service-requests/?status=pending&page=1"
|
||||
```
|
||||
|
||||
### Получение конкретной заявки
|
||||
|
||||
**GET** `/api/service-requests/{id}/`
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Token your-token" \
|
||||
"https://smartsoltech.kr/api/service-requests/15/"
|
||||
```
|
||||
|
||||
### Обновление статуса заявки
|
||||
|
||||
**PATCH** `/api/service-requests/{id}/`
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "confirmed",
|
||||
"chat_id": "123456789"
|
||||
}
|
||||
```
|
||||
|
||||
### Проверка статуса заявки (публичный endpoint)
|
||||
|
||||
**GET** `/api/check-request-status/{id}/`
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "confirmed",
|
||||
"message": "Ваша заявка подтверждена! Мы свяжемся с вами в ближайшее время."
|
||||
}
|
||||
```
|
||||
|
||||
## 🏢 Companies API
|
||||
|
||||
### Получение информации о компании
|
||||
|
||||
**GET** `/api/companies/`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "SmartSolTech",
|
||||
"description": "Инновационные IT решения для бизнеса",
|
||||
"email": "info@smartsoltech.kr",
|
||||
"phone": "+7 800 555-35-35",
|
||||
"website": "https://smartsoltech.kr",
|
||||
"address": "г. Москва, ул. Технологическая, д. 1",
|
||||
"logo": "/media/company/logo.png",
|
||||
"founded_year": 2023,
|
||||
"employees_count": "10-50",
|
||||
"specializations": [
|
||||
"Веб-разработка",
|
||||
"Мобильные приложения",
|
||||
"DevOps"
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 👥 Team API
|
||||
|
||||
### Получение команды
|
||||
|
||||
**GET** `/api/team-members/`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Алексей Петров",
|
||||
"position": "Lead Developer",
|
||||
"bio": "10+ лет в веб-разработке",
|
||||
"photo": "/media/team/alexey.jpg",
|
||||
"linkedin": "https://linkedin.com/in/alexey",
|
||||
"github": "https://github.com/alexey",
|
||||
"email": "alexey@smartsoltech.kr",
|
||||
"skills": ["Python", "Django", "React", "Docker"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 📊 Projects API
|
||||
|
||||
### Получение портфолио
|
||||
|
||||
**GET** `/api/projects/`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"title": "E-commerce платформа",
|
||||
"description": "Современная платформа интернет-торговли",
|
||||
"image": "/media/projects/ecommerce.jpg",
|
||||
"url": "https://example-shop.com",
|
||||
"category": "Веб-разработка",
|
||||
"technologies": ["Django", "React", "PostgreSQL"],
|
||||
"completion_date": "2023-10-15",
|
||||
"client": "ООО Торговый Дом",
|
||||
"status": "completed"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 📝 Blog API
|
||||
|
||||
### Получение статей блога
|
||||
|
||||
**GET** `/api/blog/posts/`
|
||||
|
||||
**Query Parameters:**
|
||||
- `category` - Фильтр по категории
|
||||
- `tag` - Фильтр по тегу
|
||||
- `published_after` - Статьи после даты
|
||||
- `search` - Поиск по заголовку и содержанию
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Тренды веб-разработки 2024",
|
||||
"slug": "web-dev-trends-2024",
|
||||
"excerpt": "Обзор основных трендов в веб-разработке",
|
||||
"content": "Полный текст статьи...",
|
||||
"author": {
|
||||
"name": "Алексей Петров",
|
||||
"photo": "/media/team/alexey.jpg"
|
||||
},
|
||||
"category": {
|
||||
"name": "Разработка",
|
||||
"slug": "development"
|
||||
},
|
||||
"tags": ["веб-разработка", "тренды", "2024"],
|
||||
"featured_image": "/media/blog/trends-2024.jpg",
|
||||
"published_at": "2023-11-20T10:00:00Z",
|
||||
"reading_time": 8,
|
||||
"views_count": 1234,
|
||||
"is_featured": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Получение конкретной статьи
|
||||
|
||||
**GET** `/api/blog/posts/{slug}/`
|
||||
|
||||
## 🏷️ Categories & Tags API
|
||||
|
||||
### Категории услуг
|
||||
|
||||
**GET** `/api/categories/`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Веб-разработка",
|
||||
"slug": "web-development",
|
||||
"description": "Создание современных веб-приложений",
|
||||
"icon": "fas fa-code",
|
||||
"services_count": 15
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Теги
|
||||
|
||||
**GET** `/api/tags/`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Django",
|
||||
"slug": "django",
|
||||
"color": "#092E20",
|
||||
"posts_count": 12
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 📞 Contact API
|
||||
|
||||
### Отправка сообщения
|
||||
|
||||
**POST** `/api/contact/`
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Анна Смирнова",
|
||||
"email": "anna@example.com",
|
||||
"subject": "Вопрос по услугам",
|
||||
"message": "Здравствуйте! Интересует разработка мобильного приложения.",
|
||||
"phone": "+7 900 123-45-67"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Сообщение успешно отправлено. Мы ответим в ближайшее время.",
|
||||
"id": 42
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 Analytics API (Admin only)
|
||||
|
||||
### Статистика заявок
|
||||
|
||||
**GET** `/api/analytics/service-requests/`
|
||||
|
||||
```json
|
||||
{
|
||||
"total_requests": 156,
|
||||
"pending_requests": 23,
|
||||
"confirmed_requests": 89,
|
||||
"completed_requests": 44,
|
||||
"requests_by_month": [
|
||||
{"month": "2023-10", "count": 45},
|
||||
{"month": "2023-11", "count": 67}
|
||||
],
|
||||
"popular_services": [
|
||||
{"service": "Веб-разработка", "count": 78},
|
||||
{"service": "Мобильные приложения", "count": 34}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Статистика сайта
|
||||
|
||||
**GET** `/api/analytics/site-stats/`
|
||||
|
||||
```json
|
||||
{
|
||||
"total_views": 12456,
|
||||
"unique_visitors": 3456,
|
||||
"popular_pages": [
|
||||
{"path": "/services/", "views": 2345},
|
||||
{"path": "/portfolio/", "views": 1876}
|
||||
],
|
||||
"referrers": [
|
||||
{"source": "google.com", "visits": 1234},
|
||||
{"source": "direct", "visits": 987}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 🤖 Telegram Integration API
|
||||
|
||||
### Webhook для Telegram Bot
|
||||
|
||||
**POST** `/api/telegram/webhook/`
|
||||
|
||||
Эндпоинт для получения обновлений от Telegram Bot API.
|
||||
|
||||
### Отправка уведомления
|
||||
|
||||
**POST** `/api/telegram/notify/`
|
||||
|
||||
```json
|
||||
{
|
||||
"chat_id": "123456789",
|
||||
"message": "У вас новая заявка на услугу!",
|
||||
"parse_mode": "HTML",
|
||||
"disable_web_page_preview": true
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Utilities API
|
||||
|
||||
### Генерация QR-кода
|
||||
|
||||
**POST** `/api/generate-qr/`
|
||||
|
||||
```json
|
||||
{
|
||||
"data": "https://smartsoltech.kr/confirm/15/",
|
||||
"size": "200x200",
|
||||
"format": "PNG"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"qr_code_url": "/static/qr_codes/custom_qr_1234567890.png",
|
||||
"expires_at": "2023-11-26T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Загрузка файлов
|
||||
|
||||
**POST** `/api/upload/`
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Token your-token" \
|
||||
-F "file=@document.pdf" \
|
||||
-F "category=documents" \
|
||||
"https://smartsoltech.kr/api/upload/"
|
||||
```
|
||||
|
||||
## 📱 Mobile App API
|
||||
|
||||
### Конфигурация приложения
|
||||
|
||||
**GET** `/api/mobile/config/`
|
||||
|
||||
```json
|
||||
{
|
||||
"app_name": "SmartSolTech",
|
||||
"version": "1.0.0",
|
||||
"api_version": "v1",
|
||||
"features": {
|
||||
"push_notifications": true,
|
||||
"offline_mode": true,
|
||||
"biometric_auth": true
|
||||
},
|
||||
"endpoints": {
|
||||
"base_url": "https://smartsoltech.kr/api/",
|
||||
"websocket_url": "wss://smartsoltech.kr/ws/"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚨 Error Handling
|
||||
|
||||
### Стандартные HTTP коды
|
||||
|
||||
- `200 OK` - Успешный запрос
|
||||
- `201 Created` - Ресурс создан
|
||||
- `400 Bad Request` - Некорректные данные
|
||||
- `401 Unauthorized` - Необходима аутентификация
|
||||
- `403 Forbidden` - Недостаточно прав
|
||||
- `404 Not Found` - Ресурс не найден
|
||||
- `429 Too Many Requests` - Превышен лимит запросов
|
||||
- `500 Internal Server Error` - Ошибка сервера
|
||||
|
||||
### Формат ошибок
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Некорректные данные",
|
||||
"details": {
|
||||
"email": ["Введите корректный email адрес"],
|
||||
"phone": ["Номер телефона обязателен"]
|
||||
},
|
||||
"timestamp": "2023-11-25T10:30:00Z",
|
||||
"request_id": "req_1234567890"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔒 Rate Limiting
|
||||
|
||||
### Лимиты для разных типов пользователей
|
||||
|
||||
- **Anonymous**: 100 запросов в час
|
||||
- **Authenticated**: 1000 запросов в час
|
||||
- **Premium**: 10000 запросов в час
|
||||
|
||||
### Headers ответа
|
||||
|
||||
```http
|
||||
X-RateLimit-Limit: 1000
|
||||
X-RateLimit-Remaining: 999
|
||||
X-RateLimit-Reset: 1635724800
|
||||
```
|
||||
|
||||
## 📚 SDK и библиотеки
|
||||
|
||||
### Python SDK
|
||||
|
||||
```python
|
||||
from smartsoltech_api import SmartSolTechClient
|
||||
|
||||
client = SmartSolTechClient(
|
||||
api_key='your-api-key',
|
||||
base_url='https://smartsoltech.kr/api/'
|
||||
)
|
||||
|
||||
# Создание заявки
|
||||
request = client.service_requests.create({
|
||||
'client_name': 'Test Client',
|
||||
'service_type': 'web_development',
|
||||
'description': 'Test request'
|
||||
})
|
||||
|
||||
# Получение статистики
|
||||
stats = client.analytics.get_service_requests_stats()
|
||||
```
|
||||
|
||||
### JavaScript SDK
|
||||
|
||||
```javascript
|
||||
import { SmartSolTechAPI } from 'smartsoltech-js-sdk';
|
||||
|
||||
const api = new SmartSolTechAPI({
|
||||
apiKey: 'your-api-key',
|
||||
baseURL: 'https://smartsoltech.kr/api/'
|
||||
});
|
||||
|
||||
// Создание заявки
|
||||
const request = await api.serviceRequests.create({
|
||||
clientName: 'Test Client',
|
||||
serviceType: 'web_development',
|
||||
description: 'Test request'
|
||||
});
|
||||
|
||||
// Получение проектов
|
||||
const projects = await api.projects.list();
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Тестирование API
|
||||
|
||||
```bash
|
||||
# Запуск тестов API
|
||||
./cli manage test api.tests
|
||||
|
||||
# Тестирование конкретного эндпоинта
|
||||
./cli manage test api.tests.test_service_requests
|
||||
|
||||
# Запуск coverage
|
||||
./cli exec coverage run --source='.' manage.py test
|
||||
./cli exec coverage report
|
||||
```
|
||||
|
||||
### Примеры запросов
|
||||
|
||||
```bash
|
||||
# Получение токена
|
||||
curl -X POST http://localhost:8000/api/auth/token/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username": "admin", "password": "password"}'
|
||||
|
||||
# Создание заявки
|
||||
curl -X POST http://localhost:8000/api/service-requests/ \
|
||||
-H "Authorization: Token your-token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"client_name": "Test Client",
|
||||
"client_email": "test@example.com",
|
||||
"service_type": "web_development",
|
||||
"description": "Test request"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
🎯 **Следующие шаги**: [Развертывание](DEPLOYMENT.md) | [Управление скриптами](SCRIPTS_README.md)
|
||||
352
docs/DEPLOYMENT.md
Normal file
@@ -0,0 +1,352 @@
|
||||
# 🚀 Руководство по развертыванию SmartSolTech
|
||||
|
||||
## 📋 Подготовка сервера
|
||||
|
||||
### Системные требования
|
||||
|
||||
- **OS**: Ubuntu 20.04+ / CentOS 8+ / Debian 11+
|
||||
- **RAM**: Минимум 2GB, рекомендуется 4GB+
|
||||
- **Storage**: Минимум 20GB свободного места
|
||||
- **Network**: Открытые порты 80, 443, 8000, 5432, 8080
|
||||
|
||||
### Установка Docker
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sh get-docker.sh
|
||||
|
||||
# Добавление пользователя в группу docker
|
||||
sudo usermod -aG docker $USER
|
||||
|
||||
# Установка Docker Compose
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/v2.21.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
```
|
||||
|
||||
## 🔧 Настройка проекта
|
||||
|
||||
### 1. Клонирование репозитория
|
||||
|
||||
```bash
|
||||
git clone https://github.com/smartsoltech/smartsoltech.kr.git
|
||||
cd smartsoltech.kr
|
||||
```
|
||||
|
||||
### 2. Конфигурация переменных окружения
|
||||
|
||||
```bash
|
||||
# Копирование примера конфигурации
|
||||
cp smartsoltech/.env.example smartsoltech/.env
|
||||
|
||||
# Редактирование конфигурации
|
||||
nano smartsoltech/.env
|
||||
```
|
||||
|
||||
**Основные переменные:**
|
||||
|
||||
```env
|
||||
DEBUG=False
|
||||
SECRET_KEY=your-super-secret-key
|
||||
DATABASE_URL=postgresql://user:password@postgres_db:5432/smartsoltech
|
||||
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
|
||||
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
||||
```
|
||||
|
||||
### 3. Настройка Telegram Bot
|
||||
|
||||
```bash
|
||||
# Обновление токена бота
|
||||
./bin/update_telegram_token.py
|
||||
|
||||
# Или через CLI
|
||||
./cli manage set_telegram_token <YOUR_BOT_TOKEN>
|
||||
```
|
||||
|
||||
## 🚀 Развертывание
|
||||
|
||||
### Автоматическое развертывание
|
||||
|
||||
```bash
|
||||
# Полное обновление и запуск
|
||||
./update
|
||||
|
||||
# Проверка статуса
|
||||
./cli status
|
||||
```
|
||||
|
||||
### Пошаговое развертывание
|
||||
|
||||
```bash
|
||||
# 1. Остановка старых контейнеров
|
||||
./stop
|
||||
|
||||
# 2. Сборка образов
|
||||
docker-compose build --no-cache
|
||||
|
||||
# 3. Запуск сервисов
|
||||
docker-compose up -d
|
||||
|
||||
# 4. Выполнение миграций
|
||||
./cli migrate
|
||||
|
||||
# 5. Создание суперпользователя
|
||||
./cli createsuperuser
|
||||
|
||||
# 6. Сбор статических файлов
|
||||
./cli collectstatic
|
||||
```
|
||||
|
||||
## 🔒 Настройка SSL/HTTPS
|
||||
|
||||
### Использование Let's Encrypt
|
||||
|
||||
```bash
|
||||
# Установка Certbot
|
||||
sudo apt install certbot python3-certbot-nginx
|
||||
|
||||
# Получение сертификата
|
||||
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
|
||||
|
||||
# Настройка автообновления
|
||||
sudo crontab -e
|
||||
# Добавить: 0 12 * * * /usr/bin/certbot renew --quiet
|
||||
```
|
||||
|
||||
### Настройка Nginx
|
||||
|
||||
```nginx
|
||||
# /etc/nginx/sites-available/smartsoltech
|
||||
server {
|
||||
listen 80;
|
||||
server_name yourdomain.com www.yourdomain.com;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name yourdomain.com www.yourdomain.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /static/ {
|
||||
alias /path/to/smartsoltech.kr/smartsoltech/staticfiles/;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
location /media/ {
|
||||
alias /path/to/smartsoltech.kr/smartsoltech/media/;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 Мониторинг и логи
|
||||
|
||||
### Системные логи
|
||||
|
||||
```bash
|
||||
# Логи всех сервисов
|
||||
./logs
|
||||
|
||||
# Логи конкретного сервиса
|
||||
./logs web
|
||||
./logs db
|
||||
./logs pgadmin
|
||||
|
||||
# Последние 100 строк
|
||||
./logs --tail 100
|
||||
```
|
||||
|
||||
### Мониторинг ресурсов
|
||||
|
||||
```bash
|
||||
# Использование ресурсов контейнерами
|
||||
docker stats
|
||||
|
||||
# Размер образов и контейнеров
|
||||
docker system df
|
||||
|
||||
# Проверка дискового пространства
|
||||
df -h
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
# Проверка состояния сервисов
|
||||
./cli status
|
||||
|
||||
# Подключение к базе данных
|
||||
./cli dbshell
|
||||
|
||||
# Проверка Django
|
||||
./cli check
|
||||
```
|
||||
|
||||
## 🔄 Backup и восстановление
|
||||
|
||||
### Настройка автоматических бэкапов
|
||||
|
||||
```bash
|
||||
# Настройка backup репозитория
|
||||
./bin/setup-backup.sh git@backup-server.com:backups/smartsoltech.git
|
||||
|
||||
# Добавление в crontab
|
||||
crontab -e
|
||||
# Добавить: 0 2 * * * cd /path/to/smartsoltech.kr && ./update origin backup
|
||||
```
|
||||
|
||||
### Backup базы данных
|
||||
|
||||
```bash
|
||||
# Создание дампа БД
|
||||
docker-compose exec postgres_db pg_dump -U postgres smartsoltech > backup_$(date +%Y%m%d).sql
|
||||
|
||||
# Восстановление из дампа
|
||||
docker-compose exec -i postgres_db psql -U postgres smartsoltech < backup_20231125.sql
|
||||
```
|
||||
|
||||
### Backup медиафайлов
|
||||
|
||||
```bash
|
||||
# Создание архива медиафайлов
|
||||
tar -czf media_backup_$(date +%Y%m%d).tar.gz smartsoltech/media/
|
||||
|
||||
# Восстановление медиафайлов
|
||||
tar -xzf media_backup_20231125.tar.gz
|
||||
```
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Частые проблемы
|
||||
|
||||
**1. Контейнер не запускается**
|
||||
```bash
|
||||
# Проверка логов
|
||||
./logs web
|
||||
|
||||
# Пересборка образов
|
||||
./stop --clean
|
||||
./update
|
||||
```
|
||||
|
||||
**2. Ошибки базы данных**
|
||||
```bash
|
||||
# Проверка подключения к БД
|
||||
./cli dbshell
|
||||
|
||||
# Проверка миграций
|
||||
./cli showmigrations
|
||||
|
||||
# Сброс миграций (ОСТОРОЖНО!)
|
||||
./cli migrate --fake-initial
|
||||
```
|
||||
|
||||
**3. Проблемы с статическими файлами**
|
||||
```bash
|
||||
# Пересбор статики
|
||||
./cli collectstatic --clear
|
||||
|
||||
# Проверка прав доступа
|
||||
./cli exec ls -la /app/smartsoltech/staticfiles/
|
||||
```
|
||||
|
||||
### Логи для диагностики
|
||||
|
||||
```bash
|
||||
# Django логи
|
||||
./logs web
|
||||
|
||||
# База данных логи
|
||||
./logs db
|
||||
|
||||
# Nginx логи (если используется)
|
||||
sudo tail -f /var/log/nginx/error.log
|
||||
|
||||
# Системные логи
|
||||
sudo journalctl -u docker.service
|
||||
```
|
||||
|
||||
## 📈 Оптимизация производительности
|
||||
|
||||
### Настройки Django
|
||||
|
||||
```python
|
||||
# smartsoltech/settings.py
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'OPTIONS': {
|
||||
'MAX_CONNS': 20,
|
||||
'conn_max_age': 600,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Кэширование
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
||||
'LOCATION': 'redis://redis:6379/1',
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Docker оптимизации
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
web:
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
reservations:
|
||||
memory: 256M
|
||||
```
|
||||
|
||||
## 🔐 Безопасность
|
||||
|
||||
### Основные рекомендации
|
||||
|
||||
1. **Обновление системы:**
|
||||
```bash
|
||||
sudo apt update && sudo apt upgrade
|
||||
```
|
||||
|
||||
2. **Настройка Firewall:**
|
||||
```bash
|
||||
sudo ufw enable
|
||||
sudo ufw allow ssh
|
||||
sudo ufw allow 80
|
||||
sudo ufw allow 443
|
||||
```
|
||||
|
||||
3. **Ограничение доступа к админке:**
|
||||
```python
|
||||
# settings.py
|
||||
ADMIN_URL = 'secret-admin-url/'
|
||||
```
|
||||
|
||||
4. **Регулярные обновления:**
|
||||
```bash
|
||||
# Еженедельное обновление
|
||||
./update
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
🎯 **Следующие шаги**: [API документация](API.md) | [Управление скриптами](SCRIPTS_README.md)
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
BASE_URL="http://localhost:8002/auth"
|
||||
EMAIL="testuser@example.com"
|
||||
PASSWORD="secret123"
|
||||
|
||||
echo "1️⃣ Регистрация пользователя..."
|
||||
curl -s -X POST "$BASE_URL/register" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\": \"$EMAIL\", \"password\": \"$PASSWORD\"}" | tee response_register.json
|
||||
echo -e "\n"
|
||||
|
||||
USER_ID=$(jq .id response_register.json)
|
||||
|
||||
echo "2️⃣ Аутентификация..."
|
||||
curl -s -X POST "$BASE_URL/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\": \"$EMAIL\", \"password\": \"$PASSWORD\"}" | tee response_login.json
|
||||
echo -e "\n"
|
||||
|
||||
TOKEN=$(jq -r .access_token response_login.json)
|
||||
|
||||
echo "🔐 Получен токен: $TOKEN"
|
||||
AUTH_HEADER="Authorization: Bearer $TOKEN"
|
||||
|
||||
echo "3️⃣ Получение текущего пользователя (/me)..."
|
||||
curl -s -X GET "$BASE_URL/me" -H "$AUTH_HEADER" | tee response_me.json
|
||||
echo -e "\n"
|
||||
|
||||
echo "4️⃣ Получение списка всех пользователей..."
|
||||
curl -s -X GET "$BASE_URL/users" | tee response_users.json
|
||||
echo -e "\n"
|
||||
|
||||
echo "5️⃣ Получение пользователя по ID ($USER_ID)..."
|
||||
curl -s -X GET "$BASE_URL/users/$USER_ID" | tee response_user.json
|
||||
echo -e "\n"
|
||||
|
||||
echo "6️⃣ Обновление пользователя..."
|
||||
curl -s -X PUT "$BASE_URL/users/$USER_ID" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\": \"updated_$EMAIL\", \"role\": \"admin\"}" | tee response_update.json
|
||||
echo -e "\n"
|
||||
|
||||
echo "7️⃣ Удаление пользователя..."
|
||||
curl -s -X DELETE "$BASE_URL/users/$USER_ID" | tee response_delete.json
|
||||
echo -e "\n"
|
||||
|
||||
echo "✅ Тест завершён."
|
||||
1
frontend/assets/css/styles.min.css
vendored
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 56 KiB |
1
frontend/assets/js/script.min.js
vendored
@@ -1 +0,0 @@
|
||||
!function(){"use strict";var e=document.querySelector("#mainNav");if(e){var o=e.querySelector(".navbar-collapse");if(o){var n=new bootstrap.Collapse(o,{toggle:!1}),t=o.querySelectorAll("a");for(var a of t)a.addEventListener("click",(function(e){n.hide()}))}var r=function(){(void 0!==window.pageYOffset?window.pageYOffset:(document.documentElement||document.body.parentNode||document.body).scrollTop)>100?e.classList.add("navbar-shrink"):e.classList.remove("navbar-shrink")};r(),document.addEventListener("scroll",r);var d=document.querySelectorAll(".portfolio-modal");for(var s of d)s.addEventListener("shown.bs.modal",(function(o){e.classList.add("d-none")})),s.addEventListener("hidden.bs.modal",(function(o){e.classList.remove("d-none")}))}}();
|
||||
@@ -1,75 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html data-bs-theme="light" lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>Home - Brand</title>
|
||||
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
|
||||
<link rel="icon" type="image/jpeg" sizes="500x500" href="/assets/img/photo_2024-10-06_10-06-15.jpg" media="(prefers-color-scheme: dark)">
|
||||
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
|
||||
<link rel="icon" type="image/jpeg" sizes="500x500" href="/assets/img/photo_2024-10-06_10-06-15.jpg" media="(prefers-color-scheme: dark)">
|
||||
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
|
||||
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
|
||||
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
|
||||
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
|
||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.12.0/css/all.css">
|
||||
<link rel="stylesheet" href="/assets/css/styles.min.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" data-bs-spy="scroll" data-bs-target="#mainNav" data-bs-offset="54">
|
||||
<!-- Start: footer -->
|
||||
<footer class="text-light bg-dark pt-5 pb-4">
|
||||
<div class="container text-md-left">
|
||||
<div class="row text-md-left">
|
||||
<div class="col-md-3 col-lg-3 col-xl-3 mx-auto mt-3">
|
||||
<h5 class="text-uppercase text-warning mb-4 font-weight-bold">Company Name</h5>
|
||||
<p>Here you can use rows and columns to organize your footer content. Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
|
||||
</div>
|
||||
<div class="col-md-2 col-lg-2 col-xl-2 mx-auto mt-3">
|
||||
<h5 class="text-uppercase text-warning mb-4 font-weight-bold">Products</h5>
|
||||
<p><a href="#" class="text-light" style="text-decoration:none;">Product 1</a></p>
|
||||
<p><a href="#" class="text-light" style="text-decoration:none;">Product 2</a></p>
|
||||
<p><a href="#" class="text-light" style="text-decoration:none;">Product 3</a></p>
|
||||
<p><a href="#" class="text-light" style="text-decoration:none;">Product 4</a></p>
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-2 col-xl-2 mx-auto mt-3">
|
||||
<h5 class="text-uppercase text-warning mb-4 font-weight-bold">Useful Links</h5>
|
||||
<p><a href="#" class="text-light" style="text-decoration:none;">Your Account</a></p>
|
||||
<p><a href="#" class="text-light" style="text-decoration:none;">Become an Affiliate</a></p>
|
||||
<p><a href="#" class="text-light" style="text-decoration:none;">Shipping Rates</a></p>
|
||||
<p><a href="#" class="text-light" style="text-decoration:none;">Help</a></p>
|
||||
</div>
|
||||
<div class="col-md-4 col-lg-3 col-xl-3 mx-auto mt-3">
|
||||
<h5 class="text-uppercase text-warning mb-4 font-weight-bold">Contact</h5>
|
||||
<p><i class="fas fa-home mr-3"></i> 123 Street, City, State</p>
|
||||
<p><i class="fas fa-envelope mr-3"></i> info@example.com</p>
|
||||
<p><i class="fas fa-phone mr-3"></i> + 01 234 567 88</p>
|
||||
<p><i class="fas fa-print mr-3"></i> + 01 234 567 89</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="mb-4">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-7 col-lg-8">
|
||||
<p class="text-md-left">© 2024 Company Name. All rights reserved.</p>
|
||||
</div>
|
||||
<div class="col-md-5 col-lg-4">
|
||||
<div class="text-md-right">
|
||||
<ul class="list-unstyled list-inline">
|
||||
<li class="list-inline-item"><a href="#" class="btn-floating btn-sm text-light" style="font-size:23px;"><i class="fab fa-facebook"></i></a></li>
|
||||
<li class="list-inline-item"><a href="#" class="btn-floating btn-sm text-light" style="font-size:23px;"><i class="fab fa-twitter"></i></a></li>
|
||||
<li class="list-inline-item"><a href="#" class="btn-floating btn-sm text-light" style="font-size:23px;"><i class="fab fa-google-plus"></i></a></li>
|
||||
<li class="list-inline-item"><a href="#" class="btn-floating btn-sm text-light" style="font-size:23px;"><i class="fab fa-linkedin-in"></i></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer><!-- End: footer -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/assets/js/script.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,41 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html data-bs-theme="light" lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>Home - Brand</title>
|
||||
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
|
||||
<link rel="icon" type="image/jpeg" sizes="500x500" href="/assets/img/photo_2024-10-06_10-06-15.jpg" media="(prefers-color-scheme: dark)">
|
||||
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
|
||||
<link rel="icon" type="image/jpeg" sizes="500x500" href="/assets/img/photo_2024-10-06_10-06-15.jpg" media="(prefers-color-scheme: dark)">
|
||||
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
|
||||
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
|
||||
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
|
||||
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
|
||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
|
||||
<link rel="stylesheet" href="/assets/css/styles.min.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" data-bs-spy="scroll" data-bs-target="#mainNav" data-bs-offset="54">
|
||||
<!-- Start: Navbar Right Links (Dark) -->
|
||||
<nav class="navbar navbar-expand-md bg-dark py-3" data-bs-theme="dark">
|
||||
<div class="container"><a class="navbar-brand d-flex align-items-center" href="#"><span class="bs-icon-sm bs-icon-rounded bs-icon-primary d-flex justify-content-center align-items-center me-2 bs-icon"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-bezier">
|
||||
<path fill-rule="evenodd" d="M0 10.5A1.5 1.5 0 0 1 1.5 9h1A1.5 1.5 0 0 1 4 10.5v1A1.5 1.5 0 0 1 2.5 13h-1A1.5 1.5 0 0 1 0 11.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm10.5.5A1.5 1.5 0 0 1 13.5 9h1a1.5 1.5 0 0 1 1.5 1.5v1a1.5 1.5 0 0 1-1.5 1.5h-1a1.5 1.5 0 0 1-1.5-1.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zM6 4.5A1.5 1.5 0 0 1 7.5 3h1A1.5 1.5 0 0 1 10 4.5v1A1.5 1.5 0 0 1 8.5 7h-1A1.5 1.5 0 0 1 6 5.5zM7.5 4a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5z"></path>
|
||||
<path d="M6 4.5H1.866a1 1 0 1 0 0 1h2.668A6.517 6.517 0 0 0 1.814 9H2.5c.123 0 .244.015.358.043a5.517 5.517 0 0 1 3.185-3.185A1.503 1.503 0 0 1 6 5.5zm3.957 1.358A1.5 1.5 0 0 0 10 5.5v-1h4.134a1 1 0 1 1 0 1h-2.668a6.517 6.517 0 0 1 2.72 3.5H13.5c-.123 0-.243.015-.358.043a5.517 5.517 0 0 0-3.185-3.185z"></path>
|
||||
</svg></span><span>Brand</span></a><button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-5"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
|
||||
<div class="collapse navbar-collapse" id="navcol-5">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item"><a class="nav-link active" href="#">First Item</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#">Second Item</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#">Third Item</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav><!-- End: Navbar Right Links (Dark) -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/assets/js/script.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1 +0,0 @@
|
||||
{"short_name":"sst","name":"smartsoltech","icons":[{"src":"/assets/img/photo_2024-10-06_10-06-08.jpg","type":"image/jpeg","sizes":"1011x702"},{"src":"/assets/img/photo_2024-10-06_10-06-08.jpg","type":"image/jpeg","sizes":"1011x702"}],"start_url":"smartsoltech.kr","display":"fullscreen"}
|
||||
@@ -1,82 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html data-bs-theme="light" lang="ru">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>SmartSoltech</title>
|
||||
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
|
||||
<link rel="icon" type="image/jpeg" sizes="500x500" href="/assets/img/photo_2024-10-06_10-06-15.jpg" media="(prefers-color-scheme: dark)">
|
||||
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
|
||||
<link rel="icon" type="image/jpeg" sizes="500x500" href="/assets/img/photo_2024-10-06_10-06-15.jpg" media="(prefers-color-scheme: dark)">
|
||||
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
|
||||
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
|
||||
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
|
||||
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
|
||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
|
||||
<link rel="stylesheet" href="/assets/css/styles.min.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Start: Articles Cards -->
|
||||
<div class="container py-4 py-xl-5">
|
||||
<div class="row mb-5">
|
||||
<div class="col-md-8 col-xl-6 text-center mx-auto">
|
||||
<h2>Heading</h2>
|
||||
<p class="w-lg-50">Curae hendrerit donec commodo hendrerit egestas tempus, turpis facilisis nostra nunc. Vestibulum dui eget ultrices.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row gy-4 row-cols-1 row-cols-md-2 row-cols-xl-3">
|
||||
<div class="col">
|
||||
<div class="card"><img class="card-img-top w-100 d-block fit-cover" style="height: 200px;" src="https://cdn.bootstrapstudio.io/placeholders/1400x800.png">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-primary card-text mb-0">Article</p>
|
||||
<h4 class="card-title">Lorem libero donec</h4>
|
||||
<p class="card-text">Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus.</p>
|
||||
<div class="d-flex"><img class="rounded-circle flex-shrink-0 me-3 fit-cover" width="50" height="50" src="https://cdn.bootstrapstudio.io/placeholders/1400x800.png">
|
||||
<div>
|
||||
<p class="fw-bold mb-0">John Smith</p>
|
||||
<p class="text-muted mb-0">Erat netus</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card"><img class="card-img-top w-100 d-block fit-cover" style="height: 200px;" src="https://cdn.bootstrapstudio.io/placeholders/1400x800.png">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-primary card-text mb-0">Article</p>
|
||||
<h4 class="card-title">Lorem libero donec</h4>
|
||||
<p class="card-text">Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus.</p>
|
||||
<div class="d-flex"><img class="rounded-circle flex-shrink-0 me-3 fit-cover" width="50" height="50" src="https://cdn.bootstrapstudio.io/placeholders/1400x800.png">
|
||||
<div>
|
||||
<p class="fw-bold mb-0">John Smith</p>
|
||||
<p class="text-muted mb-0">Erat netus</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card"><img class="card-img-top w-100 d-block fit-cover" style="height: 200px;" src="https://cdn.bootstrapstudio.io/placeholders/1400x800.png">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-primary card-text mb-0">Article</p>
|
||||
<h4 class="card-title">Lorem libero donec</h4>
|
||||
<p class="card-text">Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus.</p>
|
||||
<div class="d-flex"><img class="rounded-circle flex-shrink-0 me-3 fit-cover" width="50" height="50" src="https://cdn.bootstrapstudio.io/placeholders/1400x800.png">
|
||||
<div>
|
||||
<p class="fw-bold mb-0">John Smith</p>
|
||||
<p class="text-muted mb-0">Erat netus</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- End: Articles Cards -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/assets/js/script.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
350
preview.html
Normal file
@@ -0,0 +1,350 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Проект Django E-commerce - Предварительный просмотр</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.3/css/lightbox.min.css">
|
||||
<link rel="stylesheet" href="smartsoltech/static/assets/css/modern-styles.css">
|
||||
<link rel="stylesheet" href="smartsoltech/static/assets/css/compact-gallery.css">"
|
||||
<style>
|
||||
.main-content {
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
.content-wrapper {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-content">
|
||||
<div class="content-wrapper">
|
||||
|
||||
<!-- Компактная медиа-галерея -->
|
||||
<div class="compact-media-gallery">
|
||||
<div class="row g-3">
|
||||
<!-- Основное изображение -->
|
||||
<div class="col-lg-8">
|
||||
<div class="main-media-display">
|
||||
<div class="main-media-item" id="main-media">
|
||||
<a href="https://via.placeholder.com/800x500/4f46e5/ffffff?text=Главное+изображение" data-lightbox="project-gallery" data-title="Главное изображение проекта">
|
||||
<img src="https://via.placeholder.com/800x500/4f46e5/ffffff?text=Главное+изображение" alt="Главное изображение" class="main-media-img">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Сетка превью -->
|
||||
<div class="col-lg-4">
|
||||
<div class="media-thumbnails-grid">
|
||||
<div class="thumbnail-item active" data-index="0">
|
||||
<div class="thumbnail-wrapper" onclick="switchMainMedia('https://via.placeholder.com/800x500/4f46e5/ffffff?text=Главное+изображение', 'image', 'Главное изображение проекта')">
|
||||
<img src="https://via.placeholder.com/200x200/4f46e5/ffffff?text=Превью+1" alt="Превью 1" class="thumbnail-img">
|
||||
<div class="thumbnail-overlay">
|
||||
<i class="fas fa-search-plus"></i>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://via.placeholder.com/800x500/4f46e5/ffffff?text=Главное+изображение" data-lightbox="project-gallery" data-title="Главное изображение проекта" style="display: none;"></a>
|
||||
</div>
|
||||
|
||||
<div class="thumbnail-item" data-index="1">
|
||||
<div class="thumbnail-wrapper" onclick="switchMainMedia('https://via.placeholder.com/800x500/7c3aed/ffffff?text=Скриншот+1', 'image', 'Скриншот интерфейса')">
|
||||
<img src="https://via.placeholder.com/200x200/7c3aed/ffffff?text=Превью+2" alt="Превью 2" class="thumbnail-img">
|
||||
<div class="thumbnail-overlay">
|
||||
<i class="fas fa-search-plus"></i>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://via.placeholder.com/800x500/7c3aed/ffffff?text=Скриншот+1" data-lightbox="project-gallery" data-title="Скриншот интерфейса" style="display: none;"></a>
|
||||
</div>
|
||||
|
||||
<div class="thumbnail-item" data-index="2">
|
||||
<div class="thumbnail-wrapper" onclick="switchMainMedia('https://via.placeholder.com/800x500/f59e0b/ffffff?text=Скриншот+2', 'image', 'Мобильная версия')">
|
||||
<img src="https://via.placeholder.com/200x200/f59e0b/ffffff?text=Превью+3" alt="Превью 3" class="thumbnail-img">
|
||||
<div class="thumbnail-overlay">
|
||||
<i class="fas fa-search-plus"></i>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://via.placeholder.com/800x500/f59e0b/ffffff?text=Скриншот+2" data-lightbox="project-gallery" data-title="Мобильная версия" style="display: none;"></a>
|
||||
</div>
|
||||
|
||||
<div class="thumbnail-item" data-index="3">
|
||||
<div class="thumbnail-wrapper" onclick="switchMainMedia('https://via.placeholder.com/800x500/10b981/ffffff?text=Админ+панель', 'image', 'Административная панель')">
|
||||
<img src="https://via.placeholder.com/200x200/10b981/ffffff?text=Превью+4" alt="Превью 4" class="thumbnail-img">
|
||||
<div class="thumbnail-overlay">
|
||||
<i class="fas fa-search-plus"></i>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://via.placeholder.com/800x500/10b981/ffffff?text=Админ+панель" data-lightbox="project-gallery" data-title="Административная панель" style="display: none;"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Основной контент - двухколоночная структура -->
|
||||
<div class="row">
|
||||
|
||||
<!-- Левая колонка - описание проекта -->
|
||||
<div class="col-lg-8">
|
||||
<div class="project-content">
|
||||
<h2 class="mb-4">Описание проекта</h2>
|
||||
<div class="description-text">
|
||||
<p>Этот проект представляет собой <strong>современное веб-приложение</strong> электронной коммерции, разработанное с использованием Django и современных технологий фронтенда.</p>
|
||||
|
||||
<h3>Ключевые особенности</h3>
|
||||
<ul>
|
||||
<li><em>Адаптивный дизайн</em>, оптимизированный для всех устройств</li>
|
||||
<li>Интеграция с <a href="#">популярными платежными системами</a></li>
|
||||
<li>Многоуровневая система категорий товаров</li>
|
||||
<li>Расширенная система поиска и фильтрации</li>
|
||||
<li>Административная панель для управления контентом</li>
|
||||
</ul>
|
||||
|
||||
<blockquote>
|
||||
Проект демонстрирует современные подходы к веб-разработке, включая использование микросервисной архитектуры, контейнеризации и непрерывной интеграции.
|
||||
</blockquote>
|
||||
|
||||
<h4>Технические детали</h4>
|
||||
<p>Для обеспечения высокой производительности использовались следующие решения:</p>
|
||||
|
||||
<ol>
|
||||
<li><code>Redis</code> для кеширования данных</li>
|
||||
<li><code>PostgreSQL</code> как основная база данных</li>
|
||||
<li><code>Docker</code> для контейнеризации</li>
|
||||
</ol>
|
||||
|
||||
<hr>
|
||||
|
||||
<p><strong>Результат:</strong> Платформа способна обрабатывать <em>более 10,000 одновременных пользователей</em> с временем отклика менее 200ms.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Правая колонка - технологии -->
|
||||
<div class="col-lg-4">
|
||||
<div class="tech-sidebar-section">
|
||||
<h3 class="tech-sidebar-title">Технологии</h3>
|
||||
<div class="technologies-html-content">
|
||||
<p><code>Python</code> <code>Django</code> <code>PostgreSQL</code></p>
|
||||
<p><code>JavaScript</code> <code>HTML5</code> <code>CSS3</code></p>
|
||||
<p><code>Docker</code> <code>Redis</code> <code>Bootstrap</code></p>
|
||||
<p><strong>Дополнительно:</strong> <code>Bash</code> <code>SQLite3</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Дополнительная секция -->
|
||||
<div class="row mt-5">
|
||||
<div class="col-12">
|
||||
<div class="additional-info p-4 rounded-4" style="background: #f8fafc; border: 1px solid #e2e8f0;">
|
||||
<h3 class="mb-3">Результаты проекта</h3>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-3 text-center">
|
||||
<div class="stat-number" style="font-size: 2rem; font-weight: bold; color: #4f46e5;">150%</div>
|
||||
<div class="stat-label">Рост продаж</div>
|
||||
</div>
|
||||
<div class="col-md-3 text-center">
|
||||
<div class="stat-number" style="font-size: 2rem; font-weight: bold; color: #4f46e5;">2.5x</div>
|
||||
<div class="stat-label">Быстрее загрузка</div>
|
||||
</div>
|
||||
<div class="col-md-3 text-center">
|
||||
<div class="stat-number" style="font-size: 2rem; font-weight: bold; color: #4f46e5;">95%</div>
|
||||
<div class="stat-label">Доступность</div>
|
||||
</div>
|
||||
<div class="col-md-3 text-center">
|
||||
<div class="stat-number" style="font-size: 2rem; font-weight: bold; color: #4f46e5;">100%</div>
|
||||
<div class="stat-label">Адаптивность</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Карусель похожих проектов -->
|
||||
<div class="similar-projects-section">
|
||||
<div class="container">
|
||||
<h2 class="section-title">Похожие проекты</h2>
|
||||
<div class="similar-projects-carousel">
|
||||
<div class="swiper similarSwiper">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide">
|
||||
<div class="similar-project-card">
|
||||
<div class="similar-thumb">
|
||||
<img src="https://via.placeholder.com/300x200/4f46e5/ffffff?text=Проект+1" alt="Проект 1">
|
||||
<div class="project-overlay">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="similar-content">
|
||||
<h4 class="project-title">E-commerce платформа</h4>
|
||||
<p class="project-description">Современная платформа для онлайн-торговли с интеграцией платежных систем</p>
|
||||
<div class="project-categories">
|
||||
<span class="category-tag">Web</span>
|
||||
<span class="category-tag">Django</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<div class="similar-project-card">
|
||||
<div class="similar-thumb">
|
||||
<img src="https://via.placeholder.com/300x200/7c3aed/ffffff?text=Проект+2" alt="Проект 2">
|
||||
<div class="project-overlay">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="similar-content">
|
||||
<h4 class="project-title">CRM система</h4>
|
||||
<p class="project-description">Система управления клиентскими отношениями с аналитикой</p>
|
||||
<div class="project-categories">
|
||||
<span class="category-tag">CRM</span>
|
||||
<span class="category-tag">Analytics</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<div class="similar-project-card">
|
||||
<div class="similar-thumb">
|
||||
<img src="https://via.placeholder.com/300x200/f59e0b/ffffff?text=Проект+3" alt="Проект 3">
|
||||
<div class="project-overlay">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="similar-content">
|
||||
<h4 class="project-title">Мобильное приложение</h4>
|
||||
<p class="project-description">iOS и Android приложение для управления задачами</p>
|
||||
<div class="project-categories">
|
||||
<span class="category-tag">Mobile</span>
|
||||
<span class="category-tag">React Native</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<div class="similar-project-card">
|
||||
<div class="similar-thumb">
|
||||
<div class="image-placeholder">
|
||||
<div class="placeholder-content">
|
||||
<i class="fas fa-image placeholder-icon"></i>
|
||||
<div class="placeholder-text">Проект без изображения</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-overlay">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="similar-content">
|
||||
<h4 class="project-title">Analytics Dashboard</h4>
|
||||
<p class="project-description">Интерактивная панель аналитики с визуализацией данных</p>
|
||||
<div class="project-categories">
|
||||
<span class="category-tag">Analytics</span>
|
||||
<span class="category-tag">D3.js</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swiper-button-next similar-next"></div>
|
||||
<div class="swiper-button-prev similar-prev"></div>
|
||||
<div class="swiper-pagination similar-pagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.3/js/lightbox.min.js"></script>
|
||||
<script>
|
||||
// Функция переключения основного медиа
|
||||
function switchMainMedia(url, type, caption, poster = '') {
|
||||
const mainMediaContainer = document.getElementById('main-media');
|
||||
if (!mainMediaContainer) return;
|
||||
|
||||
// Очищаем контейнер
|
||||
mainMediaContainer.innerHTML = '';
|
||||
|
||||
if (type === 'image') {
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('data-lightbox', 'project-gallery');
|
||||
link.setAttribute('data-title', caption);
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = url;
|
||||
img.alt = caption;
|
||||
img.className = 'main-media-img';
|
||||
|
||||
link.appendChild(img);
|
||||
mainMediaContainer.appendChild(link);
|
||||
} else if (type === 'video') {
|
||||
const video = document.createElement('video');
|
||||
video.controls = true;
|
||||
video.className = 'main-media-video';
|
||||
if (poster) {
|
||||
video.poster = poster;
|
||||
}
|
||||
|
||||
const source = document.createElement('source');
|
||||
source.src = url;
|
||||
source.type = 'video/mp4';
|
||||
|
||||
video.appendChild(source);
|
||||
video.appendChild(document.createTextNode('Ваш браузер не поддерживает видео.'));
|
||||
|
||||
mainMediaContainer.appendChild(video);
|
||||
}
|
||||
|
||||
// Обновляем активный thumbnail
|
||||
document.querySelectorAll('.thumbnail-item').forEach(item => item.classList.remove('active'));
|
||||
event.target.closest('.thumbnail-item').classList.add('active');
|
||||
}
|
||||
|
||||
// Инициализация Swiper для карусели
|
||||
const swiper = new Swiper('.similarSwiper', {
|
||||
effect: 'coverflow',
|
||||
grabCursor: true,
|
||||
centeredSlides: true,
|
||||
slidesPerView: 'auto',
|
||||
spaceBetween: 40,
|
||||
loop: true,
|
||||
coverflowEffect: {
|
||||
rotate: 15,
|
||||
stretch: 0,
|
||||
depth: 200,
|
||||
modifier: 1.5,
|
||||
slideShadows: true,
|
||||
},
|
||||
navigation: {
|
||||
nextEl: '.similar-next',
|
||||
prevEl: '.similar-prev',
|
||||
},
|
||||
pagination: {
|
||||
el: '.similar-pagination',
|
||||
clickable: true,
|
||||
},
|
||||
breakpoints: {
|
||||
768: {
|
||||
slidesPerView: 2,
|
||||
spaceBetween: 20,
|
||||
},
|
||||
1024: {
|
||||
slidesPerView: 3,
|
||||
spaceBetween: 30,
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -16,4 +16,12 @@ qrcode==8.0
|
||||
sniffio==1.3.1
|
||||
sqlparse==0.5.1
|
||||
typing_extensions==4.12.2
|
||||
pyTelegramBotAPI
|
||||
pyTelegramBotAPI
|
||||
dj-database-url==2.1.0
|
||||
coverage==7.3.2
|
||||
pytest==7.4.3
|
||||
pytest-django==4.7.0
|
||||
pytest-cov==4.1.0
|
||||
django-tinymce==4.1.0
|
||||
Pillow==10.4.0
|
||||
django-tinymce==4.1.0
|
||||
73
reset_database.sql
Normal file
@@ -0,0 +1,73 @@
|
||||
-- Скрипт для полной очистки базы данных smartsoltech_db
|
||||
-- ВНИМАНИЕ: Этот скрипт удалит ВСЕ данные из базы данных!
|
||||
|
||||
-- Отключаем проверку внешних ключей
|
||||
SET session_replication_role = replica;
|
||||
|
||||
-- Удаляем все таблицы Django приложений
|
||||
DROP TABLE IF EXISTS django_migrations CASCADE;
|
||||
DROP TABLE IF EXISTS django_content_type CASCADE;
|
||||
DROP TABLE IF EXISTS auth_permission CASCADE;
|
||||
DROP TABLE IF EXISTS auth_group CASCADE;
|
||||
DROP TABLE IF EXISTS auth_group_permissions CASCADE;
|
||||
DROP TABLE IF EXISTS auth_user CASCADE;
|
||||
DROP TABLE IF EXISTS auth_user_groups CASCADE;
|
||||
DROP TABLE IF EXISTS auth_user_user_permissions CASCADE;
|
||||
DROP TABLE IF EXISTS django_admin_log CASCADE;
|
||||
DROP TABLE IF EXISTS django_session CASCADE;
|
||||
|
||||
-- Удаляем таблицы приложения web
|
||||
DROP TABLE IF EXISTS web_herobanner CASCADE;
|
||||
DROP TABLE IF EXISTS web_category CASCADE;
|
||||
DROP TABLE IF EXISTS web_service CASCADE;
|
||||
DROP TABLE IF EXISTS web_client CASCADE;
|
||||
DROP TABLE IF EXISTS web_order CASCADE;
|
||||
DROP TABLE IF EXISTS web_project CASCADE;
|
||||
DROP TABLE IF EXISTS web_project_categories CASCADE;
|
||||
DROP TABLE IF EXISTS web_projectmedia CASCADE;
|
||||
DROP TABLE IF EXISTS web_portfolioitem CASCADE;
|
||||
DROP TABLE IF EXISTS web_portfolioitem_categories CASCADE;
|
||||
DROP TABLE IF EXISTS web_portfoliocategory CASCADE;
|
||||
DROP TABLE IF EXISTS web_portfoliomedia CASCADE;
|
||||
DROP TABLE IF EXISTS web_review CASCADE;
|
||||
DROP TABLE IF EXISTS web_blogpost CASCADE;
|
||||
DROP TABLE IF EXISTS web_servicerequest CASCADE;
|
||||
DROP TABLE IF EXISTS web_contactinfo CASCADE;
|
||||
DROP TABLE IF EXISTS web_team CASCADE;
|
||||
DROP TABLE IF EXISTS web_career CASCADE;
|
||||
DROP TABLE IF EXISTS web_newspost CASCADE;
|
||||
|
||||
-- Удаляем таблицы приложения comunication
|
||||
DROP TABLE IF EXISTS comunication_usercommunication CASCADE;
|
||||
DROP TABLE IF EXISTS comunication_emailsettings CASCADE;
|
||||
DROP TABLE IF EXISTS comunication_telegramsettings CASCADE;
|
||||
|
||||
-- Удаляем все последовательности (sequences)
|
||||
DROP SEQUENCE IF EXISTS web_herobanner_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_category_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_service_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_client_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_order_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_project_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_project_categories_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_projectmedia_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_portfolioitem_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_portfolioitem_categories_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_portfoliocategory_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_portfoliomedia_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_review_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_blogpost_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_servicerequest_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_contactinfo_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_team_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_career_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS web_newspost_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS comunication_usercommunication_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS comunication_emailsettings_id_seq CASCADE;
|
||||
DROP SEQUENCE IF EXISTS comunication_telegramsettings_id_seq CASCADE;
|
||||
|
||||
-- Включаем обратно проверку внешних ключей
|
||||
SET session_replication_role = DEFAULT;
|
||||
|
||||
-- Выводим сообщение о завершении
|
||||
SELECT 'База данных успешно очищена!' as status;
|
||||
@@ -1 +0,0 @@
|
||||
{"detail":"User 4 deleted"}
|
||||
@@ -1 +0,0 @@
|
||||
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0Iiwicm9sZSI6InVzZXIiLCJleHAiOjE3NTI0OTc2NDV9.S_tquLFIPnyG6XlfwIw97hJv0l9oKpTcYw_XG0mDd6w","token_type":"bearer"}
|
||||
@@ -1 +0,0 @@
|
||||
{"id":4,"email":"testuser@example.com","role":"user"}
|
||||
@@ -1 +0,0 @@
|
||||
{"id":4,"email":"testuser@example.com","role":"user"}
|
||||
@@ -1 +0,0 @@
|
||||
{"id":4,"email":"updated_testuser@example.com","role":"admin"}
|
||||
@@ -1 +0,0 @@
|
||||
{"id":4,"email":"testuser@example.com","role":"user"}
|
||||
@@ -1 +0,0 @@
|
||||
[{"id":1,"email":"user1@example.com","role":"user"},{"id":4,"email":"testuser@example.com","role":"user"}]
|
||||
23
scripts/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# 🐍 Scripts
|
||||
|
||||
Папка содержит вспомогательные скрипты для проекта SmartSolTech.
|
||||
|
||||
## Файлы:
|
||||
|
||||
- `create_hero_banner.py` - Скрипт для создания героических баннеров
|
||||
- `hero_script.py` - Дополнительный скрипт для работы с баннерами
|
||||
|
||||
## Использование:
|
||||
|
||||
Запуск скриптов должен производиться из корневой директории проекта:
|
||||
|
||||
```bash
|
||||
cd smartsoltech/
|
||||
python ../scripts/create_hero_banner.py
|
||||
```
|
||||
|
||||
или через Django management команды из папки smartsoltech/:
|
||||
|
||||
```bash
|
||||
python manage.py shell < ../scripts/script_name.py
|
||||
```
|
||||
58
scripts/create_hero_banner.py
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Создание тестового Hero баннера с видео
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Добавляем путь к Django проекту
|
||||
sys.path.append('/home/data/smartsoltech.kr/smartsoltech')
|
||||
|
||||
# Настройка Django
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'smartsoltech.settings')
|
||||
|
||||
import django
|
||||
django.setup()
|
||||
|
||||
from web.models import HeroBanner
|
||||
|
||||
def create_hero_banner():
|
||||
"""Создает тестовый Hero баннер с видео"""
|
||||
|
||||
# Создаем баннер с видео
|
||||
hero_video = HeroBanner.objects.create(
|
||||
title="Smart Solutions для вашего бизнеса",
|
||||
subtitle="Профессиональная разработка и внедрение IT-решений",
|
||||
description="Мы создаем инновативные технологические решения, которые помогут вашему бизнесу достичь новых высот эффективности и успеха.",
|
||||
button_text="Получить консультацию",
|
||||
button_link="/contact/",
|
||||
video="static/video/hero/hero-demo.mp4",
|
||||
is_active=True,
|
||||
order=1
|
||||
)
|
||||
|
||||
# Создаем баннер с изображением (fallback)
|
||||
hero_image = HeroBanner.objects.create(
|
||||
title="Цифровые решения нового поколения",
|
||||
subtitle="Автоматизация, интеграция, оптимизация",
|
||||
description="Трансформируйте свой бизнес с помощью наших передовых IT-решений и экспертного подхода к каждому проекту.",
|
||||
button_text="Наши услуги",
|
||||
button_link="/services/",
|
||||
image="static/img/about/about-1.jpg",
|
||||
is_active=True,
|
||||
order=2
|
||||
)
|
||||
|
||||
print(f"✅ Создан Hero баннер с видео: {hero_video.title}")
|
||||
print(f"✅ Создан Hero баннер с изображением: {hero_image.title}")
|
||||
|
||||
# Показываем все активные баннеры
|
||||
active_banners = HeroBanner.objects.filter(is_active=True).order_by('order')
|
||||
print(f"\n📋 Всего активных баннеров: {active_banners.count()}")
|
||||
for banner in active_banners:
|
||||
media_type = "🎬 Видео" if banner.video else "🖼️ Изображение"
|
||||
print(f" {banner.order}. {banner.title} ({media_type})")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_hero_banner()
|
||||
35
scripts/hero_script.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from web.models import HeroBanner
|
||||
|
||||
# Создаем баннер с видео
|
||||
hero_video = HeroBanner.objects.create(
|
||||
title="Smart Solutions для вашего бизнеса",
|
||||
subtitle="Профессиональная разработка и внедрение IT-решений",
|
||||
description="Мы создаем инновативные технологические решения, которые помогут вашему бизнесу достичь новых высот эффективности и успеха.",
|
||||
button_text="Получить консультацию",
|
||||
button_link="/contact/",
|
||||
video="static/video/hero/hero-demo.mp4",
|
||||
is_active=True,
|
||||
order=1
|
||||
)
|
||||
|
||||
# Создаем баннер с изображением (fallback)
|
||||
hero_image = HeroBanner.objects.create(
|
||||
title="Цифровые решения нового поколения",
|
||||
subtitle="Автоматизация, интеграция, оптимизация",
|
||||
description="Трансформируйте свой бизнес с помощью наших передовых IT-решений и экспертного подхода к каждому проекту.",
|
||||
button_text="Наши услуги",
|
||||
button_link="/services/",
|
||||
image="static/img/about/about-1.jpg",
|
||||
is_active=True,
|
||||
order=2
|
||||
)
|
||||
|
||||
print(f"✅ Создан Hero баннер с видео: {hero_video.title}")
|
||||
print(f"✅ Создан Hero баннер с изображением: {hero_image.title}")
|
||||
|
||||
# Показываем все активные баннеры
|
||||
active_banners = HeroBanner.objects.filter(is_active=True).order_by('order')
|
||||
print(f"\n📋 Всего активных баннеров: {active_banners.count()}")
|
||||
for banner in active_banners:
|
||||
media_type = "🎬 Видео" if banner.video else "🖼️ Изображение"
|
||||
print(f" {banner.order}. {banner.title} ({media_type})")
|
||||
@@ -12,8 +12,17 @@ from django.utils.crypto import get_random_string
|
||||
|
||||
class TelegramBot:
|
||||
def __init__(self):
|
||||
# ПРОВЕРЯЕМ ОТКЛЮЧЕНИЕ БОТА ДЛЯ ТЕСТОВ
|
||||
if os.environ.get('DISABLE_TELEGRAM_BOT') == 'True':
|
||||
logging.info("[TelegramBot] Бот отключен для тестирования")
|
||||
raise Exception("Telegram bot disabled for testing")
|
||||
|
||||
# Получение настроек бота из базы данных
|
||||
bot_settings = TelegramSettings.objects.first()
|
||||
try:
|
||||
bot_settings = TelegramSettings.objects.first()
|
||||
except Exception as e:
|
||||
logging.error(f"[TelegramBot] Ошибка доступа к настройкам: {e}")
|
||||
raise Exception("Telegram bot settings not found or token is empty")
|
||||
if bot_settings and bot_settings.bot_token:
|
||||
TELEGRAM_BOT_TOKEN = bot_settings.bot_token.strip()
|
||||
|
||||
|
||||
195
smartsoltech/settings_test.py
Normal file
@@ -0,0 +1,195 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
import dj_database_url
|
||||
from decouple import config as original_config
|
||||
|
||||
# КРИТИЧЕСКИ ВАЖНО: Перехватываем config() для CSRF_TRUSTED_ORIGINS
|
||||
def patched_config(key, default=None, cast=None):
|
||||
if key == 'CSRF_TRUSTED_ORIGINS':
|
||||
# Всегда возвращаем пустую строку для CSRF_TRUSTED_ORIGINS
|
||||
return ''
|
||||
return original_config(key, default, cast)
|
||||
|
||||
# Заменяем config в модуле decouple
|
||||
import decouple
|
||||
decouple.config = patched_config
|
||||
|
||||
# ОТКЛЮЧАЕМ инициализацию Telegram бота в тестах
|
||||
os.environ['DISABLE_TELEGRAM_BOT'] = 'True'
|
||||
|
||||
from .settings import *
|
||||
|
||||
print("🧪 Test settings loaded")
|
||||
|
||||
# НЕМЕДЛЕННОЕ переопределение CSRF_TRUSTED_ORIGINS после импорта
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
'http://localhost',
|
||||
'http://127.0.0.1',
|
||||
'http://postgres',
|
||||
'https://smartsoltech.kr'
|
||||
]
|
||||
|
||||
print("🔒 CSRF_TRUSTED_ORIGINS НЕМЕДЛЕННО переопределен:", CSRF_TRUSTED_ORIGINS)
|
||||
print("🔍 Проверка типа CSRF_TRUSTED_ORIGINS:", type(CSRF_TRUSTED_ORIGINS))
|
||||
print("🔍 Длина CSRF_TRUSTED_ORIGINS:", len(CSRF_TRUSTED_ORIGINS))
|
||||
|
||||
# База данных для тестирования
|
||||
DATABASES = {
|
||||
'default': dj_database_url.config(
|
||||
default=os.environ.get(
|
||||
'DATABASE_URL',
|
||||
'postgresql://postgres:postgres@postgres:5432/smartsoltech_test'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
print("📊 Database:", DATABASES['default']['NAME'], "at", DATABASES['default']['HOST'])
|
||||
|
||||
# Секретный ключ для тестирования
|
||||
SECRET_KEY = os.environ.get(
|
||||
'SECRET_KEY',
|
||||
'test-secret-key-for-ci-very-long-and-secure-key-12345'
|
||||
)
|
||||
print("🔐 Secret key length:", len(SECRET_KEY))
|
||||
|
||||
# Отладка отключена в тестах
|
||||
DEBUG = os.environ.get('DEBUG', 'False').lower() in ['true', '1', 'yes']
|
||||
|
||||
# Разрешенные хосты для CI
|
||||
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost,127.0.0.1,postgres,*').split(',')
|
||||
|
||||
print("🌐 Allowed hosts:", ALLOWED_HOSTS)
|
||||
print("🔒 CSRF trusted origins:", CSRF_TRUSTED_ORIGINS)
|
||||
|
||||
# Упрощенный хеширователь паролей для быстрых тестов
|
||||
PASSWORD_HASHERS = [
|
||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||
]
|
||||
|
||||
# Отключаем миграции для ускорения тестов
|
||||
class DisableMigrations:
|
||||
def __contains__(self, item):
|
||||
return True
|
||||
|
||||
def __getitem__(self, item):
|
||||
return None
|
||||
|
||||
# В CI НЕ отключаем миграции - нужны для Telegram бота
|
||||
# if os.environ.get('CI'):
|
||||
# MIGRATION_MODULES = DisableMigrations()
|
||||
|
||||
# Вместо этого используем быстрые миграции в памяти
|
||||
if os.environ.get('CI'):
|
||||
# Ускоряем тесты, но оставляем миграции
|
||||
TEST_RUNNER = 'django.test.runner.DiscoverRunner'
|
||||
|
||||
# Логирование для отладки в CI
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'django': {
|
||||
'handlers': ['console'],
|
||||
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
|
||||
},
|
||||
'web': {
|
||||
'handlers': ['console'],
|
||||
'level': 'DEBUG',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Кеширование отключено для тестов
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
|
||||
}
|
||||
}
|
||||
|
||||
# Email для тестов (console backend)
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
# Медиа файлы для тестов
|
||||
MEDIA_ROOT = '/tmp/media_test/'
|
||||
|
||||
# Статические файлы для тестов
|
||||
STATIC_ROOT = '/tmp/static_test/'
|
||||
|
||||
# Telegram Bot настройки для тестирования
|
||||
TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', 'test-token')
|
||||
|
||||
# Отключаем инициализацию Telegram бота в тестах
|
||||
TELEGRAM_BOT_ENABLED = False
|
||||
|
||||
# Отключаем CSRF для API тестов
|
||||
if 'test' in sys.argv:
|
||||
CSRF_COOKIE_SECURE = False
|
||||
CSRF_COOKIE_HTTPONLY = False
|
||||
|
||||
# Отключаем сигналы для ускорения тестов
|
||||
if 'test' in sys.argv:
|
||||
from django.core.signals import setting_changed
|
||||
setting_changed.disconnect()
|
||||
|
||||
# КРИТИЧЕСКИ ВАЖНО: Финальное переопределение CSRF_TRUSTED_ORIGINS
|
||||
# Django 4.0+ требует схемы (http://, https://)
|
||||
# Игнорируем переменную окружения и задаем напрямую
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
'http://localhost',
|
||||
'http://127.0.0.1',
|
||||
'http://postgres',
|
||||
'https://smartsoltech.kr'
|
||||
]
|
||||
|
||||
# Принудительно очищаем любые пустые значения
|
||||
if '' in CSRF_TRUSTED_ORIGINS:
|
||||
CSRF_TRUSTED_ORIGINS.remove('')
|
||||
|
||||
print("🔒 ФИНАЛЬНАЯ проверка CSRF_TRUSTED_ORIGINS:", CSRF_TRUSTED_ORIGINS)
|
||||
|
||||
# Дополнительная проверка - если все еще пустой, принудительно устанавливаем
|
||||
if not CSRF_TRUSTED_ORIGINS or CSRF_TRUSTED_ORIGINS == ['']:
|
||||
CSRF_TRUSTED_ORIGINS = ['http://localhost', 'https://smartsoltech.kr']
|
||||
print("🚨 ПРИНУДИТЕЛЬНАЯ установка CSRF_TRUSTED_ORIGINS:", CSRF_TRUSTED_ORIGINS)
|
||||
|
||||
# Дополнительное переопределение на случай поздней загрузки из config()
|
||||
def override_csrf_config():
|
||||
"""Функция для принудительного переопределения CSRF настроек"""
|
||||
global CSRF_TRUSTED_ORIGINS
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
'http://localhost',
|
||||
'http://127.0.0.1',
|
||||
'http://postgres',
|
||||
'https://smartsoltech.kr'
|
||||
]
|
||||
print("🔒 OVERRIDE CSRF_TRUSTED_ORIGINS:", CSRF_TRUSTED_ORIGINS)
|
||||
|
||||
# Вызываем переопределение
|
||||
override_csrf_config()
|
||||
|
||||
# АБСОЛЮТНО ФИНАЛЬНОЕ переопределение
|
||||
# Это должно быть ПОСЛЕДНИМ в файле
|
||||
import sys
|
||||
if __name__ != '__main__':
|
||||
# Принудительное переопределение CSRF_TRUSTED_ORIGINS
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
'http://localhost',
|
||||
'http://127.0.0.1',
|
||||
'http://postgres',
|
||||
'https://smartsoltech.kr'
|
||||
]
|
||||
print("🔥 ABSOLUTE FINAL CSRF_TRUSTED_ORIGINS:", CSRF_TRUSTED_ORIGINS)
|
||||
|
||||
# Проверяем результат
|
||||
if CSRF_TRUSTED_ORIGINS and CSRF_TRUSTED_ORIGINS != ['']:
|
||||
print("✅ CSRF_TRUSTED_ORIGINS configured correctly!")
|
||||
else:
|
||||
print("❌ CSRF_TRUSTED_ORIGINS STILL EMPTY!")
|
||||
# Аварийная установка
|
||||
CSRF_TRUSTED_ORIGINS = ['https://smartsoltech.kr']
|
||||
@@ -30,7 +30,22 @@ DEBUG = True
|
||||
|
||||
# Allowed hosts and CSRF trusted origins
|
||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost').split(',')
|
||||
CSRF_TRUSTED_ORIGINS = config('CSRF_TRUSTED_ORIGINS', default='').split(',')
|
||||
|
||||
# ИСПРАВЛЕНИЕ для Django 4.0+ совместимости
|
||||
try:
|
||||
csrf_origins_str = config('CSRF_TRUSTED_ORIGINS', default='')
|
||||
if isinstance(csrf_origins_str, str) and csrf_origins_str.strip():
|
||||
CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in csrf_origins_str.split(',') if origin.strip()]
|
||||
else:
|
||||
raise ValueError("Empty CSRF origins")
|
||||
except:
|
||||
# Для тестов и CI используем схемы по умолчанию
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
'http://localhost',
|
||||
'http://127.0.0.1',
|
||||
'http://postgres',
|
||||
'https://smartsoltech.kr'
|
||||
]
|
||||
|
||||
print(f"ALLOWED_HOSTS: {ALLOWED_HOSTS}")
|
||||
print(f"CSRF_TRUSTED_ORIGINS: {CSRF_TRUSTED_ORIGINS}")
|
||||
@@ -45,6 +60,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'tinymce',
|
||||
'web',
|
||||
'comunication'
|
||||
]
|
||||
@@ -143,6 +159,36 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # Папка для соб
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
|
||||
# TinyMCE Configuration
|
||||
TINYMCE_DEFAULT_CONFIG = {
|
||||
'height': 500,
|
||||
'width': '100%',
|
||||
'cleanup_on_startup': True,
|
||||
'custom_undo_redo_levels': 20,
|
||||
'selector': 'textarea',
|
||||
'theme': 'silver',
|
||||
'plugins': '''
|
||||
textcolor save link image media preview codesample contextmenu
|
||||
table code lists fullscreen insertdatetime nonbreaking
|
||||
contextmenu directionality searchreplace wordcount visualblocks
|
||||
visualchars code fullscreen autolink lists charmap print hr
|
||||
anchor pagebreak
|
||||
''',
|
||||
'toolbar1': '''
|
||||
fullscreen preview bold italic underline | fontselect,
|
||||
fontsizeselect | forecolor backcolor | alignleft alignright |
|
||||
aligncenter alignjustify | indent outdent | bullist numlist table |
|
||||
| link image media | codesample |
|
||||
''',
|
||||
'toolbar2': '''
|
||||
visualblocks visualchars |
|
||||
charmap hr pagebreak nonbreaking anchor | code |
|
||||
''',
|
||||
'contextmenu': 'formats | link image',
|
||||
'menubar': True,
|
||||
'statusbar': True,
|
||||
}
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
|
||||
|
||||
|
||||
111
smartsoltech/smartsoltech/settings_test.py
Normal file
@@ -0,0 +1,111 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Настройки Django для тестирования
|
||||
"""
|
||||
|
||||
from .settings import *
|
||||
import os
|
||||
|
||||
# Отключаем отладку для тестов
|
||||
DEBUG = False
|
||||
|
||||
# Базовые настройки для CI/CD
|
||||
SECRET_KEY = os.getenv('SECRET_KEY', 'test-secret-key-for-ci-very-long-and-secure-key-12345')
|
||||
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'postgres', '*']
|
||||
|
||||
# Настройки базы данных для тестирования
|
||||
if 'DATABASE_URL' in os.environ:
|
||||
import dj_database_url
|
||||
DATABASES = {
|
||||
'default': dj_database_url.config(
|
||||
default=os.environ.get('DATABASE_URL'),
|
||||
conn_max_age=600,
|
||||
conn_health_checks=True,
|
||||
)
|
||||
}
|
||||
else:
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': os.getenv('POSTGRES_DB', 'smartsoltech_test'),
|
||||
'USER': os.getenv('POSTGRES_USER', 'postgres'),
|
||||
'PASSWORD': os.getenv('POSTGRES_PASSWORD', 'postgres'),
|
||||
'HOST': os.getenv('POSTGRES_HOST', 'postgres'),
|
||||
'PORT': int(os.getenv('POSTGRES_PORT', 5432)),
|
||||
'TEST': {
|
||||
'NAME': 'test_smartsoltech',
|
||||
},
|
||||
'OPTIONS': {
|
||||
'connect_timeout': 60,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Настройки для тестирования
|
||||
TEST_RUNNER = 'django.test.runner.DiscoverRunner'
|
||||
|
||||
# Отключаем миграции для ускорения тестов (опционально)
|
||||
class DisableMigrations:
|
||||
def __contains__(self, item):
|
||||
return True
|
||||
|
||||
def __getitem__(self, item):
|
||||
return None
|
||||
|
||||
# Раскомментируйте для отключения миграций в тестах
|
||||
# MIGRATION_MODULES = DisableMigrations()
|
||||
|
||||
# Простые настройки паролей для тестов
|
||||
AUTH_PASSWORD_VALIDATORS = []
|
||||
|
||||
# Настройки логирования для тестов
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
},
|
||||
},
|
||||
'root': {
|
||||
'handlers': ['console'],
|
||||
'level': 'WARNING',
|
||||
},
|
||||
'loggers': {
|
||||
'django': {
|
||||
'handlers': ['console'],
|
||||
'level': 'WARNING',
|
||||
'propagate': False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Отключаем статические файлы и медиа в тестах
|
||||
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
|
||||
|
||||
# Настройки для быстрого хеширования паролей в тестах
|
||||
PASSWORD_HASHERS = [
|
||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||
]
|
||||
|
||||
# Настройки Telegram бота для тестов
|
||||
TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', 'test-token-for-ci')
|
||||
|
||||
# Отключаем CSRF для тестов API
|
||||
CSRF_COOKIE_SECURE = False
|
||||
CSRF_USE_SESSIONS = False
|
||||
|
||||
# Настройки кэширования для тестов
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
}
|
||||
}
|
||||
|
||||
# Отключаем отправку email в тестах
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
|
||||
|
||||
print(f"🧪 Test settings loaded")
|
||||
print(f"📊 Database: {DATABASES['default']['NAME']} at {DATABASES['default']['HOST']}")
|
||||
print(f"🔐 Secret key length: {len(SECRET_KEY)}")
|
||||
print(f"🌐 Allowed hosts: {ALLOWED_HOSTS}")
|
||||
@@ -1,8 +1,16 @@
|
||||
# smartsoltech/urls.py
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('tinymce/', include('tinymce.urls')),
|
||||
path('', include('web.urls')), # Включаем маршруты приложения web
|
||||
]
|
||||
|
||||
# Serve media files in development
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
|
||||
589
smartsoltech/static/assets/css/compact-gallery.css
Normal file
@@ -0,0 +1,589 @@
|
||||
/* Современная медиа-галерея */
|
||||
.modern-media-gallery {
|
||||
background: white;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #f1f5f9;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
/* Основное медиа */
|
||||
.main-media-container {
|
||||
position: relative;
|
||||
aspect-ratio: 16/10;
|
||||
overflow: hidden;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.main-media-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.main-media-item {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.main-media-item.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.main-media-img,
|
||||
.main-media-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.embed-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.main-media-embed {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Overlay с информацией */
|
||||
.media-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||||
padding: 2rem;
|
||||
color: white;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.main-media-item:hover .media-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.media-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.media-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.media-meta {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.media-action-btn {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.media-action-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Навигационные кнопки */
|
||||
.media-nav-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
color: #4f46e5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.main-media-wrapper:hover .media-nav-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.prev-btn {
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.media-nav-btn:hover {
|
||||
background: white;
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Миниатюры */
|
||||
.thumbnails-container {
|
||||
padding: 1.5rem;
|
||||
background: #fafbfc;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.thumbnails-wrapper {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Для Firefox */
|
||||
.thumbnails-wrapper {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #cbd5e0 transparent;
|
||||
}
|
||||
|
||||
/* Для Webkit браузеров */
|
||||
.thumbnails-wrapper::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.thumbnails-wrapper::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.thumbnails-wrapper::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.thumbnail-item {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.thumbnail-item:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.thumbnail-item.active {
|
||||
border-color: #4f46e5;
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
||||
}
|
||||
|
||||
.thumbnail-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.video-thumbnail-placeholder,
|
||||
.embed-thumbnail-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.media-type-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.media-type-badge.video {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.media-type-badge.embed {
|
||||
background: #06b6d4;
|
||||
}
|
||||
|
||||
.thumbnail-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.thumbnail-item:hover .thumbnail-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.thumbnail-number {
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Индикатор прогресса */
|
||||
.gallery-progress {
|
||||
height: 4px;
|
||||
background: #f1f5f9;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4f46e5 0%, #7c3aed 100%);
|
||||
transition: width 0.4s ease;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Placeholder для проектов без изображений */
|
||||
.project-placeholder-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.project-placeholder-image i {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.project-placeholder-image p {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 1024px) {
|
||||
.main-media-container {
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
|
||||
.media-overlay {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.thumbnails-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.thumbnail-item {
|
||||
width: 70px;
|
||||
height: 52px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.modern-media-gallery {
|
||||
border-radius: 16px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.media-overlay {
|
||||
padding: 1rem;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.media-nav-btn {
|
||||
opacity: 1;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.prev-btn {
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.thumbnails-container {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.thumbnail-item {
|
||||
width: 60px;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.media-action-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* HTML-контент в описании проекта */
|
||||
.description-text h1,
|
||||
.description-text h2,
|
||||
.description-text h3,
|
||||
.description-text h4,
|
||||
.description-text h5,
|
||||
.description-text h6 {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 600;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.description-text h1 { font-size: 2rem; }
|
||||
.description-text h2 { font-size: 1.75rem; }
|
||||
.description-text h3 { font-size: 1.5rem; }
|
||||
.description-text h4 { font-size: 1.25rem; }
|
||||
.description-text h5 { font-size: 1.1rem; }
|
||||
.description-text h6 { font-size: 1rem; }
|
||||
|
||||
.description-text p {
|
||||
margin-bottom: 1.2rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.description-text ul,
|
||||
.description-text ol {
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.description-text li {
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.description-text blockquote {
|
||||
border-left: 4px solid #4f46e5;
|
||||
padding-left: 1.5rem;
|
||||
margin: 2rem 0;
|
||||
font-style: italic;
|
||||
background: #f8fafc;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.description-text code {
|
||||
background: #f1f5f9;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.9em;
|
||||
color: #e53e3e;
|
||||
}
|
||||
|
||||
.description-text pre {
|
||||
background: #1a202c;
|
||||
color: #fff;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.description-text pre code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.description-text a {
|
||||
color: #4f46e5;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.description-text a:hover {
|
||||
border-bottom-color: #4f46e5;
|
||||
}
|
||||
|
||||
.description-text strong,
|
||||
.description-text b {
|
||||
font-weight: 600;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.description-text em,
|
||||
.description-text i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.description-text img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 1.5rem 0;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.description-text hr {
|
||||
border: none;
|
||||
height: 2px;
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
||||
margin: 3rem 0;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.description-text table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 2rem 0;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.description-text th,
|
||||
.description-text td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.description-text th {
|
||||
background: #f8fafc;
|
||||
font-weight: 600;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
/* HTML-контент в технологиях */
|
||||
.technologies-html-content {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.technologies-html-content p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.technologies-html-content code {
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
margin: 0.25rem 0;
|
||||
box-shadow: 0 2px 8px rgba(79, 70, 229, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.technologies-html-content code:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
||||
}
|
||||
|
||||
.technologies-html-content p code {
|
||||
margin: 0.2rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.technologies-html-content h1,
|
||||
.technologies-html-content h2,
|
||||
.technologies-html-content h3,
|
||||
.technologies-html-content h4,
|
||||
.technologies-html-content h5,
|
||||
.technologies-html-content h6 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.technologies-html-content ul,
|
||||
.technologies-html-content ol {
|
||||
margin: 1rem 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.technologies-html-content li {
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.technologies-html-content strong,
|
||||
.technologies-html-content b {
|
||||
font-weight: 600;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.technologies-html-content em,
|
||||
.technologies-html-content i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.technologies-html-content a {
|
||||
color: #4f46e5;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.technologies-html-content a:hover {
|
||||
border-bottom-color: #4f46e5;
|
||||
}
|
||||
358
smartsoltech/static/assets/js/modern-scripts-broken.js
Normal file
@@ -0,0 +1,358 @@
|
||||
// Modern Scripts for SmartSolTech Website
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('SmartSolTech: DOM loaded, initializing...');
|
||||
|
||||
// Hide loading screen
|
||||
const loadingScreen = document.getElementById('loading-screen');
|
||||
if (loadingScreen) {
|
||||
console.log('SmartSolTech: Loading screen found, hiding...');
|
||||
setTimeout(() => {
|
||||
loadingScreen.style.opacity = '0';
|
||||
loadingScreen.style.pointerEvents = 'none';
|
||||
setTimeout(() => {
|
||||
loadingScreen.style.display = 'none';
|
||||
loadingScreen.remove(); // Полностью удаляем элемент
|
||||
console.log('SmartSolTech: Loading screen removed');
|
||||
}, 300);
|
||||
}, 500); // Уменьшили время ожидания
|
||||
} else {
|
||||
console.log('SmartSolTech: Loading screen not found');
|
||||
}
|
||||
|
||||
// Theme Toggle Functionality
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const html = document.documentElement;
|
||||
|
||||
// Check for saved theme preference
|
||||
const currentTheme = localStorage.getItem('theme') || 'light';
|
||||
html.setAttribute('data-theme', currentTheme);
|
||||
updateThemeIcon(currentTheme);
|
||||
|
||||
themeToggle.addEventListener('click', function() {
|
||||
const currentTheme = html.getAttribute('data-theme');
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
|
||||
html.setAttribute('data-theme', newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
updateThemeIcon(newTheme);
|
||||
|
||||
// Add animation
|
||||
this.style.transform = 'scale(0.8)';
|
||||
setTimeout(() => {
|
||||
this.style.transform = 'scale(1)';
|
||||
}, 150);
|
||||
});
|
||||
|
||||
function updateThemeIcon(theme) {
|
||||
const icon = themeToggle.querySelector('i');
|
||||
if (theme === 'dark') {
|
||||
icon.className = 'fas fa-sun';
|
||||
themeToggle.setAttribute('aria-label', 'Переключить на светлую тему');
|
||||
} else {
|
||||
icon.className = 'fas fa-moon';
|
||||
themeToggle.setAttribute('aria-label', 'Переключить на темную тему');
|
||||
}
|
||||
}
|
||||
|
||||
// Navbar scroll behavior
|
||||
const navbar = document.querySelector('.navbar-modern');
|
||||
let lastScrollTop = 0;
|
||||
|
||||
window.addEventListener('scroll', function() {
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
|
||||
// Add/remove scrolled class
|
||||
if (scrollTop > 50) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
|
||||
// Hide/show navbar on scroll
|
||||
if (scrollTop > lastScrollTop && scrollTop > 100) {
|
||||
navbar.style.transform = 'translateY(-100%)';
|
||||
} else {
|
||||
navbar.style.transform = 'translateY(0)';
|
||||
}
|
||||
|
||||
lastScrollTop = scrollTop;
|
||||
});
|
||||
|
||||
// Scroll to top button
|
||||
const scrollToTopBtn = document.getElementById('scroll-to-top');
|
||||
|
||||
window.addEventListener('scroll', function() {
|
||||
if (window.pageYOffset > 300) {
|
||||
scrollToTopBtn.style.display = 'block';
|
||||
scrollToTopBtn.style.opacity = '1';
|
||||
} else {
|
||||
scrollToTopBtn.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
if (window.pageYOffset <= 300) {
|
||||
scrollToTopBtn.style.display = 'none';
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
scrollToTopBtn.addEventListener('click', function() {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
});
|
||||
|
||||
// Smooth scrolling for anchor links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
const offsetTop = target.offsetTop - 80; // Account for fixed navbar
|
||||
window.scrollTo({
|
||||
top: offsetTop,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Intersection Observer for animations
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(function(entries) {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('animate-fade-in-up');
|
||||
// Add stagger delay for child elements
|
||||
const children = entry.target.querySelectorAll('.service-card, .feature-list > *, .step-card');
|
||||
children.forEach((child, index) => {
|
||||
setTimeout(() => {
|
||||
child.classList.add('animate-fade-in-up');
|
||||
}, index * 100);
|
||||
});
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// Observe elements for animation
|
||||
document.querySelectorAll('.service-card, .card-modern, .step-card').forEach(el => {
|
||||
observer.observe(el);
|
||||
});
|
||||
|
||||
// Form enhancements
|
||||
const forms = document.querySelectorAll('form');
|
||||
forms.forEach(form => {
|
||||
form.addEventListener('submit', function(e) {
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
if (submitBtn) {
|
||||
const originalContent = submitBtn.innerHTML;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Отправляем...';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
// Re-enable after 3 seconds (in case of slow response)
|
||||
setTimeout(() => {
|
||||
submitBtn.innerHTML = originalContent;
|
||||
submitBtn.disabled = false;
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Parallax effect for hero section
|
||||
window.addEventListener('scroll', function() {
|
||||
const scrolled = window.pageYOffset;
|
||||
const parallaxElements = document.querySelectorAll('.animate-float');
|
||||
|
||||
parallaxElements.forEach(element => {
|
||||
const speed = 0.5;
|
||||
element.style.transform = `translateY(${scrolled * speed}px)`;
|
||||
});
|
||||
});
|
||||
|
||||
// Service cards hover effect
|
||||
document.querySelectorAll('.service-card').forEach(card => {
|
||||
card.addEventListener('mouseenter', function() {
|
||||
this.style.transform = 'translateY(-10px) scale(1.02)';
|
||||
});
|
||||
|
||||
card.addEventListener('mouseleave', function() {
|
||||
this.style.transform = 'translateY(0) scale(1)';
|
||||
});
|
||||
});
|
||||
|
||||
// Card modern hover effects
|
||||
document.querySelectorAll('.card-modern').forEach(card => {
|
||||
card.addEventListener('mouseenter', function() {
|
||||
this.style.boxShadow = '0 25px 50px -12px rgba(0, 0, 0, 0.25)';
|
||||
});
|
||||
|
||||
card.addEventListener('mouseleave', function() {
|
||||
this.style.boxShadow = 'var(--shadow)';
|
||||
});
|
||||
});
|
||||
|
||||
// Add loading animation to buttons
|
||||
document.querySelectorAll('.btn-primary-modern, .btn-secondary-modern').forEach(btn => {
|
||||
btn.addEventListener('click', function(e) {
|
||||
// Create ripple effect
|
||||
const ripple = document.createElement('span');
|
||||
const rect = this.getBoundingClientRect();
|
||||
const size = Math.max(rect.width, rect.height);
|
||||
const x = e.clientX - rect.left - size / 2;
|
||||
const y = e.clientY - rect.top - size / 2;
|
||||
|
||||
ripple.style.cssText = `
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
transform: scale(0);
|
||||
animation: ripple 0.6s linear;
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
left: ${x}px;
|
||||
top: ${y}px;
|
||||
`;
|
||||
|
||||
this.style.position = 'relative';
|
||||
this.style.overflow = 'hidden';
|
||||
this.appendChild(ripple);
|
||||
|
||||
setTimeout(() => {
|
||||
ripple.remove();
|
||||
}, 600);
|
||||
});
|
||||
});
|
||||
|
||||
// Typing animation for hero text (optional)
|
||||
const typingText = document.querySelector('.typing-text');
|
||||
if (typingText) {
|
||||
const text = typingText.textContent;
|
||||
typingText.textContent = '';
|
||||
let i = 0;
|
||||
|
||||
function typeWriter() {
|
||||
if (i < text.length) {
|
||||
typingText.textContent += text.charAt(i);
|
||||
i++;
|
||||
setTimeout(typeWriter, 100);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(typeWriter, 1000);
|
||||
}
|
||||
|
||||
// Mobile menu enhancements
|
||||
const navbarToggler = document.querySelector('.navbar-toggler');
|
||||
const navbarCollapse = document.querySelector('.navbar-collapse');
|
||||
|
||||
if (navbarToggler && navbarCollapse) {
|
||||
navbarToggler.addEventListener('click', function() {
|
||||
const isExpanded = this.getAttribute('aria-expanded') === 'true';
|
||||
|
||||
// Animate the toggler icon
|
||||
this.style.transform = 'rotate(180deg)';
|
||||
setTimeout(() => {
|
||||
this.style.transform = 'rotate(0deg)';
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Close menu when clicking on a link
|
||||
document.querySelectorAll('.navbar-nav .nav-link').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
const bsCollapse = new bootstrap.Collapse(navbarCollapse, {
|
||||
hide: true
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Newsletter form
|
||||
const newsletterForm = document.querySelector('footer form');
|
||||
if (newsletterForm) {
|
||||
newsletterForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const email = this.querySelector('input[type="email"]').value;
|
||||
|
||||
if (email) {
|
||||
// Show success message
|
||||
const button = this.querySelector('button');
|
||||
const originalContent = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-check"></i>';
|
||||
button.style.background = '#10b981';
|
||||
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalContent;
|
||||
button.style.background = '';
|
||||
this.reset();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add CSS for ripple animation
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes ripple {
|
||||
to {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
opacity: 1 !important;
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
|
||||
/* Smooth transitions */
|
||||
.navbar-modern {
|
||||
transition: transform 0.3s ease, background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.service-card, .card-modern {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.6s ease;
|
||||
}
|
||||
|
||||
.step-card {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
transition: all 0.6s ease;
|
||||
}
|
||||
|
||||
.step-card:nth-child(even) {
|
||||
transform: translateX(30px);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Отладка кликов
|
||||
document.addEventListener('click', function(event) {
|
||||
console.log('SmartSolTech: Click detected on:', event.target);
|
||||
|
||||
// Проверяем, не блокируются ли клики
|
||||
const computedStyle = getComputedStyle(event.target);
|
||||
if (computedStyle.pointerEvents === 'none') {
|
||||
console.warn('SmartSolTech: Element has pointer-events: none');
|
||||
}
|
||||
|
||||
// Проверяем z-index элементов
|
||||
let element = event.target;
|
||||
while (element && element !== document.body) {
|
||||
const style = getComputedStyle(element);
|
||||
if (style.zIndex && style.zIndex !== 'auto') {
|
||||
console.log('SmartSolTech: Element z-index:', element.tagName, style.zIndex);
|
||||
}
|
||||
element = element.parentElement;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('SmartSolTech: All scripts loaded successfully');
|
||||
});
|
||||
});
|
||||
@@ -10,55 +10,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
loadingScreen.style.opacity = '0';
|
||||
loadingScreen.style.pointerEvents = 'none';
|
||||
setTimeout(() => {
|
||||
loadingScreen.style.display = 'none';
|
||||
loadingScreen.remove(); // Полностью удаляем элемент
|
||||
console.log('SmartSolTech: Loading screen removed');
|
||||
// Полностью удаляем элемент из DOM
|
||||
if (loadingScreen.parentNode) {
|
||||
loadingScreen.parentNode.removeChild(loadingScreen);
|
||||
console.log('SmartSolTech: Loading screen completely removed from DOM');
|
||||
}
|
||||
}, 300);
|
||||
}, 500); // Уменьшили время ожидания
|
||||
}, 200); // Уменьшили время ожидания до 200ms
|
||||
} else {
|
||||
console.log('SmartSolTech: Loading screen not found');
|
||||
}
|
||||
|
||||
// Theme Toggle Functionality
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const html = document.documentElement;
|
||||
|
||||
if (themeToggle) {
|
||||
// Check for saved theme preference
|
||||
const currentTheme = localStorage.getItem('theme') || 'light';
|
||||
html.setAttribute('data-theme', currentTheme);
|
||||
updateThemeIcon(currentTheme);
|
||||
|
||||
themeToggle.addEventListener('click', function() {
|
||||
const currentTheme = html.getAttribute('data-theme');
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
|
||||
html.setAttribute('data-theme', newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
updateThemeIcon(newTheme);
|
||||
|
||||
// Add animation
|
||||
this.style.transform = 'scale(0.8)';
|
||||
setTimeout(() => {
|
||||
this.style.transform = 'scale(1)';
|
||||
}, 150);
|
||||
});
|
||||
}
|
||||
|
||||
function updateThemeIcon(theme) {
|
||||
if (!themeToggle) return;
|
||||
const icon = themeToggle.querySelector('i');
|
||||
if (icon) {
|
||||
if (theme === 'dark') {
|
||||
icon.className = 'fas fa-sun';
|
||||
themeToggle.setAttribute('aria-label', 'Переключить на светлую тему');
|
||||
} else {
|
||||
icon.className = 'fas fa-moon';
|
||||
themeToggle.setAttribute('aria-label', 'Переключить на темную тему');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Navbar scroll behavior
|
||||
const navbar = document.querySelector('.navbar-modern');
|
||||
if (navbar) {
|
||||
|
||||
227
smartsoltech/static/assets/js/project-detail.js
Normal file
@@ -0,0 +1,227 @@
|
||||
// Modern Project Detail Page Enhancements
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Animate counter numbers
|
||||
function animateCounters() {
|
||||
const counters = document.querySelectorAll('.stat-number[data-target]');
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const counter = entry.target;
|
||||
const target = parseInt(counter.dataset.target);
|
||||
const duration = 2000; // 2 seconds
|
||||
const step = target / (duration / 16); // 60fps
|
||||
let current = 0;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
current += step;
|
||||
counter.textContent = Math.floor(current);
|
||||
|
||||
if (current >= target) {
|
||||
counter.textContent = target;
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, 16);
|
||||
|
||||
observer.unobserve(counter);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
threshold: 0.5
|
||||
});
|
||||
|
||||
counters.forEach(counter => observer.observe(counter));
|
||||
}
|
||||
|
||||
// Scroll-triggered animations
|
||||
function initScrollAnimations() {
|
||||
const animatedElements = document.querySelectorAll('.content-section, .tech-item, .info-item');
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry, index) => {
|
||||
if (entry.isIntersecting) {
|
||||
setTimeout(() => {
|
||||
entry.target.style.opacity = '1';
|
||||
entry.target.style.transform = 'translateY(0)';
|
||||
}, index * 100);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
});
|
||||
|
||||
animatedElements.forEach(el => {
|
||||
el.style.opacity = '0';
|
||||
el.style.transform = 'translateY(30px)';
|
||||
el.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
|
||||
observer.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
// Share functionality
|
||||
function initShareButton() {
|
||||
const shareBtn = document.querySelector('.share-btn');
|
||||
if (shareBtn) {
|
||||
shareBtn.addEventListener('click', async function() {
|
||||
const projectTitle = document.querySelector('.hero-title').textContent;
|
||||
const url = window.location.href;
|
||||
|
||||
if (navigator.share) {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: projectTitle,
|
||||
text: `Посмотрите на этот проект: ${projectTitle}`,
|
||||
url: url
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('Sharing failed:', err);
|
||||
fallbackShare(url);
|
||||
}
|
||||
} else {
|
||||
fallbackShare(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackShare(url) {
|
||||
// Copy to clipboard as fallback
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
showToast('Ссылка скопирована в буфер обмена!');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message) {
|
||||
// Create toast notification
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast-notification';
|
||||
toast.textContent = message;
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: linear-gradient(135deg, #48bb78, #38a169);
|
||||
color: white;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
z-index: 10000;
|
||||
transform: translateX(400px);
|
||||
transition: transform 0.3s ease;
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.transform = 'translateX(0)';
|
||||
}, 100);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.transform = 'translateX(400px)';
|
||||
setTimeout(() => document.body.removeChild(toast), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Tech item hover effects
|
||||
function initTechInteractions() {
|
||||
const techItems = document.querySelectorAll('.tech-item');
|
||||
|
||||
techItems.forEach(item => {
|
||||
item.addEventListener('mouseenter', function() {
|
||||
this.style.transform = 'translateY(-8px) scale(1.02)';
|
||||
});
|
||||
|
||||
item.addEventListener('mouseleave', function() {
|
||||
this.style.transform = 'translateY(-5px) scale(1)';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Parallax effect for hero background
|
||||
function initParallaxEffect() {
|
||||
const heroBackground = document.querySelector('.hero-pattern');
|
||||
if (!heroBackground) return;
|
||||
|
||||
let ticking = false;
|
||||
|
||||
function updateParallax() {
|
||||
const scrolled = window.pageYOffset;
|
||||
const rate = scrolled * -0.3;
|
||||
|
||||
heroBackground.style.transform = `translateY(${rate}px)`;
|
||||
ticking = false;
|
||||
}
|
||||
|
||||
function requestTick() {
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(updateParallax);
|
||||
ticking = true;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', requestTick);
|
||||
}
|
||||
|
||||
// Smooth scroll for internal links
|
||||
function initSmoothScroll() {
|
||||
const links = document.querySelectorAll('a[href^="#"]');
|
||||
|
||||
links.forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const targetId = this.getAttribute('href');
|
||||
const targetSection = document.querySelector(targetId);
|
||||
|
||||
if (targetSection) {
|
||||
targetSection.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize all enhancements
|
||||
animateCounters();
|
||||
initScrollAnimations();
|
||||
initShareButton();
|
||||
initTechInteractions();
|
||||
initParallaxEffect();
|
||||
initSmoothScroll();
|
||||
|
||||
// Add loading class removal after page load
|
||||
window.addEventListener('load', function() {
|
||||
document.body.classList.add('page-loaded');
|
||||
});
|
||||
});
|
||||
|
||||
// CSS for page loading animation
|
||||
const loadingStyles = `
|
||||
body:not(.page-loaded) .project-hero {
|
||||
opacity: 0;
|
||||
transform: translateY(50px);
|
||||
transition: opacity 0.8s ease, transform 0.8s ease;
|
||||
}
|
||||
|
||||
body.page-loaded .project-hero {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.toast-notification {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
|
||||
// Inject loading styles
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.textContent = loadingStyles;
|
||||
document.head.appendChild(styleSheet);
|
||||
BIN
smartsoltech/static/img/favicon.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
smartsoltech/static/img/logo.jpg
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
smartsoltech/static/img/logo.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
121
smartsoltech/static/img/лого (2).svg
Normal file
@@ -0,0 +1,121 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Creator: CorelDRAW 2019 (64-Bit) -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="100mm" height="100mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||
viewBox="0 0 10000 10000"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<font id="FontID1" horiz-adv-x="596" font-variant="normal" style="fill-rule:nonzero" font-weight="400">
|
||||
<font-face
|
||||
font-family="Futura_Book-Normal">
|
||||
<font-face-src>
|
||||
<font-face-name name="Futura_Book-Normal"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
<missing-glyph><path d="M0 0z"/></missing-glyph>
|
||||
<glyph unicode="B" horiz-adv-x="516" d="M216.997 618.991l13.0124 0c8.66769,0 17.4875,-0.152065 26.481,-0.499642 9.01527,-0.325853 17.835,-0.825495 26.5027,-1.49892 16.0103,-1.32514 31.6729,-4.1492 47.0098,-8.49391 15.3368,-4.32299 29.6744,-11.1659 42.9909,-20.507 16.6837,-12.6648 29.8482,-28.3275 39.5151,-46.988 9.66698,-18.6605 15.8147,-37.9945 18.4867,-58.0019 0.67343,-3.99713 1.17307,-7.8422 1.49892,-11.4918 0.347577,-3.67128 0.499642,-7.51635 0.499642,-11.5135 0,-28.0017 -7.32084,-54.5044 -21.9842,-79.4865 -14.6851,-25.0038 -37.6686,-42.1871 -69.0157,-51.5065 36.6693,-7.34256 64.8448,-25.0038 84.5046,-53.0055 19.6598,-28.0017 30.8257,-58.3277 33.4977,-90.9999 0,-1.99857 0,-4.17092 0,-6.49534 0,-2.34614 0,-4.49677 0,-6.49534 0,-27.3499 -5.9957,-53.8309 -17.9871,-79.5082 -12.0131,-25.6555 -29.6744,-46.836 -53.0055,-63.4979 -14.6634,-8.66769 -30.8474,-15.163 -48.5087,-19.5077 -17.6612,-4.32299 -35.4963,-6.82119 -53.5051,-7.49462 -9.3194,-0.67343 -18.4867,-1.17307 -27.4803,-1.49892 -9.01527,-0.325853 -17.835,-0.499642 -26.5027,-0.499642l-145.005 0 0 618.991 128.994 0zm-58.0019 -553.994c4.67056,0 9.34112,-0.173788 14.0117,-0.499642 4.67056,-0.325853 9.3194,-0.499642 13.99,-0.499642 4.67056,0 9.49319,-0.173788 14.5113,-0.499642 4.99642,-0.325853 9.81904,-0.499642 14.4896,-0.499642 2.672,0 5.49606,0 8.49391,0 3.01957,0 6.16949,0 9.51491,0 36.6693,1.32514 70.3191,8.84148 100.993,22.5056 30.6736,13.6641 46.0105,44.164 46.0105,91.4996l0 2.99785c0,45.9887 -14.0117,75.989 -42.0133,90.0007 -28.0017,13.99 -59.0012,21.3325 -92.9985,22.006 -1.99857,0 -3.82334,0 -5.49606,0 -1.67271,0 -3.49749,0 -5.49606,0 -2.672,0 -5.34399,0 -8.01599,0 -2.65027,0 -5.32227,0 -7.99426,0 -3.99713,0 -7.82048,-0.173788 -11.4918,-0.499642 -3.67128,-0.347577 -7.49462,-0.499642 -11.5135,-0.499642 -6.64741,0 -12.9907,-0.173788 -18.9864,-0.499642 -5.9957,-0.347577 -12.0131,-0.499642 -18.0088,-0.499642l0 -225.012zm0 488.997l0 -206.004c3.99713,0 8.16805,0 12.5128,0 4.32299,0 8.81976,0 13.4903,0 0.67343,-0.651706 1.49892,-0.999283 2.49821,-0.999283 0.999283,0 1.8465,0 2.49821,0 3.34543,0 6.84292,0 10.5142,0 3.64956,0 7.16877,0 10.4925,0 1.99857,0 3.82334,0 5.49606,0 1.67271,0 3.49749,0 5.49606,0 33.9973,0.67343 65.1706,7.16877 93.4981,19.5077 28.3492,12.339 43.1864,40.8403 44.5115,85.5039l0 2.99785c0,37.9945 -13.0124,63.3241 -38.9938,76.0107 -26.0031,12.6648 -54.6782,19.9857 -86.0035,21.9842 -5.9957,0.67343 -12.0131,0.999283 -18.0088,0.999283 -5.9957,0 -11.9914,0 -17.9871,0l-40.0148 0z"/>
|
||||
<glyph unicode="E" horiz-adv-x="464" d="M81.0071 0l0 618.991 325.984 0 0 -67.9947 -252.992 0 0 -177.003 252.992 0 0 -67.9947 -252.992 0 0 -238.003 252.992 0 0 -67.9947 -325.984 0z"/>
|
||||
<glyph unicode="F" horiz-adv-x="364" d="M81.0071 0l0 618.991 325.984 0 0 -67.9947 -252.992 0 0 -177.003 252.992 0 0 -67.9947 -252.992 0 0 -305.998 -72.9911 0z"/>
|
||||
<glyph unicode="G" horiz-adv-x="736" d="M427.997 319.01l256.012 0c0,-1.34686 0,-2.84578 0,-4.5185 0,-1.65099 0,-3.14991 0,-4.49677 -0.67343,-7.32084 -1.17307,-14.3375 -1.49892,-20.9849 -0.347577,-6.66913 -0.499642,-13.3383 -0.499642,-20.0074 -1.34686,-17.3354 -3.17164,-34.3449 -5.51778,-51.0069 -2.32442,-16.662 -6.16949,-32.9981 -11.4918,-48.9866 -21.3325,-61.999 -58.8274,-108.009 -112.506,-138.01 -53.6572,-30.0002 -110.486,-44.9895 -170.486,-44.9895 -1.99857,0 -4.17092,0 -6.51706,0 -2.32442,0 -4.82263,0 -7.49462,0 -59.327,2.65027 -115.656,19.334 -168.987,49.9859 -53.353,30.6736 -91.6734,74.338 -115.004,131.015 -5.9957,16.662 -11.3397,33.9973 -16.0103,51.9844 -4.67056,18.0088 -7.66841,35.9959 -8.99355,54.0047 -0.67343,5.34399 -1.17307,10.6663 -1.49892,16.0103 -0.325853,5.32227 -0.499642,10.6663 -0.499642,15.9885 0,8.66769 0.325853,17.3354 0.999283,26.0031 0.67343,8.66769 1.67271,17.3354 2.99785,26.0031 3.99713,28.6534 11.4918,57.0026 22.5056,85.0042 10.9921,28.0017 25.1559,53.6572 42.4913,76.9882 30.0002,37.9945 67.1692,66.3437 111.507,85.0042 44.3378,18.6605 89.8269,28.0017 136.489,28.0017 3.99713,0 8.01599,0 12.0131,0 3.99713,0 8.32012,-0.325853 12.9907,-0.999283 46.6622,-3.34543 91.9992,-15.8365 136.011,-37.4948 43.9902,-21.6801 79.6602,-52.18 106.988,-91.4996l-61.999 -48.009c-22.006,35.3225 -49.66,62.8245 -83.0057,82.506 -33.3239,19.6598 -70.9925,29.5006 -112.984,29.5006l-10.0146 0c-72.6653,-0.67343 -130.493,-27.3499 -173.484,-80.0078 -43.0126,-52.6579 -64.5189,-113.658 -64.5189,-182.999 0,-2.672 0.173788,-5.66985 0.521365,-8.99355 0.325853,-3.34543 0.825495,-6.34328 1.49892,-9.01527 0,-3.3237 0.152065,-6.64741 0.499642,-9.99283 0.325853,-3.3237 0.499642,-6.32155 0.499642,-8.99355 7.99426,-67.343 34.497,-118.828 79.4865,-154.498 45.0112,-35.6701 93.8457,-55.1778 146.504,-58.5015 2.672,-0.67343 5.34399,-0.999283 7.99426,-0.999283 2.672,0 5.34399,0 8.01599,0 53.3313,0 102.818,16.9878 148.48,50.9852 45.6846,34.0191 70.8405,84.3525 75.511,151l-177.003 0 0 65.0186z"/>
|
||||
<glyph unicode="H" horiz-adv-x="627" d="M88.0021 0l0 618.991 71.9918 0 0 -237.981 329.003 0 0 237.981 75.0114 0 0 -618.991 -75.0114 0 0 310.994 -329.003 0 0 -310.994 -71.9918 0z"/>
|
||||
<glyph unicode="I" horiz-adv-x="223" d="M86.0035 0l0 618.991 73.9904 0 0 -618.991 -73.9904 0z"/>
|
||||
<glyph unicode="N" horiz-adv-x="666" d="M83.0057 0l0 642.995 452.002 -493.993 0 469.989 70.9925 0 0 -641.996 -454 487.998 0 -464.993 -68.994 0z"/>
|
||||
<glyph unicode="R" horiz-adv-x="536" d="M163.991 0l-71.9918 0 0 618.991 153.998 0c6.66913,0 13.6641,-0.152065 21.0067,-0.499642 7.32084,-0.325853 14.3375,-0.825495 21.0067,-1.49892 21.9842,-1.32514 42.9909,-5.64812 62.9983,-12.9907 19.9857,-7.34256 36.9952,-19.6598 50.9852,-36.9952 8.66769,-11.3397 15.6627,-24.0045 21.0067,-38.0162 5.32227,-13.99 9.34112,-28.3275 11.9914,-42.9909 0.67343,-5.9957 1.34686,-12.1652 1.99857,-18.5085 0.67343,-6.32155 0.999283,-12.491 0.999283,-18.4867l0 -16.0103c-4.64884,-53.3313 -23.9828,-89.6531 -57.9801,-108.987 -34.0191,-19.334 -74.6856,-29.0009 -121.999,-29.0009l235.983 -295.006 -93.9978 0 -226.989 295.006 -9.01527 0 0 -295.006zm0 555.992l0 -205.983c4.67056,0 9.51491,-0.173788 14.5113,-0.499642 4.99642,-0.347577 10.4925,-0.499642 16.4882,-0.499642 4.01886,0 8.01599,-0.173788 12.0131,-0.499642 3.99713,-0.347577 7.99426,-0.521365 11.9914,-0.521365 1.99857,0 3.84507,0 5.49606,0 1.67271,0 3.17164,0 4.5185,0 2.65027,0 5.32227,0 7.99426,0 2.672,0 5.32227,0 7.99426,0 29.3268,1.34686 56.5029,7.8422 81.5067,19.5077 25.0038,11.6655 37.8207,35.4963 38.4941,71.4922l0 13.0124c0,46.6622 -13.99,75.6631 -41.9916,87.0028 -28.0017,11.318 -59.3487,16.9878 -93.9978,16.9878l-65.0186 0z"/>
|
||||
<glyph unicode="S" horiz-adv-x="506" d="M327.005 343.992c33.3239,-13.99 60.6521,-32.8243 81.9847,-56.5029 21.3542,-23.6569 34.0191,-54.1568 38.0162,-91.4996 0,-1.99857 0.152065,-4.1492 0.499642,-6.49534 0.325853,-2.32442 0.499642,-4.49677 0.499642,-6.49534 0,-2.672 0.152065,-5.1702 0.499642,-7.49462 0.325853,-2.34614 0.499642,-4.49677 0.499642,-6.49534 0,-54.0047 -20.6808,-97.8429 -61.999,-131.514 -41.3399,-33.6715 -86.6769,-50.8331 -136.011,-51.4848 -0.651706,0 -1.15135,0 -1.49892,0 -0.325853,0 -0.825495,0 -1.49892,0 -7.32084,0 -14.6634,0.499642 -22.006,1.49892 -7.32084,0.999283 -14.6634,2.15063 -21.9842,3.49749 -9.34112,1.99857 -18.6822,4.49677 -28.0017,7.49462 -9.34112,2.99785 -18.3347,6.84292 -27.0024,11.4918 -23.3311,13.3383 -41.8395,30.174 -55.5037,50.5072 -13.6641,20.3332 -24.1566,42.8388 -31.4991,67.4951l65.9961 31.9988c0.67343,-1.32514 1.17307,-2.65027 1.49892,-3.99713 0.347577,-1.32514 0.499642,-2.672 0.499642,-3.99713 5.34399,-16.0103 11.8393,-31.6729 19.5077,-47.0098 7.66841,-15.3151 18.8343,-28.3275 33.4977,-38.9938 5.34399,-3.3237 10.84,-6.16949 16.5099,-8.49391 5.64812,-2.34614 11.4918,-4.17092 17.4875,-5.49606 5.34399,-1.99857 10.84,-3.34543 16.5099,-3.99713 5.66985,-0.67343 11.1659,-0.999283 16.4882,-0.999283 31.3471,0 59.8484,10.9921 85.5039,32.9981 25.6772,21.9842 38.4941,49.3342 38.4941,81.9847 0,5.34399 -0.325853,11.0138 -0.999283,17.0095 -0.651706,5.9957 -1.99857,12.339 -3.99713,19.0081 -0.67343,2.65027 -1.49892,5.64812 -2.49821,8.99355 -0.999283,3.3237 -2.17235,6.32155 -3.49749,8.99355 -8.66769,18.0088 -21.3325,32.4984 -37.9945,43.5123 -16.662,10.9921 -34.3449,20.4853 -53.0055,28.5013 -1.99857,0.651706 -4.17092,1.49892 -6.49534,2.49821 -2.34614,0.999283 -4.49677,1.82478 -6.49534,2.49821 -2.02029,0.651706 -4.17092,1.49892 -6.51706,2.49821 -2.32442,0.999283 -4.49677,2.15063 -6.49534,3.49749 -3.3237,1.32514 -6.66913,2.672 -9.99283,3.99713 -3.34543,1.32514 -6.66913,2.672 -10.0146,3.99713 -25.3297,10.6663 -49.4862,23.179 -72.4915,37.4948 -23.0052,14.3375 -41.1661,34.1711 -54.5044,59.5008 -3.99713,7.34256 -7.32084,15.3368 -9.99283,24.0045 -2.672,8.66769 -4.67056,17.3354 -5.9957,26.0031 -0.67343,3.3237 -1.17307,6.84292 -1.49892,10.4925 -0.347577,3.67128 -0.499642,7.16877 -0.499642,10.5142 0,1.99857 0,3.99713 0,5.9957 0,1.99857 0,3.99713 0,5.9957 1.99857,22.6577 8.81976,44.8374 20.4853,66.4958 11.6655,21.6801 27.502,38.8417 47.5094,51.5065 10.6663,6.66913 21.6584,11.9914 32.9981,15.9885 11.3397,4.01886 22.6577,7.34256 33.9973,10.0146 5.9957,0.67343 12.0131,1.32514 18.0088,1.99857 5.9957,0.67343 11.6655,0.999283 16.9878,0.999283 0.67343,0 1.49892,0 2.49821,0 0.999283,0 2.17235,0 3.49749,0 31.3471,-1.32514 61.5211,-9.99283 90.5003,-26.0031 29.0009,-15.9885 51.5065,-38.6679 67.5168,-67.9947l-60.0004 -38.9938c-8.66769,13.99 -18.3347,25.8293 -29.0009,35.4963 -10.6663,9.66698 -21.3325,17.1616 -31.9988,22.4839 -8.01599,4.01886 -16.1623,7.01671 -24.5042,9.01527 -8.34184,1.99857 -16.1623,2.99785 -23.5049,2.99785 -24.6562,0 -46.1625,-8.84148 -64.4972,-26.5027 -18.3347,-17.6612 -27.502,-38.1683 -27.502,-61.4994 0,-5.34399 0.67343,-10.84 1.99857,-16.5099 1.34686,-5.64812 3.34543,-11.4918 5.9957,-17.4875 6.01742,-12.0131 15.3368,-24.0045 28.0017,-35.9959 12.6648,-12.0131 29.3485,-22.3318 50.0076,-30.9995l87.0028 -38.0162z"/>
|
||||
<glyph unicode="T" horiz-adv-x="454" d="M233.007 0l0 548.997 -173.006 0 0 69.9933 423.001 0 0 -69.9933 -173.006 0 0 -548.997 -76.9882 0z"/>
|
||||
<glyph unicode="U" horiz-adv-x="606" d="M72.9911 618.991l74.0121 0 0 -383.985c0,-58.6753 12.339,-103.839 36.9952,-135.511 24.6779,-31.6512 67.9947,-47.4877 129.994,-47.4877 58.6753,0 100.667,15.6627 126.018,46.988 25.3297,31.3471 37.9945,74.6638 37.9945,130.015l0 389.981 74.9897 0 0 -383.985c0,-31.3471 -2.99785,-62.3466 -8.99355,-92.9985 -5.9957,-30.6736 -19.334,-58.6753 -39.993,-84.005 -23.3311,-28.6751 -51.8324,-47.6615 -85.5039,-57.0026 -33.6715,-9.34112 -68.4943,-14.3375 -104.512,-14.9892 -84.6567,0.651706 -145.656,21.8322 -182.999,63.4979 -37.3211,41.6658 -56.655,103.491 -58.0019,185.497l0 383.985z"/>
|
||||
</font>
|
||||
<font id="FontID0" horiz-adv-x="635" font-variant="normal" style="fill-rule:nonzero" font-weight="400">
|
||||
<font-face
|
||||
font-family="Corbel">
|
||||
<font-face-src>
|
||||
<font-face-name name="Corbel"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
<missing-glyph><path d="M0 0z"/></missing-glyph>
|
||||
<glyph unicode="A" horiz-adv-x="635" d="M17.5002 0l265.497 653 68.9994 0 266.499 -653 -86.4996 0 -76.6671 196.002 -275.33 0 -77.0009 -196.002 -85.4981 0zm299.496 552.005c-29.3255,-81.3405 -57.3258,-155.337 -83.829,-221.838l-25.1679 -62.1712 218.5 0 -25.1679 62.3331c-26.8269,67.6742 -54.1596,141.509 -82.3319,221.676l-2.00291 0z"/>
|
||||
<glyph unicode="C" horiz-adv-x="589" d="M399.328 594.997c-36.6594,0 -70.8303,-5.82666 -102.664,-17.3282 -31.8342,-11.5016 -59.5007,-28.8399 -82.9995,-51.6711 -23.3269,-22.993 -41.8285,-51.4992 -55.333,-85.4981 -13.4944,-33.9989 -20.3326,-73.6627 -20.3326,-118.829 0,-45.0049 6.33244,-83.8391 18.8355,-116.998 12.503,-33.0076 30.3371,-60.3403 53.3301,-82.0082 23.165,-21.6679 50.8315,-37.6608 83.3333,-47.9991 32.5018,-10.5001 68.6655,-15.6591 108.501,-15.6591 11.0059,0 22.4974,0.49567 34.6666,1.65898 12.1692,1.00146 24.5003,2.67055 36.8314,4.67346 12.3412,1.9928 24.3384,4.32953 36.1738,7.00008 11.6634,2.66044 22.1636,5.49284 31.3284,8.65906l0 -69.667c-19.1693,-7.49575 -41.4947,-13.1606 -66.8346,-17.3282 -25.5017,-4.00583 -51.4992,-5.99862 -78.1642,-5.99862 -54.3316,0 -102.664,7.49575 -144.665,22.6592 -42.0005,15.1736 -77.1729,37.0033 -105.669,65.3375 -28.496,28.3341 -50.002,62.8389 -64.6698,103.666 -14.6678,40.999 -21.9916,87.3392 -21.9916,139 0,50.8315 7.6576,97.5055 23.165,140.163 15.4973,42.6681 37.8328,79.3376 66.9964,110.008 29.1636,30.4989 64.4979,54.4934 106.003,71.4979 41.6667,17.1664 88.3305,25.6636 140.002,25.6636 13.6663,0 27.3327,-0.49567 40.6652,-1.66909 13.3325,-1.16331 26.1593,-2.8324 38.3285,-4.8252 12.1692,-2.17488 23.4988,-4.67346 33.9989,-7.50587 10.3383,-2.99425 19.3413,-5.99862 26.837,-9.16485l0 -69.839c-19.8369,6.17059 -41.5048,11.5016 -64.8317,15.6693 -23.4988,4.16768 -46.9977,6.33244 -70.8404,6.33244z"/>
|
||||
<glyph unicode="E" horiz-adv-x="551" d="M163.005 72.0037l334.992 0 0 -72.0037 -414.998 0 0 653 396.83 0 0 -72.0037 -316.824 0 0 -210.994 281.824 0 0 -72.0037 -281.824 0 0 -225.995z"/>
|
||||
<glyph unicode="H" horiz-adv-x="667" d="M163.005 370.003l340.991 0 0 282.997 80.0053 0 0 -653 -80.0053 0 0 297.999 -340.991 0 0 -297.999 -80.0053 0 0 653 80.0053 0 0 -282.997z"/>
|
||||
<glyph unicode="L" horiz-adv-x="520" d="M163.005 653l0 -580.997 318.827 0 0 -72.0037 -398.833 0 0 653 80.0053 0z"/>
|
||||
<glyph unicode="M" horiz-adv-x="821" d="M429.332 0l-36.6695 0 -164.33 390.002c-22.6693,53.6639 -45.5005,112.001 -68.3317,175.164l-3.83386 0c4.5015,-90.8291 6.83823,-174.001 6.83823,-249.332l0 -315.833 -80.0053 0 0 653 114.834 0 153.83 -369.497c17.5002,-42.1725 37.0033,-93.1658 58.1654,-152.667l2.33673 0c23.4988,65.995 42.83,116.998 58.1654,152.667l153.84 369.497 114.834 0 0 -653 -80.0053 0 0 315.833c0,73.8347 2.33673,156.996 6.83823,249.332l-3.83386 0c-24.1665,-66.6626 -46.8358,-125.162 -68.3317,-175.164l-164.34 -390.002z"/>
|
||||
<glyph unicode="O" horiz-adv-x="730" d="M587.997 326.495c0,44.0034 -5.82666,82.6758 -17.3282,116.169 -11.5016,33.5033 -27.3327,61.5036 -47.5034,84.001 -20.3326,22.4974 -44.1653,39.5019 -71.6699,51.0035 -27.3327,11.5016 -56.8301,17.3282 -88.4924,17.3282 -31.5004,0 -61.1698,-5.82666 -88.5025,-17.3282 -27.5047,-11.5016 -51.3373,-28.5061 -71.6699,-51.0035 -20.1606,-22.4974 -36.0019,-50.4977 -47.4933,-84.001 -11.5016,-33.4932 -17.3384,-72.1656 -17.3384,-116.169 0,-43.9933 5.83677,-82.4937 17.5002,-115.997 11.6634,-33.3313 27.6665,-61.1698 47.8373,-83.6672 20.3326,-22.4974 44.1653,-39.3299 71.6598,-50.6595 27.3327,-11.5016 57.0021,-17.1664 89.0083,-17.1664 31.5004,0 60.9978,5.66481 87.9967,17.1664 27.1607,11.3296 50.8315,28.1622 71.1641,50.6595 20.1708,22.4974 36.0019,50.3358 47.5034,83.6672 11.5016,33.5033 17.3282,72.0037 17.3282,115.997zm79.0038 0c0,-49.4962 -7.17205,-94.8248 -21.3341,-136.33 -14.162,-41.3328 -34.5047,-77.0009 -60.9978,-106.832 -26.5032,-30.0032 -58.3374,-53.3301 -95.5025,-70.1627 -37.3371,-16.8326 -79.0038,-25.1679 -125.162,-25.1679 -47.8373,0 -90.5054,8.33536 -128.338,25.1679 -37.671,16.8326 -69.667,40.1594 -95.8364,70.1627 -26.1593,29.8313 -46.158,65.4993 -59.9964,106.832 -13.8383,41.5048 -20.8283,86.8334 -20.8283,136.33 0,49.8402 7.16193,95.5025 21.4959,137.007 14.334,41.4947 34.6666,77.3347 60.9978,107.328 26.3312,29.8414 58.1654,53.1682 95.6644,70.0008 37.3371,16.8326 79.3376,25.1679 125.84,25.1679 47.4933,0 89.9996,-8.33536 127.499,-25.1679 37.6608,-16.8326 69.495,-40.1594 95.6644,-70.0008 26.1694,-29.9931 46.1682,-65.8331 59.9964,-107.328 13.8383,-41.5048 20.8384,-87.1672 20.8384,-137.007z"/>
|
||||
<glyph unicode="R" horiz-adv-x="591" d="M163.005 269.999l0 -269.999 -80.0053 0 0 653 169.499 0c28.668,0 53.0064,-1.00146 73.0052,-3.16622 19.9988,-2.16476 38.3285,-5.50295 54.8272,-10.0045 45.6725,-12.3311 80.5009,-33.4932 104.506,-63.3345 24.1665,-29.6593 36.1637,-66.9964 36.1637,-111.496 0,-26.5032 -4.16768,-50.002 -12.6649,-70.8303 -8.49721,-20.8384 -20.3326,-39.0062 -35.668,-54.3316 -15.3354,-15.3354 -33.6651,-27.8385 -54.9992,-37.5091 -21.3341,-9.83248 -45.0049,-16.9944 -71.0023,-21.4959l0 -2.00291 193.332 -278.83 -79.9951 0 -200.999 269.999 -95.9982 0zm0 310.998l0 -238.994 78.6599 0c24.3384,0 45.1667,0.829489 62.5051,2.49858 17.1664,1.49713 32.6637,4.5015 46.33,8.66918 27.6665,8.49721 49.0006,21.9916 64.0022,40.4932 15.0016,18.5017 22.4974,42.8401 22.4974,72.6714 -0.333819,28.0003 -7.49575,50.4977 -21.4959,67.6641 -14.0002,17.0045 -33.3414,29.3356 -58.1654,36.8314 -11.6735,3.50004 -25.6737,6.17059 -42.0005,7.66772 -16.175,1.66909 -36.8415,2.49858 -61.9993,2.49858l-90.3334 0z"/>
|
||||
<glyph unicode="S" horiz-adv-x="551" d="M147.002 480.831c0,-19.1592 3.83386,-35.0004 11.6634,-47.4933 7.83969,-12.675 18.1678,-23.0032 31.3385,-31.1665 13.1606,-8.00154 28.3341,-14.8398 45.6624,-20.0089 17.3384,-5.15902 35.4961,-9.99434 54.1697,-14.6577 25.3298,-6.17059 50.1639,-13.0088 74.6642,-20.5046 24.5003,-7.49575 46.1682,-17.834 65.3375,-31.0047 19.1592,-13.1606 34.6666,-30.3269 46.4919,-51.4992 11.8354,-21.1621 17.6722,-48.3329 17.6722,-81.6642 0,-33.827 -6.33244,-63.0007 -19.1693,-87.6629 -12.8369,-24.5003 -30.6608,-44.671 -53.3301,-60.5021 -22.6693,-15.8311 -49.3344,-27.5047 -80.3391,-35.1623 -30.9946,-7.66772 -64.8317,-11.5016 -101.501,-11.5016 -16.9944,0 -34.3328,1.00146 -51.9948,3.16622 -17.834,2.16476 -34.6666,4.99717 -50.8315,8.49721 -16.1649,3.50004 -30.6709,7.66772 -43.6696,12.1692 -12.9987,4.5015 -23.337,9.16485 -30.8327,13.6663l0 72.4994c11.6634,-5.83677 24.8341,-11.0059 39.5019,-15.8311 14.6678,-4.83532 29.8313,-9.00299 45.5005,-12.3412 15.6693,-3.32807 31.3284,-5.99862 47.3315,-7.99142 16.0031,-1.84106 31.0047,-2.8324 44.9947,-2.8324 22.6693,0 44.5092,1.9928 65.1756,5.82666 20.6664,4.00583 38.8343,10.5001 54.3316,19.5031 15.6693,9.16485 28.0003,21.6679 37.1652,37.327 9.16485,15.8412 13.6663,35.5062 13.6663,59.3388 0,19.5031 -3.99571,35.668 -11.9972,48.3329 -7.83969,12.6649 -18.5017,23.165 -31.8342,31.5004 -13.3325,8.33536 -28.668,15.0016 -46.0063,20.3326 -17.5002,5.16913 -35.6579,10.0045 -54.8272,14.4958 -25.5017,6.00874 -50.1639,12.503 -74.3405,19.8369 -24.3283,7.3339 -45.8242,17.5002 -64.8317,30.4989 -18.9973,12.9987 -34.3328,29.8313 -45.8343,50.3358 -11.4915,20.4945 -17.3282,46.8358 -17.3282,78.67 0,33.1593 5.99862,61.8273 18.1678,85.6599 12.1692,24.0046 28.668,43.8314 49.3344,59.5007 20.8283,15.4973 44.6609,27.1708 71.6598,34.6666 26.9989,7.50587 55.8388,11.1678 86.4996,11.1678 34.8386,0 67.1684,-3.32807 97.3335,-9.99434 30.1752,-6.67638 57.6697,-15.5074 82.8377,-26.1694l0 -73.8347c-26.9989,11.3296 -54.9992,20.6664 -83.829,27.8284 -28.8399,7.17205 -59.6727,10.8339 -92.5083,11.1678 -24.6621,0 -46.158,-2.66044 -64.4979,-8.00154 -18.1678,-5.33099 -33.3313,-12.9987 -45.3286,-22.993 -12.0074,-9.83248 -21.0002,-21.8398 -26.837,-36.0019 -5.83677,-14.162 -8.83103,-29.8313 -8.83103,-47.1696z"/>
|
||||
<glyph unicode="T" horiz-adv-x="555" d="M317.998 0l-79.9951 0 0 580.997 -218.338 0 0 72.0037 516.671 0 0 -72.0037 -218.338 0 0 -580.997z"/>
|
||||
</font>
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
@font-face { font-family:"Futura_Book-Normal";font-variant:normal;font-weight:normal;src:url("#FontID1") format(svg)}
|
||||
@font-face { font-family:"Corbel";font-variant:normal;font-weight:normal;src:url("#FontID0") format(svg)}
|
||||
.fil7 {fill:#A1A1A1}
|
||||
.fil0 {fill:#1C4983}
|
||||
.fil6 {fill:#272E42}
|
||||
.fil4 {fill:#FEFEFE;fill-rule:nonzero}
|
||||
.fil2 {fill:url(#id6)}
|
||||
.fil1 {fill:url(#id7);fill-rule:nonzero}
|
||||
.fil5 {fill:url(#id8);fill-rule:nonzero}
|
||||
.fil3 {fill:url(#id9);fill-rule:nonzero}
|
||||
.fnt1 {font-weight:normal;font-size:460.33px;font-family:'Futura_Book-Normal'}
|
||||
.fnt0 {font-weight:normal;font-size:988.56px;font-family:'Corbel'}
|
||||
]]>
|
||||
</style>
|
||||
<mask id="id0">
|
||||
<linearGradient id="id1" gradientUnits="userSpaceOnUse" x1="4213.8" y1="3203.52" x2="3637.47" y2="3895.11">
|
||||
<stop offset="0" style="stop-opacity:0.266667; stop-color:white"/>
|
||||
<stop offset="1" style="stop-opacity:0; stop-color:white"/>
|
||||
</linearGradient>
|
||||
<rect style="fill:url(#id1)" x="3384.38" y="2221.46" width="2393.29" height="1082.77"/>
|
||||
</mask>
|
||||
<mask id="id2">
|
||||
<linearGradient id="id3" gradientUnits="userSpaceOnUse" x1="7914.83" y1="1369.16" x2="4815" y2="4077.55">
|
||||
<stop offset="0" style="stop-opacity:1; stop-color:white"/>
|
||||
<stop offset="1" style="stop-opacity:0; stop-color:white"/>
|
||||
</linearGradient>
|
||||
<rect style="fill:url(#id3)" x="3375.58" y="2036.2" width="3347.61" height="1690.14"/>
|
||||
</mask>
|
||||
<mask id="id4">
|
||||
<linearGradient id="id5" gradientUnits="userSpaceOnUse" x1="2077.44" y1="6592.85" x2="5177.27" y2="3884.46">
|
||||
<stop offset="0" style="stop-opacity:1; stop-color:white"/>
|
||||
<stop offset="1" style="stop-opacity:0; stop-color:white"/>
|
||||
</linearGradient>
|
||||
<rect style="fill:url(#id5)" x="3269.08" y="4235.67" width="3347.61" height="1690.14"/>
|
||||
</mask>
|
||||
<radialGradient id="id6" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.31579 -0 -0 1.31579 -1756 -1317)" cx="5560.36" cy="4170.27" r="1072.44" fx="5560.36" fy="4170.27">
|
||||
<stop offset="0" style="stop-opacity:1; stop-color:#1167B4"/>
|
||||
<stop offset="1" style="stop-opacity:1; stop-color:#121445"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="id7" gradientUnits="userSpaceOnUse" x1="4213.8" y1="3203.52" x2="3637.47" y2="3895.11">
|
||||
<stop offset="0" style="stop-opacity:1; stop-color:#34262A"/>
|
||||
<stop offset="1" style="stop-opacity:1; stop-color:#34262A"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="id8" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1.25168 -1.30198 1.30198 -1.25168 7296 20245)" cx="6324.41" cy="5333.93" r="1255.85" fx="6324.41" fy="5333.93">
|
||||
<stop offset="0" style="stop-opacity:1; stop-color:#DAEFFB"/>
|
||||
<stop offset="0.458824" style="stop-opacity:1; stop-color:#7DA7D2"/>
|
||||
<stop offset="1" style="stop-opacity:1; stop-color:#215FAA"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="id9" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.25168 1.30198 -1.30198 1.25168 3446 -9496)" xlink:href="#id8" cx="6537.38" cy="3910.51" r="1255.85" fx="6537.38" fy="3910.51">
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g id="Слой_x0020_1">
|
||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||
<g id="_2169686765088">
|
||||
<g>
|
||||
<path class="fil0" d="M6316.91 5144.71c-37.7,-45.26 103.2,-202.59 164.65,-302.47 61.46,-99.87 141.84,-144.16 179.56,-98.9 37.7,45.26 18.44,162.93 -43.01,262.79 -61.45,99.87 -263.5,183.84 -301.2,138.58z"/>
|
||||
<ellipse class="fil0" transform="matrix(0.783933 0.940964 -0.788558 1.28161 3442.43 3037.82)" rx="87.09" ry="141.11"/>
|
||||
<path class="fil1" style="mask:url(#id0)" d="M4186.29 2241.74c-331.43,161.85 -608.75,417.14 -798.1,731.42 327,-253.05 741.2,33.71 890.21,314.29 199.69,-111.78 428.56,-137.1 664.5,-43.38 926.58,368.05 1539.9,-1194.63 -756.61,-1002.33z"/>
|
||||
<circle class="fil2" cx="4992.79" cy="3957.43" r="1773.65"/>
|
||||
<path class="fil3" d="M5284.31 3762.35c829.07,1128.43 1663.76,840.31 1609.21,-21.7 -100.37,-957.86 -910.36,-1704.45 -1894.78,-1704.45 -701.76,0 -1314.9,379.4 -1645.51,944.26 -22.81,57.92 -42.78,94.04 -44.05,160.5 415.44,-604.44 1220.9,-298.19 1975.13,621.39z"/>
|
||||
<path class="fil4" style="mask:url(#id2)" d="M5359.46 2774.34c559.61,611.23 822.08,1023.4 1135.2,941.68 243.48,-63.57 334.04,-413.21 62.3,-871.05 -344.86,-489.19 -914.19,-808.77 -1558.22,-808.77 -686.27,0 -1287.74,362.85 -1623.16,907.14 508.33,-671.6 1426.21,-778.15 1983.88,-169z"/>
|
||||
<path class="fil5" d="M4707.96 4199.66c-829.06,-1128.43 -1663.76,-840.31 -1609.21,21.71 100.37,957.85 910.36,1704.44 1894.78,1704.44 701.76,0 1314.9,-379.4 1645.51,-944.26 22.81,-57.92 42.78,-94.03 44.05,-160.5 -415.44,604.44 -1220.89,298.19 -1975.13,-621.39z"/>
|
||||
<path class="fil4" style="mask:url(#id4)" d="M4632.81 5187.67c-559.61,-611.23 -822.08,-1023.4 -1135.2,-941.68 -243.48,63.57 -334.04,413.21 -62.3,871.05 344.86,489.19 914.19,808.77 1558.22,808.77 686.27,0 1287.74,-362.85 1623.16,-907.13 -508.33,671.6 -1426.21,778.14 -1983.88,168.99z"/>
|
||||
</g>
|
||||
<g transform="matrix(1.22551 0 0 1 -5480.09 2301.48)">
|
||||
<text x="5000" y="5000" class="fil6 fnt0">SMARTSOLTECH</text>
|
||||
</g>
|
||||
<g transform="matrix(1.11844 0 0 1 -4910.74 2960.81)">
|
||||
<text x="5000" y="5000" class="fil7 fnt1">FUTURE BEGINS HERE</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 24 KiB |
@@ -3,12 +3,12 @@
|
||||
"name": "Smartsoltech",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/img/photo_2024-10-06_10-06-08.jpg",
|
||||
"src": "/static/img/logo.jpg",
|
||||
"type": "image/jpeg",
|
||||
"sizes": "1011x702"
|
||||
},
|
||||
{
|
||||
"src": "/static/img/photo_2024-10-06_10-06-08.jpg",
|
||||
"src": "/static/img/logo.jpg",
|
||||
"type": "image/jpeg",
|
||||
"sizes": "1011x702"
|
||||
}
|
||||
|
||||
0
smartsoltech/static/qr_codes/.gitkeep
Normal file
BIN
smartsoltech/static/qr_codes/request_10.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |