diff --git a/apps/account/admin/__init__.py b/apps/account/admin/__init__.py index edd4d68..4c99165 100644 --- a/apps/account/admin/__init__.py +++ b/apps/account/admin/__init__.py @@ -4,6 +4,7 @@ from django.template.loader import render_to_string from .user import * from .professor import * from .student import * +from .location import * @register_component diff --git a/apps/account/admin/location.py b/apps/account/admin/location.py new file mode 100644 index 0000000..8f2164e --- /dev/null +++ b/apps/account/admin/location.py @@ -0,0 +1,58 @@ +from django.contrib import admin +from django.utils.translation import gettext_lazy as _ +from unfold.admin import ModelAdmin, TabularInline +from unfold.decorators import display +from unfold.contrib.filters.admin import ( + RangeDateTimeFilter, + TextFilter, + AutocompleteSelectFilter, +) + +from apps.account.models import LocationHistory, User +from utils.admin import project_admin_site + + +class LocationHistoryInline(TabularInline): + model = LocationHistory + extra = 0 + tab = True + fields = ('lat', 'lon', 'country', 'city', 'selected_manually', 'ip', 'timezone', 'at_time') + readonly_fields = ('lat', 'lon', 'country', 'city', 'selected_manually', 'ip', 'timezone', 'at_time',) + verbose_name = _("Location History") + verbose_name_plural = _("Location History") + can_delete = False + show_change_link = True + + +class LocationHistoryAdmin(ModelAdmin): + list_display = ('user', 'display_location', 'country', 'city', 'selected_manually', 'ip', 'display_at_time') + list_filter = [ + ('user', AutocompleteSelectFilter), + 'country', + 'city', + 'selected_manually', + ('at_time', RangeDateTimeFilter), + ] + search_fields = ('user__email', 'user__fullname', 'country', 'city', 'ip') + readonly_fields = ('at_time',) + fieldsets = ( + (None, { + 'fields': ('user', ('lat', 'lon'), ('country', 'city')) + }), + (_('Additional Information'), { + 'fields': ('selected_manually', 'ip', 'timezone', 'at_time'), + 'classes': ('tab',), + }), + ) + + @display(description=_("Location")) + def display_location(self, instance: LocationHistory): + return f"{instance.lat}, {instance.lon}" + + @display(description=_("Date & Time")) + def display_at_time(self, instance: LocationHistory): + return instance.at_time.strftime("%Y-%m-%d %H:%M") if instance.at_time else "-" + + +# Register with project admin site +project_admin_site.register(LocationHistory, LocationHistoryAdmin) \ No newline at end of file diff --git a/apps/account/admin/user.py b/apps/account/admin/user.py index 3b60484..f97ee11 100644 --- a/apps/account/admin/user.py +++ b/apps/account/admin/user.py @@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework.authtoken.models import TokenProxy from ajaxdatatable.admin import AjaxDatatable -from apps.account.models import User, Notification +from apps.account.models import User, Notification, LocationHistory from django import forms from django.contrib import admin from django.urls import path, reverse @@ -34,6 +34,7 @@ from unfold.contrib.filters.admin import ( SingleNumericFilter, TextFilter, ) +from apps.account.admin.location import LocationHistoryInline @@ -56,6 +57,7 @@ class UserAdmin(BaseUserAdmin, ModelAdmin): ("last_login", RangeDateTimeFilter), ("date_joined", RangeDateTimeFilter), ] + inlines = [LocationHistoryInline] add_fieldsets = ( (None, { 'classes': ('wide',), diff --git a/apps/account/migrations/0003_locationhistory.py b/apps/account/migrations/0003_locationhistory.py new file mode 100644 index 0000000..432503b --- /dev/null +++ b/apps/account/migrations/0003_locationhistory.py @@ -0,0 +1,30 @@ +# Generated by Django 5.1.8 on 2025-05-01 15:54 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0002_alter_user_phone_number'), + ] + + operations = [ + migrations.CreateModel( + name='LocationHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('lat', models.FloatField(verbose_name='lat')), + ('lon', models.FloatField(verbose_name='lon')), + ('country', models.CharField(blank=True, max_length=255, null=True, verbose_name='country')), + ('city', models.CharField(blank=True, max_length=255, null=True, verbose_name='city')), + ('selected_manually', models.BooleanField(blank=True, null=True)), + ('ip', models.CharField(blank=True, max_length=255, null=True)), + ('timezone', models.CharField(blank=True, max_length=60, null=True)), + ('at_time', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='location_history', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/apps/account/models/user.py b/apps/account/models/user.py index 64d1bae..ab5a8f0 100644 --- a/apps/account/models/user.py +++ b/apps/account/models/user.py @@ -123,3 +123,16 @@ class LoginHistory(models.Model): ip = models.CharField(max_length=255, null=True) timezone = models.CharField(max_length=100, null=True, blank=True) at_time = models.DateTimeField(auto_now_add=True) + + +class LocationHistory(models.Model): + user = models.ForeignKey("account.User", on_delete=models.CASCADE, related_name='location_history') + lat = models.FloatField(verbose_name=_('lat')) + lon = models.FloatField(verbose_name=_('lon')) + country = models.CharField(max_length=255, verbose_name=_('country'), null=True, blank=True) + city = models.CharField(max_length=255, verbose_name=_('city'), null=True, blank=True) + selected_manually = models.BooleanField(null=True, blank=True) + ip = models.CharField(max_length=255, null=True, blank=True) + timezone = models.CharField(null=True, blank=True, max_length=60) + at_time = models.DateTimeField(auto_now_add=True) + diff --git a/apps/account/serializers/__init__.py b/apps/account/serializers/__init__.py index 8c6bf0d..178e8c6 100644 --- a/apps/account/serializers/__init__.py +++ b/apps/account/serializers/__init__.py @@ -1,2 +1,3 @@ from .user import * from .notification import * +from .location_history import * \ No newline at end of file diff --git a/apps/account/serializers/location_history.py b/apps/account/serializers/location_history.py new file mode 100644 index 0000000..49145dc --- /dev/null +++ b/apps/account/serializers/location_history.py @@ -0,0 +1,10 @@ +from rest_framework import serializers + +from apps.account.models import LocationHistory + + +class LocationHistorySerializer(serializers.ModelSerializer): + user = serializers.HiddenField(default=serializers.CurrentUserDefault()) + class Meta: + model = LocationHistory + exclude = ('at_time',) diff --git a/apps/account/urls.py b/apps/account/urls.py index 46ff361..bcddf1e 100644 --- a/apps/account/urls.py +++ b/apps/account/urls.py @@ -14,6 +14,7 @@ urlpatterns = [ path('verify/', views.UserVerifyView.as_view(), name='user-verify'), path('login/', views.UserLoginView.as_view(), name='user-login'), path('guest/', views.UserGuestView.as_view(), name='user-guest'), + path('location-update/', views.LocationHistoryView.as_view(), name='user-location-history'), # path('notif/', views.NotificationListView.as_view(), name='user-notif'), diff --git a/apps/account/views/__init__.py b/apps/account/views/__init__.py index 8ccf072..136fe46 100644 --- a/apps/account/views/__init__.py +++ b/apps/account/views/__init__.py @@ -1,4 +1,5 @@ from .user import * from .notification import * +from .location_history import * diff --git a/apps/account/views/location_history.py b/apps/account/views/location_history.py new file mode 100644 index 0000000..0cd71f5 --- /dev/null +++ b/apps/account/views/location_history.py @@ -0,0 +1,36 @@ +from rest_framework.mixins import CreateModelMixin +from rest_framework.permissions import IsAuthenticated +from rest_framework.generics import GenericAPIView +from rest_framework.response import Response +from rest_framework import status + +from apps.account.models import LocationHistory +from apps.account.serializers import LocationHistorySerializer + + +class LocationHistoryView(GenericAPIView, CreateModelMixin): + permission_classes = [IsAuthenticated] + serializer_class = LocationHistorySerializer + + def post(self, request, *args, **kwargs): + ip = self.get_client_ip() + data = request.data.copy() + data['ip'] = ip + serializer = self.get_serializer(data=data) + + if serializer.is_valid(): + serializer.save(user=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def get_queryset(self): + return LocationHistory.objects.filter(user=self.request.user) + + def get_client_ip(self): + # Retrieve the client's IP address from the request headers + x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = self.request.META.get('REMOTE_ADDR') + return ip diff --git a/sshs b/sshs index dc0a152..c13f0f6 100644 --- a/sshs +++ b/sshs @@ -1,4 +1,8 @@ +Host 62.60.197.179 + HostName 62.60.197.179 + User ubuntu + Host 88.99.212.243 HostName 88.99.212.243 User nwhco - Port 1782 + Port 1782 \ No newline at end of file