Bot functionsa mainly operational
This commit is contained in:
@@ -1,12 +1,23 @@
|
||||
from django.contrib import admin
|
||||
from .models import Hotel, UserHotel
|
||||
from .models import Hotel, UserHotel, APIConfiguration, APIRequestLog, PMSConfiguration, PMSIntegrationLog
|
||||
from django import forms
|
||||
class HotelForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Hotel
|
||||
fields = "__all__"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Исключаем API, которые уже связаны с другими отелями
|
||||
used_apis = Hotel.objects.exclude(api__isnull=True).values_list('api', flat=True)
|
||||
self.fields['api'].queryset = APIConfiguration.objects.exclude(id__in=used_apis)
|
||||
|
||||
@admin.register(Hotel)
|
||||
class HotelAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'pms_type', 'created_at')
|
||||
form = HotelForm
|
||||
list_display = ('name', 'api', 'created_at', 'pms')
|
||||
search_fields = ('name',)
|
||||
list_filter = ('pms_type',)
|
||||
ordering = ('-created_at',)
|
||||
|
||||
admin.site.register(Hotel, HotelAdmin)
|
||||
|
||||
@admin.register(UserHotel)
|
||||
class UserHotelAdmin(admin.ModelAdmin):
|
||||
@@ -14,4 +25,32 @@ class UserHotelAdmin(admin.ModelAdmin):
|
||||
search_fields = ('user', 'hotel')
|
||||
list_filter = ('hotel',)
|
||||
ordering = ('-hotel',)
|
||||
|
||||
|
||||
|
||||
@admin.register(APIConfiguration)
|
||||
class ApiConfigurationAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'url', 'token', 'username', 'password')
|
||||
search_fields = ('name', 'url', 'token', 'username', 'password')
|
||||
list_filter = ('name', 'url', 'token', 'username', 'password')
|
||||
ordering = ('-name',)
|
||||
|
||||
@admin.register(APIRequestLog)
|
||||
class ApiRequestLogAdmin(admin.ModelAdmin):
|
||||
list_display = ('api', 'request_time', 'response_status', 'response_data')
|
||||
search_fields = ('api', 'request_time', 'response_status', 'response_data')
|
||||
list_filter = ('api', 'request_time', 'response_status', 'response_data')
|
||||
ordering = ('-api',)
|
||||
|
||||
|
||||
@admin.register(PMSConfiguration)
|
||||
class PMSConfigurationAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'parser_settings', 'description')
|
||||
|
||||
@admin.register(PMSIntegrationLog)
|
||||
class PMSIntegreationLogAdmin(admin.ModelAdmin):
|
||||
list_display = ('hotel', 'checked_at', 'status', 'message')
|
||||
search_fields = ('hotel', 'checked_at', 'status', 'message')
|
||||
list_filter = ('hotel', 'checked_at', 'status', 'message')
|
||||
ordering = ('-checked_at',)
|
||||
|
||||
26
hotels/migrations/0004_datalog.py
Normal file
26
hotels/migrations/0004_datalog.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-06 13:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0003_alter_hotel_options_alter_userhotel_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DataLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Название')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('data', models.JSONField(verbose_name='Данные')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Лог данных',
|
||||
'verbose_name_plural': 'Логи данных',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,47 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-06 13:51
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0004_datalog'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='APIConfiguration',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Название API')),
|
||||
('url', models.URLField(verbose_name='URL API')),
|
||||
('token', models.CharField(blank=True, max_length=255, null=True, verbose_name='Токен')),
|
||||
('username', models.CharField(blank=True, max_length=255, null=True, verbose_name='Логин')),
|
||||
('password', models.CharField(blank=True, max_length=255, null=True, verbose_name='Пароль')),
|
||||
('last_updated', models.DateTimeField(auto_now=True, verbose_name='Дата последнего обновления')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Конфигурация API',
|
||||
'verbose_name_plural': 'Конфигурации API',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='APIRequestLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('request_time', models.DateTimeField(auto_now_add=True, verbose_name='Время запроса')),
|
||||
('response_status', models.IntegerField(verbose_name='HTTP статус ответа')),
|
||||
('response_data', models.JSONField(blank=True, null=True, verbose_name='Данные ответа')),
|
||||
('api', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.apiconfiguration', verbose_name='API')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Журнал запросов API',
|
||||
'verbose_name_plural': 'Журналы запросов API',
|
||||
},
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='DataLog',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,45 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-06 14:09
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0005_apiconfiguration_apirequestlog_delete_datalog'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PMSConfiguration',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Название PMS')),
|
||||
('parser_settings', models.JSONField(default=dict, verbose_name='Настройки разбора данных')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'PMS система',
|
||||
'verbose_name_plural': 'PMS системы',
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='hotel',
|
||||
name='api_key',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='hotel',
|
||||
name='pms_type',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='hotel',
|
||||
name='public_key',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='hotel',
|
||||
name='api',
|
||||
field=models.OneToOneField(blank=True, help_text='API, связанный с этим отелем.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='hotels.apiconfiguration', verbose_name='API'),
|
||||
),
|
||||
]
|
||||
28
hotels/migrations/0007_pmsintegrationlog.py
Normal file
28
hotels/migrations/0007_pmsintegrationlog.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-06 23:02
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0006_pmsconfiguration_remove_hotel_api_key_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PMSIntegrationLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('checked_at', models.DateTimeField(auto_now_add=True, verbose_name='Время проверки')),
|
||||
('status', models.CharField(choices=[('success', 'Успех'), ('error', 'Ошибка')], max_length=50, verbose_name='Статус')),
|
||||
('message', models.TextField(blank=True, null=True, verbose_name='Сообщение')),
|
||||
('hotel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Журнал интеграции PMS',
|
||||
'verbose_name_plural': 'Журналы интеграции PMS',
|
||||
},
|
||||
),
|
||||
]
|
||||
19
hotels/migrations/0008_hotel_pms.py
Normal file
19
hotels/migrations/0008_hotel_pms.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-06 23:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0007_pmsintegrationlog'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='hotel',
|
||||
name='pms',
|
||||
field=models.OneToOneField(blank=True, help_text='PMS система? используемая в заведении', null=True, on_delete=django.db.models.deletion.SET_NULL, to='hotels.pmsconfiguration', verbose_name='PMS система'),
|
||||
),
|
||||
]
|
||||
19
hotels/migrations/0009_alter_hotel_pms.py
Normal file
19
hotels/migrations/0009_alter_hotel_pms.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-06 23:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0008_hotel_pms'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='hotel',
|
||||
name='pms',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hotels.pmsconfiguration', verbose_name='PMS система'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-07 00:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0009_alter_hotel_pms'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='apirequestlog',
|
||||
index=models.Index(fields=['api'], name='hotels_apir_api_id_686bb0_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='apirequestlog',
|
||||
index=models.Index(fields=['request_time'], name='hotels_apir_request_f65147_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='pmsintegrationlog',
|
||||
index=models.Index(fields=['hotel'], name='hotels_pmsi_hotel_i_718b32_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='pmsintegrationlog',
|
||||
index=models.Index(fields=['checked_at'], name='hotels_pmsi_checked_e7768d_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='pmsintegrationlog',
|
||||
index=models.Index(fields=['status'], name='hotels_pmsi_status_dbf1d8_idx'),
|
||||
),
|
||||
]
|
||||
@@ -1,25 +1,72 @@
|
||||
from django.db import models
|
||||
from users.models import User
|
||||
|
||||
class Hotel(models.Model):
|
||||
name = models.CharField(max_length=255, verbose_name="Название отеля")
|
||||
pms_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=[('bnovo', 'Bnovo'), ('travelline', 'Travel Line')],
|
||||
verbose_name="PMS система"
|
||||
)
|
||||
api_key = models.CharField(max_length=255, blank=True, null=True, verbose_name="API ключ")
|
||||
public_key = models.CharField(max_length=255, blank=True, null=True, verbose_name="Публичный ключ")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан")
|
||||
|
||||
class PMSConfiguration(models.Model):
|
||||
name = models.CharField(max_length=255, verbose_name="Название PMS")
|
||||
parser_settings = models.JSONField(default=dict, verbose_name="Настройки разбора данных")
|
||||
description = models.TextField(blank=True, null=True, verbose_name="Описание")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = "PMS система"
|
||||
verbose_name_plural = "PMS системы"
|
||||
|
||||
|
||||
class APIConfiguration(models.Model):
|
||||
name = models.CharField(max_length=255, verbose_name="Название API")
|
||||
url = models.URLField(verbose_name="URL API")
|
||||
token = models.CharField(max_length=255, blank=True, null=True, verbose_name="Токен")
|
||||
username = models.CharField(max_length=255, blank=True, null=True, verbose_name="Логин")
|
||||
password = models.CharField(max_length=255, blank=True, null=True, verbose_name="Пароль")
|
||||
last_updated = models.DateTimeField(auto_now=True, verbose_name="Дата последнего обновления")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Конфигурация API"
|
||||
verbose_name_plural = "Конфигурации API"
|
||||
|
||||
class Hotel(models.Model):
|
||||
name = models.CharField(max_length=255, verbose_name="Название отеля")
|
||||
api = models.OneToOneField(
|
||||
APIConfiguration,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="API",
|
||||
help_text="API, связанный с этим отелем."
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан")
|
||||
pms = models.ForeignKey(PMSConfiguration, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="PMS система")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Отель"
|
||||
verbose_name_plural = "Отели"
|
||||
|
||||
|
||||
class PMSIntegrationLog(models.Model):
|
||||
hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель")
|
||||
checked_at = models.DateTimeField(auto_now_add=True, verbose_name="Время проверки")
|
||||
status = models.CharField(max_length=50, verbose_name="Статус", choices=[('success', 'Успех'), ('error', 'Ошибка')])
|
||||
message = models.TextField(verbose_name="Сообщение", blank=True, null=True)
|
||||
def __str__(self):
|
||||
return f"{self.hotel.name} - {self.status} - {self.checked_at}"
|
||||
class Meta:
|
||||
verbose_name = "Журнал интеграции PMS"
|
||||
verbose_name_plural = "Журналы интеграции PMS"
|
||||
indexes = [
|
||||
models.Index(fields=['hotel']),
|
||||
models.Index(fields=['checked_at']),
|
||||
models.Index(fields=['status']),
|
||||
]
|
||||
|
||||
|
||||
class UserHotel(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Пользователь")
|
||||
@@ -31,4 +78,26 @@ class UserHotel(models.Model):
|
||||
class Meta:
|
||||
verbose_name = "Пользователь отеля"
|
||||
verbose_name_plural = "Пользователи отелей"
|
||||
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
|
||||
class APIRequestLog(models.Model):
|
||||
api = models.ForeignKey(APIConfiguration, on_delete=models.CASCADE, verbose_name="API")
|
||||
request_time = models.DateTimeField(auto_now_add=True, verbose_name="Время запроса")
|
||||
response_status = models.IntegerField(verbose_name="HTTP статус ответа")
|
||||
response_data = models.JSONField(verbose_name="Данные ответа", blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.api.name} - {self.request_time}"
|
||||
class Meta:
|
||||
verbose_name = "Журнал запросов API"
|
||||
verbose_name_plural = "Журналы запросов API"
|
||||
indexes = [
|
||||
models.Index(fields=['api']),
|
||||
models.Index(fields=['request_time']),
|
||||
]
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
92
hotels/pms_check.py
Normal file
92
hotels/pms_check.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
import requests
|
||||
from django.db import models
|
||||
from asgiref.sync import sync_to_async
|
||||
|
||||
|
||||
class APIDataLogger:
|
||||
"""Класс для работы с API, сохранения и обработки данных."""
|
||||
|
||||
def __init__(self, name, url, token=None, username=None, password=None):
|
||||
self.name = name
|
||||
self.url = url
|
||||
self.token = token
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.output_dir = "modules"
|
||||
|
||||
def ensure_directory_exists(self, path):
|
||||
"""Создать директорию, если она не существует."""
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
def fetch_data(self, additional_data=None):
|
||||
"""Получить данные из API."""
|
||||
headers = {"Content-Type": "application/json"}
|
||||
data = additional_data or {}
|
||||
|
||||
if self.token:
|
||||
data["token"] = self.token
|
||||
|
||||
response = requests.post(self.url, headers=headers, json=data, auth=(self.username, self.password) if self.username and self.password else None)
|
||||
if response.status_code != 200:
|
||||
print(f'{self.name}: API запрос не удался. Код статуса: {response.status_code}')
|
||||
return []
|
||||
|
||||
return response.json()
|
||||
|
||||
def save_data(self, data, suffix):
|
||||
"""Сохранить данные в файл JSON."""
|
||||
now = datetime.now()
|
||||
current_date = now.strftime('%Y-%m-%d')
|
||||
directory = os.path.join(self.output_dir, current_date, self.name)
|
||||
self.ensure_directory_exists(directory)
|
||||
|
||||
filename = f"{self.name} {suffix}.json"
|
||||
filepath = os.path.join(directory, filename)
|
||||
with open(filepath, 'w') as file:
|
||||
json.dump(data, file)
|
||||
return filepath
|
||||
|
||||
def load_previous_data(self, suffixes):
|
||||
"""Загрузить данные из файлов за текущий и предыдущий интервалы."""
|
||||
now = datetime.now()
|
||||
current_date = now.strftime('%Y-%m-%d')
|
||||
yesterday_date = (now - timedelta(days=1)).strftime('%Y-%m-%d')
|
||||
|
||||
directories = [(yesterday_date, "21"), (current_date, "9")] if 9 <= now.hour < 21 else [(current_date, "9"), (current_date, "21")]
|
||||
data_combined = []
|
||||
|
||||
for date, suffix in directories:
|
||||
filepath = os.path.join(self.output_dir, date, self.name, f"{self.name} {suffix}.json")
|
||||
try:
|
||||
with open(filepath, 'r') as file:
|
||||
data_combined.extend(json.load(file))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
return data_combined
|
||||
|
||||
def filter_data(self, data, filter_function):
|
||||
"""Фильтрация данных с использованием переданной функции."""
|
||||
return filter_function(data)
|
||||
|
||||
def process_and_save(self, additional_data=None, filter_function=None):
|
||||
"""Основной процесс: запрос, сохранение, чтение, фильтрация."""
|
||||
now = datetime.now()
|
||||
suffix = "9" if 9 <= now.hour < 21 else "21"
|
||||
|
||||
# Шаг 1: Получить данные
|
||||
raw_data = self.fetch_data(additional_data)
|
||||
self.save_data(raw_data, suffix)
|
||||
|
||||
# Шаг 2: Загрузить данные за текущий и предыдущий интервал
|
||||
combined_data = self.load_previous_data(["9", "21"])
|
||||
|
||||
# Шаг 3: Фильтрация
|
||||
if filter_function:
|
||||
combined_data = self.filter_data(combined_data, filter_function)
|
||||
|
||||
return combined_data
|
||||
53
hotels/pms_parse.py
Normal file
53
hotels/pms_parse.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
def parse_pms_data(data, parser_settings):
|
||||
date_format = parser_settings["date_format"]
|
||||
fields_mapping = parser_settings["fields_mapping"]
|
||||
conditions = parser_settings.get("conditions", {})
|
||||
|
||||
parsed_data = []
|
||||
|
||||
for record in data:
|
||||
# Применение условий фильтрации
|
||||
if conditions:
|
||||
for condition_field, expected_value in conditions.items():
|
||||
if record.get(condition_field) != expected_value:
|
||||
break
|
||||
else:
|
||||
# Условие выполнено
|
||||
pass
|
||||
else:
|
||||
# Условие отсутствует
|
||||
pass
|
||||
|
||||
# Разбор полей
|
||||
parsed_record = {}
|
||||
for internal_field, external_field in fields_mapping.items():
|
||||
if "." in external_field: # Например, "guests[0].lastName"
|
||||
keys = external_field.split(".")
|
||||
value = record
|
||||
try:
|
||||
for key in keys:
|
||||
if key.endswith("]"): # Обработка индексов, например, "guests[0]"
|
||||
key, index = key[:-1].split("[")
|
||||
value = value[key][int(index)]
|
||||
else:
|
||||
value = value[key]
|
||||
except (KeyError, IndexError, TypeError):
|
||||
value = None
|
||||
else:
|
||||
value = record.get(external_field)
|
||||
|
||||
# Преобразование дат
|
||||
if "date" in internal_field or "time" in internal_field:
|
||||
try:
|
||||
value = datetime.strptime(value, date_format)
|
||||
except (ValueError, TypeError):
|
||||
value = None
|
||||
|
||||
parsed_record[internal_field] = value
|
||||
|
||||
parsed_data.append(parsed_record)
|
||||
|
||||
return parsed_data
|
||||
Reference in New Issue
Block a user