diff --git a/.gitignore b/.gitignore index 6bec0bd..8a466f4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ wheels/ .venv .hintrc .vscode +data.json +.env django-leaflet admin-interface diff --git a/dbapp/.dockerignore b/dbapp/.dockerignore new file mode 100644 index 0000000..9142acd --- /dev/null +++ b/dbapp/.dockerignore @@ -0,0 +1,29 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +.pytest_cache +.coverage +.git +.gitignore +README.md +.env +.DS_Store +.settings +.vscode +.idea +*.swp +*.swo +*~ +__pycache__/ +*.so +.Python +.coverage +.pytest_cache +.venv +venv/ +env/ +.pyre/ +node_modules/ +.DS_Store \ No newline at end of file diff --git a/dbapp/.env.example b/dbapp/.env.example new file mode 100644 index 0000000..0f97818 --- /dev/null +++ b/dbapp/.env.example @@ -0,0 +1,10 @@ +# Production environment variables +DEBUG=False +ENVIRONMENT=production +SECRET_KEY=your_very_long_secret_key_here_change_this_to_something_secure +DB_NAME=geodb +DB_USER=geralt +DB_PASSWORD=your_secure_db_password +DB_HOST=db +DB_PORT=5432 +ALLOWED_HOSTS=localhost,yourdomain.com \ No newline at end of file diff --git a/dbapp/Dockerfile b/dbapp/Dockerfile new file mode 100644 index 0000000..8b95d3f --- /dev/null +++ b/dbapp/Dockerfile @@ -0,0 +1,51 @@ +# Use Python 3.13 slim image as base +FROM python:3.13.9-slim + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/app \ + DJANGO_SETTINGS_MODULE=dbapp.settings.production + +# Install system dependencies including GDAL and PostGIS dependencies +RUN apt-get update && apt-get install -y \ + gdal-bin \ + libgdal-dev \ + proj-bin \ + proj-data \ + libproj-dev \ + libgeos-dev \ + postgresql-client \ + build-essential \ + libpq-dev \ + gcc \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies for GDAL +RUN pip install --upgrade pip && \ + pip install --no-cache-dir GDAL==$(gdal-config --version) + +# Set work directory +WORKDIR /app + +# Copy project requirements +COPY pyproject.toml uv.lock ./ + +# Install uv package manager +RUN pip install --upgrade pip && pip install uv + +# Install dependencies using uv +RUN uv pip install --system --no-cache-dir -r uv.lock + +# Copy project +COPY . . + +# Collect static files +RUN python manage.py collectstatic --noinput + +# Expose port +EXPOSE 8000 + +# Run gunicorn server +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "dbapp.wsgi:application"] \ No newline at end of file diff --git a/dbapp/Dockerfile.prod b/dbapp/Dockerfile.prod new file mode 100644 index 0000000..1e14d2a --- /dev/null +++ b/dbapp/Dockerfile.prod @@ -0,0 +1,73 @@ +# Multi-stage build for production +FROM python:3.13-slim as requirements-stage + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gdal-bin \ + libgdal-dev \ + proj-bin \ + proj-data \ + libproj-dev \ + libgeos-dev \ + build-essential \ + libpq-dev \ + gcc \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies for GDAL +RUN pip install --upgrade pip && \ + pip install --no-cache-dir GDAL==$(gdal-config --version) + +WORKDIR /app + +# Copy project requirements +COPY pyproject.toml uv.lock ./ + +# Install uv package manager +RUN pip install --upgrade pip && pip install uv + +# Install dependencies using uv +RUN uv pip install --system --only-binary=gdal,shapely,pyproj --no-cache-dir -r uv.lock + + +# Production stage +FROM python:3.13-slim + +# Install runtime system dependencies +RUN apt-get update && apt-get install -y \ + gdal-bin \ + libgdal30 \ + libproj25 \ + libgeos-c1v5 \ + postgresql-client \ + libpq5 \ + && rm -rf /var/lib/apt/lists/* + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + DJANGO_SETTINGS_MODULE=dbapp.settings.production + +# Set work directory +WORKDIR /app + +# Copy Python dependencies from previous stage +COPY --from=requirements-stage /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages +COPY --from=requirements-stage /usr/local/bin /usr/local/bin + +# Copy project +COPY . . + +# Create non-root user for security +RUN useradd --create-home --shell /bin/bash app && chown -R app:app /app +USER app + +# Collect static files +RUN python manage.py collectstatic --noinput + +# Expose port +EXPOSE 8000 + +# Run gunicorn server +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "--timeout", "120", "dbapp.wsgi:application"] \ No newline at end of file diff --git a/dbapp/dbapp/settings/__init__.py b/dbapp/dbapp/settings/__init__.py new file mode 100644 index 0000000..21d4a31 --- /dev/null +++ b/dbapp/dbapp/settings/__init__.py @@ -0,0 +1,13 @@ +import os +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Determine the environment and import the appropriate settings +ENVIRONMENT = os.getenv('ENVIRONMENT', 'development') + +if ENVIRONMENT == 'production': + from .production import * +else: + from .development import * \ No newline at end of file diff --git a/dbapp/dbapp/settings.py b/dbapp/dbapp/settings/base.py similarity index 86% rename from dbapp/dbapp/settings.py rename to dbapp/dbapp/settings/base.py index c197e43..5a51365 100644 --- a/dbapp/dbapp/settings.py +++ b/dbapp/dbapp/settings/base.py @@ -12,6 +12,10 @@ https://docs.djangoproject.com/en/5.2/ref/settings/ from pathlib import Path import os +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() if os.name == 'nt': OSGEO4W = r"C:\Program Files\OSGeo4W" @@ -29,18 +33,17 @@ BASE_DIR = Path(__file__).resolve().parent.parent # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-7etj5f7buo2a57xv=w3^&llusq8rii7b_gd)9$t_1xcnao!^tq' +SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-7etj5f7buo2a57xv=w3^&llusq8rii7b_gd)9$t_1xcnao!^tq') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = os.getenv('DEBUG', 'True').lower() == 'true' -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',') # Application definition INSTALLED_APPS = [ - # 'django_daisy', 'dal', 'dal_select2', "admin_interface", @@ -64,6 +67,9 @@ INSTALLED_APPS = [ 'debug_toolbar' ] +# Note: Custom user model is implemented via OneToOneField relationship +# AUTH_USER_MODEL = 'mainapp.CustomUser' + MIDDLEWARE = [ "debug_toolbar.middleware.DebugToolbarMiddleware", #Добавил 'django.middleware.security.SecurityMiddleware', @@ -81,7 +87,9 @@ ROOT_URLCONF = 'dbapp.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [ + BASE_DIR / 'templates', # Main project templates directory + ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -101,12 +109,12 @@ WSGI_APPLICATION = 'dbapp.wsgi.application' DATABASES = { 'default': { - 'ENGINE': 'django.contrib.gis.db.backends.postgis', - 'NAME': 'geodb', - 'USER': 'geralt', - 'PASSWORD': '27082025STC', - 'HOST': 'localhost', - 'PORT': '5432', + 'ENGINE': os.getenv('DB_ENGINE', 'django.contrib.gis.db.backends.postgis'), + 'NAME': os.getenv('DB_NAME', 'db'), + 'USER': os.getenv('DB_USER', 'user'), + 'PASSWORD': os.getenv('DB_PASSWORD', 'password'), + 'HOST': os.getenv('DB_HOST', 'localhost'), + 'PORT': os.getenv('DB_PORT', '5432'), } } @@ -147,7 +155,7 @@ USE_TZ = True STATIC_URL = '/static/' STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'static'), + BASE_DIR.parent / 'static', # Reference to the static directory at project root ] # Default primary key field type diff --git a/dbapp/dbapp/settings/development.py b/dbapp/dbapp/settings/development.py new file mode 100644 index 0000000..da07cf4 --- /dev/null +++ b/dbapp/dbapp/settings/development.py @@ -0,0 +1,9 @@ +from .base import * + +# Development-specific settings +DEBUG = True + +# Allow all hosts in development +ALLOWED_HOSTS = ['*'] + +# Additional development settings can go here \ No newline at end of file diff --git a/dbapp/dbapp/settings/production.py b/dbapp/dbapp/settings/production.py new file mode 100644 index 0000000..2328a8d --- /dev/null +++ b/dbapp/dbapp/settings/production.py @@ -0,0 +1,48 @@ +from .base import * +import os +from dotenv import load_dotenv + +# Production-specific settings +DEBUG = False +TEMPLATE_DEBUG = DEBUG + +# In production, specify allowed hosts explicitly +ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS_PROD', 'localhost,127.0.0.1').split(',') + +# Security settings for production +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_SECONDS = 31536000 +SECURE_REDIRECT_EXEMPT = [] +SECURE_SSL_REDIRECT = True +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True + +# Template caching for production +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + BASE_DIR / 'templates', # Main project templates directory + ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + 'loaders': [ + ('django.template.loaders.cached.Loader', [ + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ]), + ], + }, + }, +] + +# Static files settings for production +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') +STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' \ No newline at end of file diff --git a/dbapp/mainapp/urls.py b/dbapp/mainapp/urls.py index c3848a9..8b62f86 100644 --- a/dbapp/mainapp/urls.py +++ b/dbapp/mainapp/urls.py @@ -5,16 +5,16 @@ from . import views urlpatterns = [ - path('', views.home_page, name='home'), - path('excel-data', views.load_excel_data, name='load_excel_data'), - path('satellites', views.add_satellites, name='add_sats'), - path('api/locations//geojson/', views.get_locations, name='locations_by_id'), - path('transponders', views.add_transponders, name='add_trans'), - path('csv-data', views.load_raw_csv_data, name='load_csv_data'), - path('map-points/', views.show_map_view, name='admin_show_map'), - path('cluster/', views.cluster_test, name='cluster'), - path('vch-upload/', views.upload_vch_load_from_html, name='vch_load'), - path('vch-link/', views.link_vch_sigma, name='link_vch_sigma'), + path('', views.HomePageView.as_view(), name='home'), + path('excel-data', views.LoadExcelDataView.as_view(), name='load_excel_data'), + path('satellites', views.AddSatellitesView.as_view(), name='add_sats'), + path('api/locations//geojson/', views.GetLocationsView.as_view(), name='locations_by_id'), + path('transponders', views.AddTranspondersView.as_view(), name='add_trans'), + path('csv-data', views.LoadCsvDataView.as_view(), name='load_csv_data'), + path('map-points/', views.ShowMapView.as_view(), name='admin_show_map'), + path('cluster/', views.ClusterTestView.as_view(), name='cluster'), + path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'), + path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'), # path('upload/', views.upload_file, name='upload_file'), ] \ No newline at end of file diff --git a/dbapp/mainapp/views.py b/dbapp/mainapp/views.py index fe7af15..564fc52 100644 --- a/dbapp/mainapp/views.py +++ b/dbapp/mainapp/views.py @@ -3,6 +3,10 @@ from django.contrib import messages from django.http import JsonResponse from django.views.decorators.http import require_GET from django.contrib.admin.views.decorators import staff_member_required +from django.utils.decorators import method_decorator +from django.views import View +from django.views.generic import TemplateView, FormView +from django.contrib.auth.mixins import UserPassesTestMixin import pandas as pd from .utils import ( fill_data_from_df, @@ -18,97 +22,101 @@ from .clusters import get_clusters from dbapp.settings import BASE_DIR -def add_satellites(request): - add_satellite_list() - return redirect('home') +class AddSatellitesView(View): + def get(self, request): + add_satellite_list() + return redirect('home') -def add_transponders(request): - try: - parse_transponders_from_json(BASE_DIR / "transponders.json") - except FileNotFoundError: - print("Файл не найден") - return redirect('home') +class AddTranspondersView(View): + def get(self, request): + try: + parse_transponders_from_json(BASE_DIR / "transponders.json") + except FileNotFoundError: + print("Файл не найден") + return redirect('home') + +class HomePageView(TemplateView): + template_name = 'mainapp/home.html' -def home_page(request): - return render(request, 'mainapp/home.html') +class LoadExcelDataView(FormView): + template_name = 'mainapp/add_data_from_excel.html' + form_class = LoadExcelData + + def form_valid(self, form): + uploaded_file = self.request.FILES['file'] + selected_sat = form.cleaned_data['sat_choice'] + number = form.cleaned_data['number_input'] + + try: + import io + df = pd.read_excel(io.BytesIO(uploaded_file.read())) + if number > 0: + df = df.head(number) + result = fill_data_from_df(df, selected_sat) + + messages.success(self.request, f"Данные успешно загружены! Обработано строк: {result}") + return redirect('load_excel_data') + except Exception as e: + messages.error(self.request, f"Ошибка при обработке файла: {str(e)}") + return redirect('load_excel_data') + + def form_invalid(self, form): + messages.error(self.request, "Форма заполнена некорректно.") + return super().form_invalid(form) -def load_excel_data(request): - if request.method == "POST": - form = LoadExcelData(request.POST, request.FILES) - if form.is_valid(): - uploaded_file = request.FILES['file'] - selected_sat = form.cleaned_data['sat_choice'] - number = form.cleaned_data['number_input'] +from django.views.generic import View - try: - # Create a temporary file-like object from the uploaded file - import io - df = pd.read_excel(io.BytesIO(uploaded_file.read())) - if number > 0: - df = df.head(number) - result = fill_data_from_df(df, selected_sat) +class GetLocationsView(View): + def get(self, request, sat_id): + locations = ObjItem.objects.filter(id_vch_load__id_satellite=sat_id) + if not locations: + return JsonResponse({'error': 'Объектов не найдено'}, status=400) - messages.success(request, f"Данные успешно загружены! Обработано строк: {result}") - return redirect('load_excel_data') - except Exception as e: - messages.error(request, f"Ошибка при обработке файла: {str(e)}") - return redirect('load_excel_data') - else: - form = LoadExcelData() + features = [] + for loc in locations: + features.append({ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [loc.id_geo.coords[0], loc.id_geo.coords[1]] + }, + "properties": { + "pol": loc.id_vch_load.polarization.name, + "freq": loc.id_vch_load.frequency*1000000, + "name": f"{loc.name}", + "id": loc.id_geo.id + } + }) - return render(request, 'mainapp/add_data_from_excel.html', {'form': form}) - - -def get_locations(request, sat_id): - locations = ObjItem.objects.filter(id_vch_load__id_satellite=sat_id) - if not locations: - return JsonResponse({'error': 'Объектов не найдено'}, status=400) - - features = [] - for loc in locations: - features.append({ - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [loc.id_geo.coords[0], loc.id_geo.coords[1]] - }, - "properties": { - "pol": loc.id_vch_load.polarization.name, - "freq": loc.id_vch_load.frequency*1000000, - "name": f"{loc.name}", - "id": loc.id_geo.id - } + return JsonResponse({ + "type": "FeatureCollection", + "features": features }) - return JsonResponse({ - "type": "FeatureCollection", - "features": features - }) +class LoadCsvDataView(FormView): + template_name = 'mainapp/add_data_from_csv.html' + form_class = LoadCsvData -def load_raw_csv_data(request): - if request.method == "POST": - form = LoadCsvData(request.POST, request.FILES) - if form.is_valid(): - uploaded_file = request.FILES['file'] - try: - # Read the file content and pass it directly to the function - content = uploaded_file.read() - if isinstance(content, bytes): - content = content.decode('utf-8') - - get_points_from_csv(content) - messages.success(request, f"Данные успешно загружены!") - return redirect('load_csv_data') - except Exception as e: - messages.error(request, f"Ошибка при обработке файла: {str(e)}") - return redirect('load_csv_data') - - else: - form = LoadCsvData() + def form_valid(self, form): + uploaded_file = self.request.FILES['file'] + try: + # Read the file content and pass it directly to the function + content = uploaded_file.read() + if isinstance(content, bytes): + content = content.decode('utf-8') + + get_points_from_csv(content) + messages.success(self.request, f"Данные успешно загружены!") + return redirect('load_csv_data') + except Exception as e: + messages.error(self.request, f"Ошибка при обработке файла: {str(e)}") + return redirect('load_csv_data') - return render(request, 'mainapp/add_data_from_csv.html', {'form': form}) + def form_invalid(self, form): + messages.error(self.request, "Форма заполнена некорректно.") + return super().form_invalid(form) # def upload_file(request): # if request.method == 'POST' and request.FILES: @@ -127,87 +135,93 @@ def load_raw_csv_data(request): # return JsonResponse({'status': 'error', 'errors': form.errors}, status=400) # return render(request, 'mainapp/add_data_from_csv.html') from collections import defaultdict -@staff_member_required -def show_map_view(request): - ids = request.GET.get('ids', '') - points = [] - if ids: - id_list = [int(x) for x in ids.split(',') if x.isdigit()] - locations = ObjItem.objects.filter(id__in=id_list) - for obj in locations: - points.append({ - 'name': f"{obj.name}", - 'freq': f"{obj.id_vch_load.frequency} [{obj.id_vch_load.freq_range}] МГц", - 'point': (obj.id_geo.coords.x, obj.id_geo.coords.y) - }) - else: - return redirect('admin') - grouped = defaultdict(list) - for p in points: - grouped[p["name"]].append({ - 'point': p["point"], - 'frequency': p["freq"] - }) - # Преобразуем в список словарей для удобства в шаблоне - groups = [ - { - "name": name, - "points": coords_list - } - for name, coords_list in grouped.items() - ] +@method_decorator(staff_member_required, name='dispatch') +class ShowMapView(UserPassesTestMixin, View): + def test_func(self): + return self.request.user.is_staff - - context = { - 'groups': groups, - } - return render(request, 'admin/map_custom.html', context) - - -def cluster_test(request): - objs = ObjItem.objects.filter(name__icontains="! Astra 4A 12654,040 [1,962] МГц H") - coords = [] - for obj in objs: - coords.append((obj.id_geo.coords[1], obj.id_geo.coords[0])) - get_clusters(coords) - - return JsonResponse({"success": "ок"}) - -def upload_vch_load_from_html(request): - if request.method == 'POST': - form = UploadFileForm(request.POST, request.FILES) - if form.is_valid(): - selected_sat = form.cleaned_data['sat_choice'] - uploaded_file = request.FILES['file'] - try: - get_vch_load_from_html(uploaded_file, selected_sat) - messages.success(request, "Файл успешно обработан") - except ValueError as e: - messages.error(request, f"Ошибка при чтении таблиц: {e}") - except Exception as e: - messages.error(request, f"Неизвестная ошибка: {e}") + def get(self, request): + ids = request.GET.get('ids', '') + points = [] + if ids: + id_list = [int(x) for x in ids.split(',') if x.isdigit()] + locations = ObjItem.objects.filter(id__in=id_list) + for obj in locations: + points.append({ + 'name': f"{obj.name}", + 'freq': f"{obj.id_vch_load.frequency} [{obj.id_vch_load.freq_range}] МГц", + 'point': (obj.id_geo.coords.x, obj.id_geo.coords.y) + }) else: - messages.error(request, "Форма заполнена некорректно.") - else: - form = UploadFileForm() - - return render(request, 'mainapp/upload_html.html', {'form': form}) + return redirect('admin') + grouped = defaultdict(list) + for p in points: + grouped[p["name"]].append({ + 'point': p["point"], + 'frequency': p["freq"] + }) + + # Преобразуем в список словарей для удобства в шаблоне + groups = [ + { + "name": name, + "points": coords_list + } + for name, coords_list in grouped.items() + ] -def link_vch_sigma(request): - if request.method == 'POST': - form = VchLinkForm(request.POST) - if form.is_valid(): - freq = form.cleaned_data['value1'] - freq_range = form.cleaned_data['value2'] - ku_range = float(form.cleaned_data['ku_range']) - sat_id = form.cleaned_data['sat_choice'] - # print(freq, freq_range, ku_range, sat_id.pk) - count_all, link_count = compare_and_link_vch_load(sat_id, freq, freq_range, ku_range) - messages.success(request, f"Привязано {link_count} из {count_all} объектов") - return redirect('link_vch_sigma') - else: - form = VchLinkForm() - - return render(request, 'mainapp/link_vch.html', {'form': form}) \ No newline at end of file + context = { + 'groups': groups, + } + return render(request, 'admin/map_custom.html', context) + + +class ClusterTestView(View): + def get(self, request): + objs = ObjItem.objects.filter(name__icontains="! Astra 4A 12654,040 [1,962] МГц H") + coords = [] + for obj in objs: + coords.append((obj.id_geo.coords[1], obj.id_geo.coords[0])) + get_clusters(coords) + + return JsonResponse({"success": "ок"}) + +class UploadVchLoadView(FormView): + template_name = 'mainapp/upload_html.html' + form_class = UploadFileForm + + def form_valid(self, form): + selected_sat = form.cleaned_data['sat_choice'] + uploaded_file = self.request.FILES['file'] + try: + get_vch_load_from_html(uploaded_file, selected_sat) + messages.success(self.request, "Файл успешно обработан") + except ValueError as e: + messages.error(self.request, f"Ошибка при чтении таблиц: {e}") + except Exception as e: + messages.error(self.request, f"Неизвестная ошибка: {e}") + return redirect('vch_load') + + def form_invalid(self, form): + messages.error(self.request, "Форма заполнена некорректно.") + return super().form_invalid(form) + + +class LinkVchSigmaView(FormView): + template_name = 'mainapp/link_vch.html' + form_class = VchLinkForm + + def form_valid(self, form): + freq = form.cleaned_data['value1'] + freq_range = form.cleaned_data['value2'] + ku_range = float(form.cleaned_data['ku_range']) + sat_id = form.cleaned_data['sat_choice'] + # print(freq, freq_range, ku_range, sat_id.pk) + count_all, link_count = compare_and_link_vch_load(sat_id, freq, freq_range, ku_range) + messages.success(self.request, f"Привязано {link_count} из {count_all} объектов") + return redirect('link_vch_sigma') + + def form_invalid(self, form): + return self.render_to_response(self.get_context_data(form=form)) \ No newline at end of file diff --git a/dbapp/mapsapp/urls.py b/dbapp/mapsapp/urls.py index 20a83ab..0067dd0 100644 --- a/dbapp/mapsapp/urls.py +++ b/dbapp/mapsapp/urls.py @@ -5,11 +5,11 @@ from . import views urlpatterns = [ - path('3dmap', views.cesium_map, name='3dmap'), - path('2dmap', views.leaflet_map, name='2dmap'), - path('api/footprint-names/', views.get_footprints, name="footprint_names"), - path('api/transponders/', views.get_transponder_on_satid, name='transponders_data'), - path('tiles////.png', views.tile_proxy, name='tile_proxy'), + path('3dmap', views.CesiumMapView.as_view(), name='3dmap'), + path('2dmap', views.LeafletMapView.as_view(), name='2dmap'), + path('api/footprint-names/', views.GetFootprintsView.as_view(), name="footprint_names"), + path('api/transponders/', views.GetTransponderOnSatIdView.as_view(), name='transponders_data'), + path('tiles////.png', views.TileProxyView.as_view(), name='tile_proxy'), # path('', views.home_page, name='home'), # path('excel-data', views.load_excel_data, name='load_excel_data'), # path('satellites', views.add_satellites, name='add_sats'), diff --git a/dbapp/mapsapp/views.py b/dbapp/mapsapp/views.py index b1e22ff..f664b6b 100644 --- a/dbapp/mapsapp/views.py +++ b/dbapp/mapsapp/views.py @@ -5,64 +5,80 @@ from django.core import serializers from django.http import HttpResponse, HttpResponseNotFound from django.views.decorators.cache import cache_page from django.views.decorators.http import require_GET +from django.utils.decorators import method_decorator +from django.views import View +from django.views.generic import TemplateView from mainapp.models import Satellite from .models import Transponders from .utils import get_band_names -def cesium_map(request): - sats = Satellite.objects.all() +class CesiumMapView(TemplateView): + template_name = 'mapsapp/map3d.html' - return render(request, 'mapsapp/map3d.html', {'sats': sats}) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['sats'] = Satellite.objects.all() + return context -def get_footprints(request, sat_id): - try: - sat_name = Satellite.objects.get(id=sat_id).name - footprint_names = get_band_names(sat_name) - - return JsonResponse(footprint_names, safe=False) - except Exception as e: - return JsonResponse({"error": str(e)}, status=400) +class GetFootprintsView(View): + def get(self, request, sat_id): + try: + sat_name = Satellite.objects.get(id=sat_id).name + footprint_names = get_band_names(sat_name) + + return JsonResponse(footprint_names, safe=False) + except Exception as e: + return JsonResponse({"error": str(e)}, status=400) -@require_GET -@cache_page(60 * 60 * 24) -def tile_proxy(request, footprint_name, z, x, y): - if not footprint_name.replace('-', '').replace('_', '').isalnum(): - return HttpResponse("Invalid footprint name", status=400) +class TileProxyView(View): + @method_decorator(require_GET) + @method_decorator(cache_page(60 * 60 * 24)) # Cache for 24 hours + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) - url = f"https://static.satbeams.com/tiles/{footprint_name}/{z}/{x}/{y}.png" + def get(self, request, footprint_name, z, x, y): + if not footprint_name.replace('-', '').replace('_', '').isalnum(): + return HttpResponse("Invalid footprint name", status=400) - try: - resp = requests.get(url, timeout=10) - if resp.status_code == 200: - response = HttpResponse(resp.content, content_type='image/png') - response["Access-Control-Allow-Origin"] = "*" - return response - else: - return HttpResponseNotFound("Tile not found") - except Exception as e: - return HttpResponse(f"Proxy error: {e}", status=500) + url = f"https://static.satbeams.com/tiles/{footprint_name}/{z}/{x}/{y}.png" + + try: + resp = requests.get(url, timeout=10) + if resp.status_code == 200: + response = HttpResponse(resp.content, content_type='image/png') + response["Access-Control-Allow-Origin"] = "*" + return response + else: + return HttpResponseNotFound("Tile not found") + except Exception as e: + return HttpResponse(f"Proxy error: {e}", status=500) -def leaflet_map(request): - sats = Satellite.objects.all() - trans = Transponders.objects.all() - return render(request, 'mapsapp/map2d.html', {'sats': sats, 'trans': trans}) +class LeafletMapView(TemplateView): + template_name = 'mapsapp/map2d.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['sats'] = Satellite.objects.all() + context['trans'] = Transponders.objects.all() + return context -def get_transponder_on_satid(request, sat_id): - trans = Transponders.objects.filter(sat_id=sat_id) - output = [] - for tran in trans: - output.append( - { - "name": tran.name, - "frequency": tran.frequency, - "frequency_range": tran.frequency_range, - "zone_name": tran.zone_name, - "polarization": tran.polarization.name - } - ) - if not trans: - return JsonResponse({'error': 'Объектов не найдено'}, status=400) +class GetTransponderOnSatIdView(View): + def get(self, request, sat_id): + trans = Transponders.objects.filter(sat_id=sat_id) + output = [] + for tran in trans: + output.append( + { + "name": tran.name, + "frequency": tran.frequency, + "frequency_range": tran.frequency_range, + "zone_name": tran.zone_name, + "polarization": tran.polarization.name + } + ) + if not trans: + return JsonResponse({'error': 'Объектов не найдено'}, status=400) - return JsonResponse(output, safe=False) \ No newline at end of file + return JsonResponse(output, safe=False) \ No newline at end of file diff --git a/pyproject.toml b/dbapp/pyproject.toml similarity index 100% rename from pyproject.toml rename to dbapp/pyproject.toml diff --git a/uv.lock b/dbapp/uv.lock similarity index 100% rename from uv.lock rename to dbapp/uv.lock