From 6d2be926921ad84b078717a9807d84376df08db5 Mon Sep 17 00:00:00 2001 From: Ahmed Nassar Date: Wed, 11 Dec 2024 21:51:00 +0200 Subject: [PATCH] feat: Restructure stations and lines apps by removing unused files, adding new models, and enhancing admin configurations --- apps/lines/admin.py | 3 -- apps/lines/apps.py | 6 --- apps/lines/migrations/__init__.py | 0 apps/lines/models.py | 20 -------- apps/lines/tests.py | 3 -- apps/lines/views.py | 3 -- apps/stations/admin.py | 47 ++++++++++++++++++- apps/stations/apps.py | 3 +- apps/stations/migrations/0001_initial.py | 53 ++++++++++++++++++++++ apps/stations/models.py | 32 ++++++++++--- apps/stations/services/__init__.py | 1 + apps/stations/services/route_service.py | 2 + apps/stations/services/ticket_service.py | 1 + apps/{lines => stations/utils}/__init__.py | 0 apps/stations/views.py | 14 +++--- apps/users/admin.py | 4 ++ egypt_metro/settings.py | 26 +++++++++-- egypt_metro/urls.py | 4 +- requirements.txt | 21 +++++++++ 19 files changed, 189 insertions(+), 54 deletions(-) delete mode 100644 apps/lines/admin.py delete mode 100644 apps/lines/apps.py delete mode 100644 apps/lines/migrations/__init__.py delete mode 100644 apps/lines/models.py delete mode 100644 apps/lines/tests.py delete mode 100644 apps/lines/views.py create mode 100644 apps/stations/migrations/0001_initial.py create mode 100644 apps/stations/services/__init__.py rename apps/{lines => stations/utils}/__init__.py (100%) diff --git a/apps/lines/admin.py b/apps/lines/admin.py deleted file mode 100644 index 8c38f3f3..00000000 --- a/apps/lines/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/apps/lines/apps.py b/apps/lines/apps.py deleted file mode 100644 index 73b41c1a..00000000 --- a/apps/lines/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class LinesConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'apps.lines' diff --git a/apps/lines/migrations/__init__.py b/apps/lines/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/lines/models.py b/apps/lines/models.py deleted file mode 100644 index ba7f6546..00000000 --- a/apps/lines/models.py +++ /dev/null @@ -1,20 +0,0 @@ -# apps/lines/models.py -from django.db import models - -# Create your models here. -class Line(models.Model): - name = models.CharField(max_length=255, unique=True, null=False) # Line name - color_code = models.CharField( - max_length=10, null=True, blank=True, help_text="Format: #RRGGBB" - ) # Optional color code - - def __str__(self): - return self.name - - def total_stations(self): - """Returns the total number of stations on this line.""" - return self.line_stations.count() - - def ordered_stations(self): - """Returns all stations in order.""" - return self.line_stations.order_by("order") \ No newline at end of file diff --git a/apps/lines/tests.py b/apps/lines/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/apps/lines/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/apps/lines/views.py b/apps/lines/views.py deleted file mode 100644 index 91ea44a2..00000000 --- a/apps/lines/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/apps/stations/admin.py b/apps/stations/admin.py index 8c38f3f3..0ab46d17 100644 --- a/apps/stations/admin.py +++ b/apps/stations/admin.py @@ -1,3 +1,48 @@ +# apps/stations/admin.py + from django.contrib import admin +from .models import Line, Station, LineStation + +class LineStationInline(admin.TabularInline): + """ + Inline admin for LineStation to manage the relationship between lines and stations. + """ + model = LineStation + extra = 1 + fields = ["station", "order"] + ordering = ["order"] + + +@admin.register(Line) +class LineAdmin(admin.ModelAdmin): + """ + Admin configuration for the Line model. + """ + list_display = ("name", "color_code", "total_stations") + search_fields = ("name",) + inlines = [LineStationInline] + + +@admin.register(Station) +class StationAdmin(admin.ModelAdmin): + """ + Admin configuration for the Station model. + """ + list_display = ("name", "latitude", "longitude", "is_interchange") + search_fields = ("name",) + inlines = [LineStationInline] # Use the inline for managing lines + fieldsets = ( + (None, { + "fields": ("name", "latitude", "longitude"), + }), + ) + -# Register your models here. +@admin.register(LineStation) +class LineStationAdmin(admin.ModelAdmin): + """ + Admin configuration for the LineStation model. + """ + list_display = ("line", "station", "order") + list_filter = ("line",) + ordering = ["line", "order"] diff --git a/apps/stations/apps.py b/apps/stations/apps.py index fe6d6a87..0141eba0 100644 --- a/apps/stations/apps.py +++ b/apps/stations/apps.py @@ -1,5 +1,6 @@ -from django.apps import AppConfig +# apps/stations/apps.py +from django.apps import AppConfig class StationsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' diff --git a/apps/stations/migrations/0001_initial.py b/apps/stations/migrations/0001_initial.py new file mode 100644 index 00000000..1702bafd --- /dev/null +++ b/apps/stations/migrations/0001_initial.py @@ -0,0 +1,53 @@ +# Generated by Django 5.1.3 on 2024-12-11 19:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Line', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True)), + ('color_code', models.CharField(blank=True, help_text='Format: #RRGGBB', max_length=10, null=True)), + ], + ), + migrations.CreateModel( + name='LineStation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.PositiveIntegerField()), + ('line', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='line_stations', to='stations.line')), + ], + options={ + 'ordering': ['order'], + }, + ), + migrations.CreateModel( + name='Station', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True)), + ('latitude', models.FloatField(blank=True, null=True)), + ('longitude', models.FloatField(blank=True, null=True)), + ('lines', models.ManyToManyField(related_name='stations', through='stations.LineStation', to='stations.line')), + ], + ), + migrations.AddField( + model_name='linestation', + name='station', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='station_lines', to='stations.station'), + ), + migrations.AlterUniqueTogether( + name='linestation', + unique_together={('line', 'station')}, + ), + ] diff --git a/apps/stations/models.py b/apps/stations/models.py index 07124a05..f5fa1e81 100644 --- a/apps/stations/models.py +++ b/apps/stations/models.py @@ -1,9 +1,28 @@ # apps/stations/models.py + from django.db import models -from lines.models import Line from geopy.distance import geodesic # type: ignore # Create your models here. +class Line(models.Model): + """Represents a metro line with stations connected in order.""" + name = models.CharField(max_length=255, unique=True, null=False) # Line name + color_code = models.CharField( + max_length=10, null=True, blank=True, help_text="Format: #RRGGBB" + ) # Optional color code + + def __str__(self): + return self.name + + def total_stations(self): + """Returns the total number of stations on this line.""" + return self.line_stations.count() + + def ordered_stations(self): + """Returns all stations in order.""" + return self.line_stations.order_by("order") + + class Station(models.Model): name = models.CharField(max_length=255, unique=True, null=False) # Station name latitude = models.FloatField(null=True, blank=True) # GPS latitude @@ -22,11 +41,12 @@ def is_interchange(self): return self.lines.count() > 1 def get_station_order(self, line): - """Get the order of this station on a specific line.""" - try: - return self.station_lines.get(line=line).order - except LineStation.DoesNotExist: - return None + """ + Get the order of this station on a specific line. + Returns None if the station is not associated with the given line. + """ + line_station = self.station_lines.filter(line=line).first() + return line_station.order if line_station else None def distance_to(self, other_station): """ Calculate distance (in meters) between two stations using lat-long.""" diff --git a/apps/stations/services/__init__.py b/apps/stations/services/__init__.py new file mode 100644 index 00000000..b1eb9599 --- /dev/null +++ b/apps/stations/services/__init__.py @@ -0,0 +1 @@ +# This file marks the directory as a Python package. \ No newline at end of file diff --git a/apps/stations/services/route_service.py b/apps/stations/services/route_service.py index d68f354e..0c755ab8 100644 --- a/apps/stations/services/route_service.py +++ b/apps/stations/services/route_service.py @@ -1,3 +1,5 @@ +# apps/stations/services/route_service.py + from collections import defaultdict import heapq diff --git a/apps/stations/services/ticket_service.py b/apps/stations/services/ticket_service.py index 44b9f308..7f58797e 100644 --- a/apps/stations/services/ticket_service.py +++ b/apps/stations/services/ticket_service.py @@ -1,4 +1,5 @@ # apps/stations/services/ticket_service.py + def calculate_ticket_price(start_station, end_station): """ Calculate the ticket price based on the number of stations between start and end. diff --git a/apps/lines/__init__.py b/apps/stations/utils/__init__.py similarity index 100% rename from apps/lines/__init__.py rename to apps/stations/utils/__init__.py diff --git a/apps/stations/views.py b/apps/stations/views.py index 212939b7..470e885c 100644 --- a/apps/stations/views.py +++ b/apps/stations/views.py @@ -1,11 +1,13 @@ +# apps/stations/views.py + from django.shortcuts import render from rest_framework.views import APIView from rest_framework.response import Response from django.http import JsonResponse -from stations.models import Station +from apps.stations.models import Station from .services.route_service import RouteService -from stations.services.ticket_service import calculate_ticket_price -from .utils.location_helpers import find_nearest_station +from apps.stations.services.ticket_service import calculate_ticket_price +from apps.stations.utils.location_helpers import find_nearest_station from geopy.distance import geodesic # type: ignore # Create your views here. @@ -42,13 +44,13 @@ def get(self, request, start_station_id, end_station_id): class NearestStationView(APIView): def get(self, request): - latitude = request.query_params.get("latitude") - longitude = request.query_params.get("longitude") + latitude = float(request.query_params.get("latitude")) + longitude = float(request.query_params.get("longitude")) if not latitude or not longitude: return Response({"error": "Latitude and Longitude are required"}, status=400) - nearest_station, distance = find_nearest_station(float(latitude), float(longitude)) + nearest_station, distance = find_nearest_station(latitude, longitude) return Response({ "nearest_station": nearest_station.name, "distance": round(distance, 2), diff --git a/apps/users/admin.py b/apps/users/admin.py index fc2610ca..765e3baa 100644 --- a/apps/users/admin.py +++ b/apps/users/admin.py @@ -9,3 +9,7 @@ class UserAdmin(admin.ModelAdmin): def has_delete_permission(self, request, obj=None): return False # Disable user deletion + +admin.site.site_header = "Egypt Metro" +admin.site.site_title = "Egypt Metro Admin Portal" +admin.site.index_title = "Welcome to Egypt Metro Admin Portal" \ No newline at end of file diff --git a/egypt_metro/settings.py b/egypt_metro/settings.py index eeb21b8a..01f4056d 100644 --- a/egypt_metro/settings.py +++ b/egypt_metro/settings.py @@ -37,14 +37,17 @@ 'django.contrib.messages', 'django.contrib.staticfiles', # External packages + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + 'allauth.socialaccount.providers.google', 'rest_framework', 'rest_framework_simplejwt', 'corsheaders', 'debug_toolbar', # Custom apps 'apps.users.apps.UsersConfig', - 'apps.stations.apps.UsersConfig', - 'apps.lines.apps.UsersConfig', + 'apps.stations.apps.StationsConfig', ] MIDDLEWARE = [ @@ -58,6 +61,7 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'corsheaders.middleware.CorsMiddleware', 'debug_toolbar.middleware.DebugToolbarMiddleware', + "allauth.account.middleware.AccountMiddleware", ] ROOT_URLCONF = 'egypt_metro.urls' @@ -76,6 +80,7 @@ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'django.template.context_processors.request', ], }, }, @@ -144,6 +149,21 @@ }, ] +AUTHENTICATION_BACKENDS = [ + 'django.contrib.auth.backends.ModelBackend', # For admin logins + 'allauth.account.auth_backends.AuthenticationBackend', +] + +SOCIALACCOUNT_PROVIDERS = { + 'google': { + 'APP': { + 'client_id': 'your-client-id', + 'secret': 'your-secret', + 'key': '' + } + } +} + REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', @@ -157,7 +177,7 @@ } SIMPLE_JWT = { - 'SIGNING_KEY': JWT_SECRET, + 'SIGNING_KEY': os.getenv("JWT_SECRET"), 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), } diff --git a/egypt_metro/urls.py b/egypt_metro/urls.py index a3e73671..cb26ff15 100644 --- a/egypt_metro/urls.py +++ b/egypt_metro/urls.py @@ -22,10 +22,10 @@ urlpatterns = [ path('admin/', admin.site.urls), # Django admin panel + path('accounts/', include('allauth.urls')), # Allauth authentication routes path('api/users/', include('apps.users.urls')), # User-related API routes + path('api/stations/', include('apps.stations.urls')), # Station-related API routes path('health/', health_check, name='health_check'), # Health check endpoint - path('api/stations/', include('apps.stations.urls')), - path('api/lines/', include('apps.lines.urls')), ] if settings.DEBUG: diff --git a/requirements.txt b/requirements.txt index ccd9cca4..0c6ffc1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,23 @@ asgiref==3.8.1 asttokens==2.4.1 beautifulsoup4==4.12.3 +cachetools==5.5.0 +certifi==2024.8.30 +cffi==1.17.1 +charset-normalizer==3.4.0 colorama==0.4.6 comm==0.2.2 +cryptography==44.0.0 debugpy==1.8.5 decorator==5.1.1 distlib==0.3.9 dj-database-url==2.3.0 Django==5.1.3 +django-allauth==65.3.0 django-cors-headers==4.6.0 +django-debug-toolbar==4.4.6 django-environ==0.11.2 +django-redis==5.4.0 djangorestframework==3.15.2 djangorestframework-simplejwt==5.3.1 drf-yasg==1.21.8 @@ -18,8 +26,12 @@ filelock==3.16.1 geographiclib==2.0 geopy==2.4.1 git-filter-repo==2.45.0 +google-auth==2.36.0 +google-auth-httplib2==0.2.0 +google-auth-oauthlib==1.2.1 gunicorn==23.0.0 httplib2==0.22.0 +idna==3.10 inflection==0.5.1 ipykernel==6.29.5 ipython==8.27.0 @@ -28,6 +40,7 @@ jupyter_client==8.6.3 jupyter_core==5.7.2 matplotlib-inline==0.1.7 nest-asyncio==1.6.0 +oauthlib==3.2.2 packaging==24.2 parso==0.8.4 platformdirs==4.3.6 @@ -36,6 +49,9 @@ psutil==6.0.0 psycopg2==2.9.10 psycopg2-binary==2.9.10 pure_eval==0.2.3 +pyasn1==0.6.1 +pyasn1_modules==0.4.1 +pycparser==2.22 Pygments==2.18.0 PyJWT==2.10.1 pyparsing==3.1.4 @@ -46,6 +62,10 @@ pytz==2024.2 pywin32==306 PyYAML==6.0.2 pyzmq==26.2.0 +redis==5.2.1 +requests==2.32.3 +requests-oauthlib==2.0.0 +rsa==4.9 six==1.16.0 soupsieve==2.6 sqlparse==0.5.2 @@ -55,6 +75,7 @@ traitlets==5.14.3 typing_extensions==4.12.2 tzdata==2024.2 uritemplate==4.1.1 +urllib3==2.2.3 virtualenv==20.28.0 waitress==3.0.2 wcwidth==0.2.13