This commit is contained in:
Ilya Mukhortov 2024-11-03 11:35:56 +10:00
commit 0e3fd06c53
42 changed files with 1444 additions and 0 deletions

18
.gitignore vendored Normal file
View File

@ -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/*

30
Dockerfile Normal file
View File

@ -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"]

1
appa/__init__.py Normal file
View File

@ -0,0 +1 @@
default_app_config = 'appa.apps.AppaConfig'

115
appa/admin.py Normal file
View File

@ -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

37
appa/admin_filters.py Normal file
View File

@ -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())

7
appa/admin_forms.py Normal file
View File

@ -0,0 +1,7 @@
from django import forms
from .models import *
__all__ = [
]

45
appa/api.py Normal file
View File

@ -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
)

11
appa/apps.py Normal file
View File

@ -0,0 +1,11 @@
from django.apps import AppConfig as DjangoAppConfig
class AppaConfig(DjangoAppConfig):
name = 'appa'
verbose_name = 'МТС'
def ready(self):
import appa.receivers

11
appa/celery.py Normal file
View File

@ -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()

View File

@ -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'])

View File

@ -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()

View File

@ -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)

View File

@ -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)

10
appa/managers.py Normal file
View File

@ -0,0 +1,10 @@
from django.db import models
class CallRequestQuerySet(models.QuerySet):
pass
class CallRequestManager(models.Manager):
pass

View File

@ -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')},
},
),
]

View File

@ -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='Данные'),
),
]

View File

@ -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='Услуга'),
),
]

View File

@ -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='Статус приема'),
),
]

View File

114
appa/models.py Normal file
View File

@ -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()

58
appa/mts_api.py Normal file
View File

@ -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

5
appa/receivers.py Normal file
View File

@ -0,0 +1,5 @@
from django.dispatch import receiver
from django.db.models.signals import pre_delete
from .models import *

4
appa/schemas.py Normal file
View File

@ -0,0 +1,4 @@
from typing import List, Optional
from ninja import Schema, ModelSchema, Field
from appa.models import *

194
appa/settings.py Normal file
View File

@ -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'

72
appa/tasks.py Normal file
View File

@ -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

View File

@ -0,0 +1,11 @@
{% extends "admin/change_list.html" %}
{% block extrastyle %}
{{ block.super }}
<style>
.paginator input {
display: none;
}
</style>
{% endblock %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

67
appa/test_views.py Normal file
View File

@ -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..."
}
]
}
})

28
appa/urls.py Normal file
View File

@ -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
appa/utils.py Normal file
View File

16
appa/wsgi.py Normal file
View File

@ -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()

8
asgi.py Normal file
View File

@ -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()

7
build.sh Executable file
View File

@ -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

53
docker-compose.yml Normal file
View File

@ -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

6
entrypoint.sh Normal file
View File

@ -0,0 +1,6 @@
#!/bin/sh
set -e
python manage.py migrate
/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf

102
l Executable file
View File

@ -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}

22
manage.py Executable file
View File

@ -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()

36
requirements.txt Normal file
View File

@ -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

43
supervisord.conf Normal file
View File

@ -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

8
wsgi.py Normal file
View File

@ -0,0 +1,8 @@
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'appa.settings')
application = get_wsgi_application()