init
This commit is contained in:
commit
0e3fd06c53
|
|
@ -0,0 +1,18 @@
|
||||||
|
*.egg-info
|
||||||
|
*.pot
|
||||||
|
*.py[co]
|
||||||
|
*.bat
|
||||||
|
*.pyproj
|
||||||
|
*.sln
|
||||||
|
__pycache__
|
||||||
|
.DS_Store
|
||||||
|
.idea/*
|
||||||
|
|
||||||
|
.env
|
||||||
|
docker-compose.override.yml
|
||||||
|
celerybeat-schedule
|
||||||
|
client/package-lock.json
|
||||||
|
|
||||||
|
volumes/*
|
||||||
|
client/build/*
|
||||||
|
client/node_modules/*
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
|
||||||
|
FROM python:3.12.2
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install -y \
|
||||||
|
vim \
|
||||||
|
supervisor
|
||||||
|
|
||||||
|
RUN pip install pip==24.0
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ADD requirements.txt ./
|
||||||
|
RUN pip install -r ./requirements.txt
|
||||||
|
|
||||||
|
COPY manage.py wsgi.py ./
|
||||||
|
|
||||||
|
COPY ./appa/ ./appa/
|
||||||
|
|
||||||
|
RUN python manage.py collectstatic --noinput
|
||||||
|
RUN chmod -R 777 /app/django_static
|
||||||
|
|
||||||
|
ADD ./client/build /client
|
||||||
|
|
||||||
|
ADD ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
|
||||||
|
ADD ./entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
CMD ["/entrypoint.sh"]
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = 'appa.apps.AppaConfig'
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.core.checks import messages
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.conf import settings
|
||||||
|
from ace_editor import AceJSONWidget
|
||||||
|
from rangefilter.filters import DateRangeFilterBuilder
|
||||||
|
|
||||||
|
from .models import *
|
||||||
|
from .admin_forms import *
|
||||||
|
from .admin_filters import *
|
||||||
|
from .tasks import send_call_request_task
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CallRequest)
|
||||||
|
class CallRequestAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
'date',
|
||||||
|
'get_status',
|
||||||
|
'get_request_status',
|
||||||
|
'patient_name',
|
||||||
|
'patient_phone',
|
||||||
|
'is_active',
|
||||||
|
)
|
||||||
|
list_filter = (
|
||||||
|
DateListFilter,
|
||||||
|
'status',
|
||||||
|
'request_status',
|
||||||
|
('date', DateRangeFilterBuilder()),
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
'patient_name',
|
||||||
|
'patient_phone',
|
||||||
|
)
|
||||||
|
form = forms.modelform_factory(CallRequest, fields='__all__', widgets={
|
||||||
|
'data': AceJSONWidget()
|
||||||
|
})
|
||||||
|
actions = ['send_call_request']
|
||||||
|
list_editable = ('is_active',)
|
||||||
|
readonly_fields = (
|
||||||
|
'call_id',
|
||||||
|
'call_data',
|
||||||
|
'request_time',
|
||||||
|
'response_status_code',
|
||||||
|
'response_message'
|
||||||
|
)
|
||||||
|
|
||||||
|
@admin.display(description='Статус приема', ordering='status')
|
||||||
|
def get_status(self, obj):
|
||||||
|
return mark_safe(f'''
|
||||||
|
<span style="
|
||||||
|
background-color: {obj.get_status_color()};
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
">{obj.get_status_display()}</span>
|
||||||
|
''')
|
||||||
|
|
||||||
|
@admin.display(description='Статус запроса', ordering='request_status')
|
||||||
|
def get_request_status(self, obj):
|
||||||
|
return mark_safe(f'''
|
||||||
|
<span style="
|
||||||
|
background-color: {obj.get_request_status_color()};
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
">{obj.get_request_status_display()}</span>
|
||||||
|
''')
|
||||||
|
|
||||||
|
@admin.action(description='Отправить выбранные записи на обзвон')
|
||||||
|
def send_call_request(self, request, queryset):
|
||||||
|
now = timezone.now()
|
||||||
|
if settings.CALL_REQUEST_TIME_START <= now.time() <= settings.CALL_REQUEST_TIME_END:
|
||||||
|
ids = [item.id for item in queryset]
|
||||||
|
send_call_request_task.apply_async(args=(ids,))
|
||||||
|
self.message_user(request, message=f'Записей отправлено на обзвон: {len(ids)}')
|
||||||
|
else:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
message=f'Время для обзвона регламентируется с '
|
||||||
|
f'{settings.CALL_REQUEST_TIME_START} по {settings.CALL_REQUEST_TIME_END}',
|
||||||
|
level=messages.ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(RequestLog)
|
||||||
|
class RequestLogAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('request_url', 'request_method', 'response_http_code', 'created')
|
||||||
|
search_fields = ('request_url', 'request_body', 'response_body', 'response_http_code')
|
||||||
|
date_hierarchy = 'created'
|
||||||
|
form = forms.modelform_factory(RequestLog, fields='__all__', widgets={
|
||||||
|
'request_body': AceJSONWidget(read_only=True),
|
||||||
|
'response_body': AceJSONWidget(read_only=True)
|
||||||
|
})
|
||||||
|
readonly_fields = [
|
||||||
|
'request_url',
|
||||||
|
'request_method',
|
||||||
|
'response_http_code',
|
||||||
|
'created'
|
||||||
|
]
|
||||||
|
fieldsets = [
|
||||||
|
('', {'fields': (
|
||||||
|
'request_url',
|
||||||
|
'request_method',
|
||||||
|
'response_http_code',
|
||||||
|
'created',
|
||||||
|
'request_body',
|
||||||
|
'response_body'
|
||||||
|
)})
|
||||||
|
]
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
return False
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.dateformat import format as django_dateformat
|
||||||
|
from dateutil import rrule
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'DateListFilter'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class DateListFilter(admin.SimpleListFilter):
|
||||||
|
title = 'Дата'
|
||||||
|
parameter_name = 'date'
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
today = datetime.datetime.today()
|
||||||
|
date_start = today - datetime.timedelta(days=3)
|
||||||
|
date_end = today + datetime.timedelta(days=2)
|
||||||
|
|
||||||
|
lookups = []
|
||||||
|
for date in rrule.rrule(rrule.DAILY, dtstart=date_start, until=date_end):
|
||||||
|
postfix = ''
|
||||||
|
if date.date() == today.date():
|
||||||
|
postfix = ' (сегодня)'
|
||||||
|
lookups.append([
|
||||||
|
date.strftime('%Y-%m-%d'), f"{django_dateformat(date, 'D d E')}{postfix}"
|
||||||
|
])
|
||||||
|
|
||||||
|
return lookups
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value():
|
||||||
|
return queryset.filter(date=self.value())
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from .models import *
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from constance import config
|
||||||
|
from typing import List
|
||||||
|
from requests.exceptions import ConnectTimeout, ConnectionError
|
||||||
|
from ninja import NinjaAPI
|
||||||
|
from django.http import Http404, HttpResponse
|
||||||
|
from django.db.models import ObjectDoesNotExist
|
||||||
|
from django.template import Context, Template
|
||||||
|
|
||||||
|
from appa.models import *
|
||||||
|
from appa.schemas import *
|
||||||
|
|
||||||
|
api = NinjaAPI()
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceUnavailableError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@api.exception_handler(ServiceUnavailableError)
|
||||||
|
@api.exception_handler(ConnectTimeout)
|
||||||
|
@api.exception_handler(ConnectionError)
|
||||||
|
def service_unavailable(request, exc):
|
||||||
|
return api.create_response(
|
||||||
|
request,
|
||||||
|
{
|
||||||
|
"error": "Сервис МТС недоступен"
|
||||||
|
},
|
||||||
|
status=200
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api.exception_handler(Http404)
|
||||||
|
@api.exception_handler(ObjectDoesNotExist)
|
||||||
|
def not_found(request, exc):
|
||||||
|
return api.create_response(
|
||||||
|
request,
|
||||||
|
{
|
||||||
|
"error": "Запись не найдена"
|
||||||
|
},
|
||||||
|
status=404
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
from django.apps import AppConfig as DjangoAppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AppaConfig(DjangoAppConfig):
|
||||||
|
|
||||||
|
name = 'appa'
|
||||||
|
verbose_name = 'МТС'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
import appa.receivers
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from celery import Celery
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'appa.settings')
|
||||||
|
|
||||||
|
app = Celery('appa')
|
||||||
|
app.config_from_object('django.conf:settings')
|
||||||
|
|
||||||
|
app.autodiscover_tasks()
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'task',
|
||||||
|
nargs='?',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'params',
|
||||||
|
nargs='*',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--async',
|
||||||
|
action='store_true',
|
||||||
|
dest='async',
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
task_path = options['task']
|
||||||
|
p, m = task_path.rsplit('.', 1)
|
||||||
|
try:
|
||||||
|
mod = importlib.import_module(p)
|
||||||
|
except ModuleNotFoundError as e:
|
||||||
|
raise CommandError(e)
|
||||||
|
|
||||||
|
if not hasattr(mod, m):
|
||||||
|
raise CommandError(f'Task {m} not found')
|
||||||
|
|
||||||
|
task = getattr(mod, m)
|
||||||
|
if options['async']:
|
||||||
|
task.apply_async(*options['params'])
|
||||||
|
else:
|
||||||
|
task(*options['params'])
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
content_types = ContentType.objects.filter(
|
||||||
|
Q(app_label='admin', model='logentry') |
|
||||||
|
Q(app_label='auth', model='permission') |
|
||||||
|
Q(app_label='sessions', model='session') |
|
||||||
|
Q(app_label='contenttypes', model='contenttype')
|
||||||
|
)
|
||||||
|
Permission.objects.filter(content_type__in=content_types).delete()
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.core import management
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
|
SUPERUSER_USERNAME = 'admin'
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
management.call_command('migrate', verbosity=1)
|
||||||
|
|
||||||
|
if not get_user_model().objects.filter(username=SUPERUSER_USERNAME).exists():
|
||||||
|
os.environ['DJANGO_SUPERUSER_PASSWORD'] = 'admin'
|
||||||
|
management.call_command('createsuperuser', '--username', SUPERUSER_USERNAME, '--email', 'admin@admin.local', '--noinput', verbosity=0)
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from appa.models import CallRequest
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
tomorrow = datetime.date.today() + datetime.timedelta(days=1)
|
||||||
|
CallRequest.objects.all().update(date=tomorrow)
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class CallRequestQuerySet(models.QuerySet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CallRequestManager(models.Manager):
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Generated by Django 3.2 on 2024-09-05 19:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RequestLog',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('request_url', models.CharField(max_length=500, verbose_name='Адрес запроса')),
|
||||||
|
('request_body', models.TextField(blank=True, null=True, verbose_name='Запрос')),
|
||||||
|
('request_method', models.CharField(max_length=100, null=True, verbose_name='Тип запроса')),
|
||||||
|
('response_body', models.TextField(blank=True, null=True, verbose_name='Ответ')),
|
||||||
|
('response_http_code', models.CharField(max_length=4, null=True, verbose_name='HTTP статус ответа')),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Дата запроса')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Запрос',
|
||||||
|
'verbose_name_plural': 'История запросов',
|
||||||
|
'ordering': ('-created',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CallRequest',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('date', models.DateField(verbose_name='Дата')),
|
||||||
|
('patient_id', models.IntegerField(verbose_name='ID пациента')),
|
||||||
|
('patient_name', models.CharField(max_length=200, verbose_name='ФИО пациента')),
|
||||||
|
('patient_phone', models.CharField(max_length=100, verbose_name='Номер телефона')),
|
||||||
|
('status', models.CharField(choices=[('PENDING', 'Обзвон еще не состоялся'), ('APPROVED', 'Прием подтвержден'), ('CANCELED', 'Прием отменен'), ('WITHOUT_ANSWER', 'Не дозвонились'), ('SERVICE_UNAVAILABLE', 'Сервис недоступен')], default='PENDING', max_length=20, verbose_name='Статус')),
|
||||||
|
('data', models.JSONField(default=dict, verbose_name='Данные')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Запрос на звонок',
|
||||||
|
'verbose_name_plural': 'Запросы на звонки',
|
||||||
|
'unique_together': {('date', 'patient_id')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 3.2 on 2024-09-08 11:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('appa', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='callrequest',
|
||||||
|
name='is_active',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='Активность'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='callrequest',
|
||||||
|
name='data',
|
||||||
|
field=models.JSONField(blank=True, default=list, verbose_name='Данные'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Generated by Django 3.2 on 2024-11-03 11:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('appa', '0002_auto_20240908_1121'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='callrequest',
|
||||||
|
name='address',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Адрес'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='callrequest',
|
||||||
|
name='call_data',
|
||||||
|
field=models.JSONField(blank=True, default=dict, editable=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='callrequest',
|
||||||
|
name='call_id',
|
||||||
|
field=models.CharField(editable=False, max_length=100, null=True, verbose_name='ID вызова'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='callrequest',
|
||||||
|
name='doctor_name',
|
||||||
|
field=models.CharField(max_length=100, null=True, verbose_name='Врач'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='callrequest',
|
||||||
|
name='request_time',
|
||||||
|
field=models.DateTimeField(editable=False, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='callrequest',
|
||||||
|
name='response_message',
|
||||||
|
field=models.TextField(editable=False, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='callrequest',
|
||||||
|
name='response_status_code',
|
||||||
|
field=models.IntegerField(editable=False, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='callrequest',
|
||||||
|
name='service_name',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Услуга'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 3.2 on 2024-11-03 11:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('appa', '0003_auto_20241103_1105'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='callrequest',
|
||||||
|
name='request_status',
|
||||||
|
field=models.CharField(choices=[('PENDING', 'Запрос не отправлен'), ('APPROVED', 'Запрос отправлен'), ('ERROR', 'Ошибка'), ('SERVICE_UNAVAILABLE', 'Сервис недоступен')], default='PENDING', max_length=20, verbose_name='Статус запроса'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='callrequest',
|
||||||
|
name='call_id',
|
||||||
|
field=models.CharField(editable=False, max_length=100, null=True, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='callrequest',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('PENDING', 'Обзвон еще не состоялся'), ('APPROVED', 'Прием подтвержден'), ('CANCELED', 'Прием отменен'), ('WITHOUT_ANSWER', 'Не дозвонились')], default='PENDING', max_length=20, verbose_name='Статус приема'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
|
||||||
|
import json
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from .managers import CallRequestManager
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'CallRequest',
|
||||||
|
'RequestLog'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CallRequest(models.Model):
|
||||||
|
|
||||||
|
class Status(models.TextChoices):
|
||||||
|
PENDING = 'PENDING', 'Обзвон еще не состоялся'
|
||||||
|
APPROVED = 'APPROVED', 'Прием подтвержден'
|
||||||
|
CANCELED = 'CANCELED', 'Прием отменен'
|
||||||
|
WITHOUT_ANSWER = 'WITHOUT_ANSWER', 'Не дозвонились'
|
||||||
|
|
||||||
|
STATUS_COLOR = {
|
||||||
|
Status.PENDING: '#2D72D2',
|
||||||
|
Status.APPROVED: '#238551',
|
||||||
|
Status.CANCELED: '#CD4246',
|
||||||
|
Status.WITHOUT_ANSWER: '#866103',
|
||||||
|
}
|
||||||
|
|
||||||
|
class RequestStatus(models.TextChoices):
|
||||||
|
NOT_SENT = 'PENDING', 'Не отправлен'
|
||||||
|
SENT = 'APPROVED', 'Отправлен'
|
||||||
|
ERROR = 'ERROR', 'Ошибка'
|
||||||
|
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE', 'Сервис недоступен'
|
||||||
|
|
||||||
|
REQUEST_STATUS_COLOR = {
|
||||||
|
RequestStatus.NOT_SENT: '#2D72D2',
|
||||||
|
RequestStatus.SENT: '#238551',
|
||||||
|
RequestStatus.ERROR: '#CD4246',
|
||||||
|
RequestStatus.SERVICE_UNAVAILABLE: '#7961DB',
|
||||||
|
}
|
||||||
|
|
||||||
|
status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING,
|
||||||
|
verbose_name='Статус приема')
|
||||||
|
request_status = models.CharField(max_length=20, choices=RequestStatus.choices, default=RequestStatus.NOT_SENT,
|
||||||
|
verbose_name='Статус запроса')
|
||||||
|
date = models.DateField(verbose_name='Дата')
|
||||||
|
patient_id = models.IntegerField(verbose_name='ID пациента')
|
||||||
|
patient_name = models.CharField(max_length=200, verbose_name='ФИО пациента')
|
||||||
|
patient_phone = models.CharField(max_length=100, verbose_name='Номер телефона')
|
||||||
|
doctor_name = models.CharField(max_length=100, null=True, verbose_name='Врач')
|
||||||
|
service_name = models.CharField(max_length=100, null=True, blank=True, verbose_name='Услуга')
|
||||||
|
address = models.CharField(max_length=100, null=True, blank=True, verbose_name='Адрес')
|
||||||
|
data = models.JSONField(default=list, blank=True, verbose_name='Данные')
|
||||||
|
is_active = models.BooleanField(default=True, verbose_name='Активность')
|
||||||
|
|
||||||
|
call_id = models.CharField(max_length=100, null=True, editable=False, verbose_name='ID')
|
||||||
|
call_data = models.JSONField(default=dict, blank=True, editable=False)
|
||||||
|
request_time = models.DateTimeField(null=True, editable=False)
|
||||||
|
response_status_code = models.IntegerField(null=True, editable=False)
|
||||||
|
response_message = models.TextField(null=True, editable=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Запрос на звонок'
|
||||||
|
verbose_name_plural = 'Запросы на звонки'
|
||||||
|
unique_together = (
|
||||||
|
('date', 'patient_id'),
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = CallRequestManager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.date.strftime('%d.%m.%Y')} / {self.patient_name}"
|
||||||
|
|
||||||
|
def get_status_color(self):
|
||||||
|
return self.STATUS_COLOR.get(self.status)
|
||||||
|
|
||||||
|
def get_request_status_color(self):
|
||||||
|
return self.REQUEST_STATUS_COLOR.get(self.request_status)
|
||||||
|
|
||||||
|
def reset_request(self):
|
||||||
|
self.request_status = CallRequest.RequestStatus.NOT_SENT
|
||||||
|
self.request_time = None
|
||||||
|
self.response_status_code = None
|
||||||
|
self.response_message = None
|
||||||
|
self.save(update_fields=(
|
||||||
|
'request_status',
|
||||||
|
'request_time',
|
||||||
|
'response_status_code',
|
||||||
|
'response_message',
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
class RequestLog(models.Model):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Запрос'
|
||||||
|
verbose_name_plural = 'История запросов'
|
||||||
|
ordering = ('-created',)
|
||||||
|
|
||||||
|
request_url = models.CharField(max_length=500, verbose_name='Адрес запроса')
|
||||||
|
request_body = models.TextField(null=True, blank=True, verbose_name='Запрос')
|
||||||
|
request_method = models.CharField(max_length=100, null=True, verbose_name='Тип запроса')
|
||||||
|
|
||||||
|
response_body = models.TextField(null=True, blank=True, verbose_name='Ответ')
|
||||||
|
response_http_code = models.CharField(max_length=4, null=True, verbose_name='HTTP статус ответа')
|
||||||
|
|
||||||
|
created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True, verbose_name='Дата запроса')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.request_url
|
||||||
|
|
||||||
|
def response_hook(self, r, *args, **kwargs):
|
||||||
|
self.response_http_code = r.status_code
|
||||||
|
self.response_body = r.text
|
||||||
|
self.save()
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from django.utils import timezone
|
||||||
|
from constance import config
|
||||||
|
|
||||||
|
from .models import CallRequest, RequestLog
|
||||||
|
|
||||||
|
|
||||||
|
def add_call_request(call_request: CallRequest, logging=True):
|
||||||
|
call_request.reset_request()
|
||||||
|
|
||||||
|
data = dict(
|
||||||
|
number_b=call_request.patient_phone,
|
||||||
|
name=call_request.patient_name,
|
||||||
|
date=call_request.date.strftime('%Y-%m-%d'),
|
||||||
|
time=call_request.date.strftime('%H:%M'),
|
||||||
|
doctor=call_request.doctor_name,
|
||||||
|
address=call_request.address,
|
||||||
|
)
|
||||||
|
|
||||||
|
hooks = None
|
||||||
|
if logging:
|
||||||
|
log = RequestLog(
|
||||||
|
request_url=config.HOST,
|
||||||
|
request_body=json.dumps(data, ensure_ascii=False, indent=4),
|
||||||
|
)
|
||||||
|
hooks = {'response': log.response_hook}
|
||||||
|
|
||||||
|
r = requests.post(
|
||||||
|
config.HOST,
|
||||||
|
auth=(config.USERNAME, config.PASSWORD),
|
||||||
|
json=data,
|
||||||
|
hooks=hooks
|
||||||
|
)
|
||||||
|
call_request.response_status_code = r.status_code
|
||||||
|
call_request.request_time = timezone.now()
|
||||||
|
if r.status_code == 200:
|
||||||
|
call_request.call_id = r.json()['request_id']
|
||||||
|
call_request.request_status = CallRequest.RequestStatus.SENT
|
||||||
|
elif r.status_code == 400:
|
||||||
|
call_request.call_id = r.json()['request_id']
|
||||||
|
call_request.response_message = r.json()['error']
|
||||||
|
call_request.request_status = CallRequest.RequestStatus.ERROR
|
||||||
|
|
||||||
|
call_request.save(update_fields=(
|
||||||
|
'call_id',
|
||||||
|
'request_status',
|
||||||
|
'request_time',
|
||||||
|
'response_status_code',
|
||||||
|
'response_message',
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def call_history(date_from: datetime.date, date_to: datetime.date):
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.db.models.signals import pre_delete
|
||||||
|
|
||||||
|
from .models import *
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
from ninja import Schema, ModelSchema, Field
|
||||||
|
from appa.models import *
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
|
||||||
|
import os
|
||||||
|
import environ
|
||||||
|
import datetime
|
||||||
|
import django as django_module
|
||||||
|
|
||||||
|
from celery.schedules import crontab
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
DJANGO_DIR = os.path.dirname(django_module.__file__)
|
||||||
|
|
||||||
|
SECRET_KEY = 'django-insecure-13n6^zl86r@=9@!z8gsfv(q6wpggo+$box)^tklnoe9auck-)c'
|
||||||
|
|
||||||
|
root = environ.Path(__file__) - 2
|
||||||
|
|
||||||
|
env = environ.Env(
|
||||||
|
DEBUG=(bool, False),
|
||||||
|
LANGUAGE_CODE=(str, 'ru-RU'),
|
||||||
|
TIME_ZONE=(str, 'Asia/Vladivostok'),
|
||||||
|
USE_TZ=(bool, False),
|
||||||
|
USE_I18N=(bool, True),
|
||||||
|
CORS_ORIGIN_WHITELIST=(list, []),
|
||||||
|
AUTH_CONVERT_PASSWORD_TO_LOWER_CASE=(bool, False)
|
||||||
|
)
|
||||||
|
|
||||||
|
DEBUG = env('DEBUG', False)
|
||||||
|
|
||||||
|
REDIS_HOST = env.str('REDIS_HOST', 'medicine-stack-redis')
|
||||||
|
REDIS_PORT = env.str('REDIS_PORT', '6379')
|
||||||
|
REDIS_DB = env.str('REDIS_DB', 1)
|
||||||
|
|
||||||
|
POSTGRES_HOST = env.str('POSTGRES_HOST', 'medicine-stack-postgres')
|
||||||
|
POSTGRES_PORT = env.str('POSTGRES_PORT', '5432')
|
||||||
|
POSTGRES_DB = env.str('POSTGRES_DB', 'medicine_mts')
|
||||||
|
POSTGRES_USER = env.str('POSTGRES_USER', 'postgres')
|
||||||
|
POSTGRES_PASSWORD = env.str('POSTGRES_PASSWORD', 'password')
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'constance',
|
||||||
|
'ace_editor',
|
||||||
|
'rangefilter',
|
||||||
|
|
||||||
|
'appa',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'appa.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [
|
||||||
|
os.path.join(BASE_DIR, 'appa/templates'),
|
||||||
|
DJANGO_DIR
|
||||||
|
],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'appa.wsgi.application'
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
'HOST': POSTGRES_HOST,
|
||||||
|
'PORT': POSTGRES_PORT,
|
||||||
|
'NAME': POSTGRES_DB,
|
||||||
|
'USER': POSTGRES_USER,
|
||||||
|
'PASSWORD': POSTGRES_PASSWORD,
|
||||||
|
},
|
||||||
|
'medicine': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
'NAME': 'medicine',
|
||||||
|
'USER': POSTGRES_USER,
|
||||||
|
'PASSWORD': POSTGRES_PASSWORD,
|
||||||
|
'HOST': POSTGRES_HOST,
|
||||||
|
'PORT': POSTGRES_PORT
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
LANGUAGE_CODE = env.str('LANGUAGE_CODE')
|
||||||
|
|
||||||
|
TIME_ZONE = env.str('TIME_ZONE')
|
||||||
|
|
||||||
|
USE_I18N = env.bool('USE_I18N')
|
||||||
|
|
||||||
|
USE_TZ = env.bool('USE_TZ')
|
||||||
|
|
||||||
|
STATIC_URL = '/medicine-mts/django_static/'
|
||||||
|
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, 'django_static')
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
SESSION_COOKIE_NAME = env.str('SESSION_COOKIE_NAME', 'medicine-mts')
|
||||||
|
|
||||||
|
CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
|
||||||
|
|
||||||
|
CONSTANCE_ADDITIONAL_FIELDS = {
|
||||||
|
'char': ['django.forms.fields.CharField', {
|
||||||
|
'widget': 'django.forms.TextInput',
|
||||||
|
'widget_kwargs': dict(attrs={'size': 60}),
|
||||||
|
'required': False
|
||||||
|
}],
|
||||||
|
'url': ['django.forms.fields.URLField', {
|
||||||
|
'widget': 'django.forms.TextInput',
|
||||||
|
'widget_kwargs': dict(attrs={'size': 60}),
|
||||||
|
'required': False
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
|
||||||
|
CONSTANCE_CONFIG = {
|
||||||
|
'USERNAME': ('', 'Имя пользователя', 'char'),
|
||||||
|
'PASSWORD': ('', 'Пароль', 'char'),
|
||||||
|
'HOST': ('', 'Хост', 'char'),
|
||||||
|
'IS_ENABLED': (False, 'Активен', bool),
|
||||||
|
|
||||||
|
'MEDICINE_TOKEN': ('', 'Токен', 'char'),
|
||||||
|
'MEDICINE_HOST': ('http://medicine-app/', 'Хост', 'char'),
|
||||||
|
}
|
||||||
|
|
||||||
|
CONSTANCE_CONFIG_FIELDSETS = (
|
||||||
|
('МТС', (
|
||||||
|
'HOST',
|
||||||
|
'USERNAME',
|
||||||
|
'PASSWORD',
|
||||||
|
'IS_ENABLED',
|
||||||
|
)),
|
||||||
|
('МИС', (
|
||||||
|
'MEDICINE_HOST',
|
||||||
|
'MEDICINE_TOKEN',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
|
REQUEST_TIMEOUT = 5
|
||||||
|
|
||||||
|
REDIS_CELERY_DB = env.str('REDIS_CELERY_DB', '3')
|
||||||
|
BROKER_URL = f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_CELERY_DB}'
|
||||||
|
CELERY_DEFAULT_QUEUE = 'default'
|
||||||
|
CELERY_ENABLE_UTC = True
|
||||||
|
CELERY_IGNORE_RESULT = True
|
||||||
|
|
||||||
|
CELERYBEAT_SCHEDULE = {
|
||||||
|
}
|
||||||
|
|
||||||
|
DATE_FORMAT = 'd.m.Y'
|
||||||
|
DATETIME_FORMAT = 'd.m.Y H:i'
|
||||||
|
|
||||||
|
CALL_REQUEST_TIME_START = datetime.time(9, 0)
|
||||||
|
CALL_REQUEST_TIME_END = datetime.time(21, 0)
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
AUTH_PASSWORD_VALIDATORS = []
|
||||||
|
SESSION_COOKIE_NAME = 'dev-medicine-mts'
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import datetime
|
||||||
|
import requests
|
||||||
|
from constance import config
|
||||||
|
|
||||||
|
from appa.celery import app
|
||||||
|
|
||||||
|
from appa.models import *
|
||||||
|
from appa.mts_api import add_call_request
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(bind=True, acks_late=True)
|
||||||
|
def send_call_request_task(self, ids=None):
|
||||||
|
tomorrow = datetime.date.today() + datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
call_requests = CallRequest.objects.filter(
|
||||||
|
date=tomorrow,
|
||||||
|
status__in=[
|
||||||
|
CallRequest.Status.PENDING,
|
||||||
|
CallRequest.Status.WITHOUT_ANSWER
|
||||||
|
],
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
if ids and len(ids) > 0:
|
||||||
|
call_requests = call_requests.filter(id__in=ids)
|
||||||
|
|
||||||
|
for call_request in call_requests:
|
||||||
|
add_call_request(call_request)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(bind=True, acks_late=True)
|
||||||
|
def update_call_requests(self):
|
||||||
|
for add_days in [1, 2]:
|
||||||
|
booking_date = datetime.date.today() + datetime.timedelta(days=add_days)
|
||||||
|
CallRequest.objects.filter(date=booking_date).delete()
|
||||||
|
|
||||||
|
for page in range(1):
|
||||||
|
r = requests.get(
|
||||||
|
f'{config.MEDICINE_HOST}api/1/booking/',
|
||||||
|
params={
|
||||||
|
'date_start__date': booking_date.strftime('%d.%m.%Y'),
|
||||||
|
'patient__phone_mobile__isnull': False,
|
||||||
|
'patient__isnull': False,
|
||||||
|
'page': page,
|
||||||
|
'status': 1,
|
||||||
|
'limit': 50
|
||||||
|
},
|
||||||
|
headers={
|
||||||
|
'Authorization': f'Token {config.MEDICINE_TOKEN}'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if r.status_code == 200:
|
||||||
|
response = r.json()
|
||||||
|
results = response['results']
|
||||||
|
for booking in results:
|
||||||
|
call_request = CallRequest.objects.get_or_create(
|
||||||
|
defaults={
|
||||||
|
'patient_name': booking['patient']['full_name'],
|
||||||
|
'patient_phone': '7' + booking['patient']['phone_mobile'],
|
||||||
|
},
|
||||||
|
date=booking_date,
|
||||||
|
patient_id=booking['patient']['id'],
|
||||||
|
)[0]
|
||||||
|
call_request.data.append({
|
||||||
|
'booking_id': booking['id'],
|
||||||
|
'time_start': booking['time_start'],
|
||||||
|
'time_end': booking['time_end'],
|
||||||
|
'speciality': booking['user']['doctor']['speciality_display'],
|
||||||
|
})
|
||||||
|
call_request.save(update_fields=['data'])
|
||||||
|
|
||||||
|
if response['next'] is None:
|
||||||
|
break
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{% extends "admin/change_list.html" %}
|
||||||
|
|
||||||
|
{% block extrastyle %}
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.paginator input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
{% load i18n static %}
|
||||||
|
{% if result_hidden_fields %}
|
||||||
|
<div class="hiddenfields">{# DIV for HTML validation #}
|
||||||
|
{% for item in result_hidden_fields %}{{ item }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if results %}
|
||||||
|
<div class="results">
|
||||||
|
<table id="result_list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{% for header in result_headers %}
|
||||||
|
<th scope="col"{{ header.class_attrib }}>
|
||||||
|
{% if header.sortable %}
|
||||||
|
{% if header.sort_priority > 0 %}
|
||||||
|
<div class="sortoptions">
|
||||||
|
<a class="sortremove" href="{{ header.url_remove }}" title="{% translate "Remove from sorting" %}"></a>
|
||||||
|
{% if num_sorted_fields > 1 %}<span class="sortpriority" title="{% blocktranslate with priority_number=header.sort_priority %}Sorting priority: {{ priority_number }}{% endblocktranslate %}">{{ header.sort_priority }}</span>{% endif %}
|
||||||
|
<a href="{{ header.url_toggle }}" class="toggle {% if header.ascending %}ascending{% else %}descending{% endif %}" title="{% translate "Toggle sorting" %}"></a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<div class="text">{% if header.sortable %}<a href="{{ header.url_primary }}">{{ header.text|capfirst }}</a>{% else %}<span>{{ header.text|capfirst }}</span>{% endif %}</div>
|
||||||
|
<div class="clear"></div>
|
||||||
|
</th>{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for result in results %}
|
||||||
|
{% if result.form and result.form.non_field_errors %}
|
||||||
|
<tr><td colspan="{{ result|length }}">{{ result.form.non_field_errors }}</td></tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr style="background-color: {{ result.form.instance.get_status_color }}30">{% for item in result %}{{ item }}{% endfor %}</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }} | МТС{% endblock %}
|
||||||
|
|
||||||
|
{% block branding %}
|
||||||
|
<h1 id="site-name">
|
||||||
|
<a href="{% url 'admin:index' %}">МТС</a>
|
||||||
|
</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends "admin/constance/change_list.html" %}
|
||||||
|
|
||||||
|
{% block extrastyle %}
|
||||||
|
{{ block.super }}
|
||||||
|
<style>
|
||||||
|
.constance table thead th:nth-child(1) {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
.constance table thead th:nth-child(2) {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
.constance table thead th:nth-child(4) {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
.constance table tbody a {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def add_success_view(request):
|
||||||
|
return JsonResponse({
|
||||||
|
'request_id': get_random_string(10),
|
||||||
|
'success': '1'
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def add_error_view(request):
|
||||||
|
return JsonResponse({
|
||||||
|
'request_id': get_random_string(10),
|
||||||
|
'success': '0',
|
||||||
|
'error': "'number_b' should be in e164 format"
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def add_500_error_view(request):
|
||||||
|
return JsonResponse({
|
||||||
|
'status': 500,
|
||||||
|
'status_message': 'Internal Server Error'
|
||||||
|
}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def history_view(request):
|
||||||
|
return JsonResponse({
|
||||||
|
{
|
||||||
|
"page": 1,
|
||||||
|
"total": 51046,
|
||||||
|
"offset": 0,
|
||||||
|
"limit": 50,
|
||||||
|
"prev": "",
|
||||||
|
"next": "https://***/history?date_from=2023-10-12+09%3A00%3A00&date_to=2023-10-12+18%3A05%3A00&limit=50&offset=50",
|
||||||
|
"talks": [
|
||||||
|
{
|
||||||
|
"call_id": "000364fa82e81b0e",
|
||||||
|
"duration": "43",
|
||||||
|
"number_a": "74993331122",
|
||||||
|
"number_b": "79155552211",
|
||||||
|
"s_date": "2023-10-1209:00:25",
|
||||||
|
"side": "B",
|
||||||
|
"link_recording": "https://***/000123fa82e81b0e?date_from=2023-10-12T08:59:25Z&date_to=2023-10-12T09:02:25Z",
|
||||||
|
"status": "отменитьзаказ",
|
||||||
|
"transcript": "[Робот,hello_main]текстробота\n[Абонент,Hello]текстабонента\n..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"call_id": "002114fa55461eb6",
|
||||||
|
"duration": "32",
|
||||||
|
"number_a": "74995443052",
|
||||||
|
"phone": "79206584411",
|
||||||
|
"s_date": "2023-10-2219:59:06",
|
||||||
|
"side": "A",
|
||||||
|
"link_recording": "https://***/000564fa82e81b0e?date_from=2023-10-12T19:58:06Z&date_to=2023-10-12T20:02:06Z",
|
||||||
|
"status": "оформитьзаказ",
|
||||||
|
"transcript": "[Робот,hello_main]текстробота\n[Абонент,Hello]текстабонента\n..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from django.urls import path, include
|
||||||
|
from django.conf import settings
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from appa.api import api
|
||||||
|
from appa import test_views
|
||||||
|
|
||||||
|
admin.site.unregister(Group)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('medicine-mts/admin/', admin.site.urls),
|
||||||
|
path('medicine-mts/api/', api.urls),
|
||||||
|
path('medicine-mts/', lambda request: redirect('/medicine-mts/admin/')),
|
||||||
|
path('admin/', lambda request: redirect('/medicine-mts/admin/')),
|
||||||
|
path('', lambda request: redirect('/medicine-mts/admin/')),
|
||||||
|
]
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
from django.contrib.staticfiles.urls import staticfiles_urlpatterns, static
|
||||||
|
urlpatterns +=[
|
||||||
|
path('medicine-mts/tests/add_success_view/', test_views.add_success_view),
|
||||||
|
path('medicine-mts/tests/add_error_view/', test_views.add_error_view),
|
||||||
|
path('medicine-mts/tests/add_500_error_view/', test_views.add_500_error_view),
|
||||||
|
path('medicine-mts/tests/history_view/', test_views.history_view),
|
||||||
|
]
|
||||||
|
urlpatterns += staticfiles_urlpatterns()
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for app project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'appa.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'appa.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
(cd client && npm run build)
|
||||||
|
docker build --tag docker.med-logic.ru/medicine-damask:latest .
|
||||||
|
docker push docker.med-logic.ru/medicine-damask:latest
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
|
||||||
|
version: '3.4'
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
local-server:
|
||||||
|
restart: "no"
|
||||||
|
container_name: "medicine-mts"
|
||||||
|
tty: True
|
||||||
|
image: python-local-3.12
|
||||||
|
environment:
|
||||||
|
DEBUG: 'True'
|
||||||
|
|
||||||
|
POSTGRES_HOST: docker.for.mac.localhost
|
||||||
|
POSTGRES_PORT: 54322
|
||||||
|
POSTGRES_USER: user
|
||||||
|
POSTGRES_PASSWORD: password
|
||||||
|
|
||||||
|
REDIS_HOST: docker.for.mac.localhost
|
||||||
|
REDIS_PORT: 63790
|
||||||
|
REDIS_DB: 1
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- ./volumes/venv:/venv
|
||||||
|
networks:
|
||||||
|
- shared-network
|
||||||
|
ports:
|
||||||
|
- "9800:80"
|
||||||
|
|
||||||
|
prod:
|
||||||
|
restart: "no"
|
||||||
|
container_name: "medicine-mts-prod"
|
||||||
|
tty: True
|
||||||
|
image: docker.med-logic.ru/medicine-mts:latest
|
||||||
|
environment:
|
||||||
|
DEBUG: 'False'
|
||||||
|
|
||||||
|
POSTGRES_HOST: docker.for.mac.localhost
|
||||||
|
POSTGRES_PORT: 54322
|
||||||
|
POSTGRES_USER: user
|
||||||
|
POSTGRES_PASSWORD: password
|
||||||
|
|
||||||
|
REDIS_HOST: docker.for.mac.localhost
|
||||||
|
REDIS_PORT: 63790
|
||||||
|
REDIS_DB: 1
|
||||||
|
networks:
|
||||||
|
- shared-network
|
||||||
|
ports:
|
||||||
|
- "9900:80"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
shared-network:
|
||||||
|
external: true
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
python manage.py migrate
|
||||||
|
|
||||||
|
/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
COMMAND="$1"
|
||||||
|
ARGUMENT="$2"
|
||||||
|
|
||||||
|
up() {
|
||||||
|
docker-compose up -d local-server
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
docker-compose stop local-server
|
||||||
|
}
|
||||||
|
|
||||||
|
enter() {
|
||||||
|
up
|
||||||
|
docker-compose exec local-server bash
|
||||||
|
}
|
||||||
|
|
||||||
|
python_env() {
|
||||||
|
export PYTHONPATH="$PYTHONPATH:/app/packages"
|
||||||
|
source /venv/bin/activate
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
s() {
|
||||||
|
python_env
|
||||||
|
echo ""
|
||||||
|
echo "http://0.0.0.0:9800"
|
||||||
|
echo ""
|
||||||
|
uwsgi --http 0.0.0.0:80 \
|
||||||
|
--wsgi-file wsgi.py \
|
||||||
|
--processes 2 \
|
||||||
|
--py-autoreload 1 \
|
||||||
|
--http-timeout 360 \
|
||||||
|
--buffer-size 32768 \
|
||||||
|
--need-app \
|
||||||
|
--disable-logging
|
||||||
|
--
|
||||||
|
}
|
||||||
|
|
||||||
|
makemigrations() {
|
||||||
|
python_env
|
||||||
|
python manage.py makemigrations
|
||||||
|
}
|
||||||
|
|
||||||
|
empty_migration() {
|
||||||
|
python_env
|
||||||
|
python manage.py makemigrations ${ARGUMENT} --empty
|
||||||
|
}
|
||||||
|
|
||||||
|
manage() {
|
||||||
|
python_env
|
||||||
|
python manage.py ${ARGUMENT}
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate() {
|
||||||
|
python_env
|
||||||
|
python manage.py migrate ${ARGUMENT}
|
||||||
|
}
|
||||||
|
|
||||||
|
mm() {
|
||||||
|
python_env
|
||||||
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate
|
||||||
|
}
|
||||||
|
|
||||||
|
runcelery() {
|
||||||
|
python_env
|
||||||
|
celery -A appa worker -B --loglevel=INFO
|
||||||
|
}
|
||||||
|
|
||||||
|
dumpdata() {
|
||||||
|
python_env
|
||||||
|
python manage.py dumpdata ${ARGUMENT} --indent=4 > tms/fixtures/${ARGUMENT}.json
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
up
|
||||||
|
enter
|
||||||
|
}
|
||||||
|
|
||||||
|
pipi() {
|
||||||
|
python_env
|
||||||
|
pip install ${ARGUMENT}
|
||||||
|
pip freeze
|
||||||
|
}
|
||||||
|
|
||||||
|
run_react() {
|
||||||
|
docker-compose up -d local-react
|
||||||
|
docker-compose exec local-react bash
|
||||||
|
}
|
||||||
|
|
||||||
|
build_image() {
|
||||||
|
docker build --tag docker.med-logic.ru/cmt:latest .
|
||||||
|
docker push docker.med-logic.ru/cmt:latest
|
||||||
|
}
|
||||||
|
|
||||||
|
damask() {
|
||||||
|
ssh -4 -g -N -o ProxyCommand="ssh -W %h:%p root@46.229.212.181" -L 50000:192.168.11.101:80 rick@localhost -p 40050
|
||||||
|
}
|
||||||
|
|
||||||
|
${COMMAND}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'appa.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
ace-editor @ git+https://github.com/ilya-muhortov/django-ace-editor@v0.1
|
||||||
|
amqp==5.2.0
|
||||||
|
annotated-types==0.7.0
|
||||||
|
asgiref==3.8.1
|
||||||
|
billiard==4.2.0
|
||||||
|
celery==5.4.0
|
||||||
|
certifi==2024.8.30
|
||||||
|
charset-normalizer==3.3.2
|
||||||
|
click==8.1.7
|
||||||
|
click-didyoumean==0.3.1
|
||||||
|
click-plugins==1.1.1
|
||||||
|
click-repl==0.3.0
|
||||||
|
Django==3.2
|
||||||
|
django-constance==3.1.0
|
||||||
|
django-environ==0.11.2
|
||||||
|
django-ninja==1.2.0
|
||||||
|
django-picklefield==3.2
|
||||||
|
idna==3.8
|
||||||
|
kombu==5.4.0
|
||||||
|
prompt_toolkit==3.0.47
|
||||||
|
psycopg2-binary==2.9.9
|
||||||
|
pydantic==2.8.2
|
||||||
|
pydantic_core==2.20.1
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
pytz==2024.1
|
||||||
|
redis==5.0.7
|
||||||
|
requests==2.32.3
|
||||||
|
setuptools==70.2.0
|
||||||
|
six==1.16.0
|
||||||
|
sqlparse==0.5.1
|
||||||
|
typing_extensions==4.12.2
|
||||||
|
tzdata==2024.1
|
||||||
|
urllib3==2.2.2
|
||||||
|
uWSGI==2.0.26
|
||||||
|
vine==5.1.0
|
||||||
|
wcwidth==0.2.13
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
|
||||||
|
[program:app]
|
||||||
|
command=uwsgi
|
||||||
|
--http 0.0.0.0:80
|
||||||
|
--http-timeout 15
|
||||||
|
--buffer-size 32768
|
||||||
|
--processes 2
|
||||||
|
--threads 2
|
||||||
|
--chdir /app/
|
||||||
|
--wsgi-file wsgi.py
|
||||||
|
--harakiri 30
|
||||||
|
--evil-reload-on-as 500
|
||||||
|
--master
|
||||||
|
--log-5xx
|
||||||
|
--vacuum
|
||||||
|
--need-app
|
||||||
|
--disable-write-exception
|
||||||
|
--static-map /medicine-mts/django_static=/app/django_static
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes = 0
|
||||||
|
stderr_maxbytes=0
|
||||||
|
|
||||||
|
[program:worker]
|
||||||
|
directory=/app
|
||||||
|
command=celery -A appa worker -Q default -n worker@appa --soft-time-limit=3600 --concurrency=1 --loglevel=INFO
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes = 0
|
||||||
|
stderr_maxbytes=0
|
||||||
|
|
||||||
|
[program:scheduler]
|
||||||
|
directory=/app
|
||||||
|
command=celery -A appa beat --loglevel=INFO
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes = 0
|
||||||
|
stderr_maxbytes=0
|
||||||
Loading…
Reference in New Issue