diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index d1dabe0..819c119 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 75bf223..00427d2 100644 Binary files a/core/__pycache__/context_processors.cpython-311.pyc and b/core/__pycache__/context_processors.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index b5c40e3..bed47d4 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 158b311..899df19 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index d97f88e..ee86b23 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 0dbb9bb..c589342 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,15 +1,32 @@ from django.contrib import admin -from .models import Profile, Parcel +from .models import Profile, Parcel, Country, Governate, City + +@admin.register(Country) +class CountryAdmin(admin.ModelAdmin): + list_display = ('name',) + search_fields = ('name',) + +@admin.register(Governate) +class GovernateAdmin(admin.ModelAdmin): + list_display = ('name', 'country') + list_filter = ('country',) + search_fields = ('name',) + +@admin.register(City) +class CityAdmin(admin.ModelAdmin): + list_display = ('name', 'governate') + list_filter = ('governate__country', 'governate') + search_fields = ('name',) @admin.register(Profile) class ProfileAdmin(admin.ModelAdmin): - list_display = ('user', 'role', 'phone_number') - list_filter = ('role',) + list_display = ('user', 'role', 'phone_number', 'country', 'governate', 'city') + list_filter = ('role', 'country', 'governate') search_fields = ('user__username', 'phone_number') @admin.register(Parcel) class ParcelAdmin(admin.ModelAdmin): list_display = ('tracking_number', 'shipper', 'carrier', 'status', 'created_at') - list_filter = ('status', 'created_at') - search_fields = ('tracking_number', 'receiver_name', 'receiver_phone') - readonly_fields = ('tracking_number',) \ No newline at end of file + list_filter = ('status', 'pickup_country', 'delivery_country') + search_fields = ('tracking_number', 'shipper__username', 'carrier__username', 'receiver_name') + readonly_fields = ('tracking_number', 'created_at', 'updated_at') diff --git a/core/context_processors.py b/core/context_processors.py index 0bf87c3..bfbe8d9 100644 --- a/core/context_processors.py +++ b/core/context_processors.py @@ -1,13 +1,22 @@ import os import time +from .models import Profile def project_context(request): """ Adds project-specific environment variables to the template context globally. """ + profile = None + if request.user.is_authenticated: + try: + profile = request.user.profile + except: + profile, created = Profile.objects.get_or_create(user=request.user) + return { "project_description": os.getenv("PROJECT_DESCRIPTION", ""), "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), # Used for cache-busting static assets "deployment_timestamp": int(time.time()), - } + "user_profile": profile, + } \ No newline at end of file diff --git a/core/forms.py b/core/forms.py index a49a99b..5345c0d 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,13 +1,17 @@ from django import forms from django.contrib.auth.models import User from django.utils.translation import gettext_lazy as _ -from .models import Profile, Parcel +from .models import Profile, Parcel, Country, Governate, City class UserRegistrationForm(forms.ModelForm): password = forms.CharField(widget=forms.PasswordInput, label=_("Password")) password_confirm = forms.CharField(widget=forms.PasswordInput, label=_("Confirm Password")) role = forms.ChoiceField(choices=Profile.ROLE_CHOICES, label=_("Register as")) phone_number = forms.CharField(max_length=20, label=_("Phone Number")) + + country = forms.ModelChoiceField(queryset=Country.objects.all(), required=False, label=_("Country")) + governate = forms.ModelChoiceField(queryset=Governate.objects.all(), required=False, label=_("Governate")) + city = forms.ModelChoiceField(queryset=City.objects.all(), required=False, label=_("City")) class Meta: model = User @@ -31,30 +35,53 @@ class UserRegistrationForm(forms.ModelForm): user.set_password(self.cleaned_data['password']) if commit: user.save() - Profile.objects.create( - user=user, - role=self.cleaned_data['role'], - phone_number=self.cleaned_data['phone_number'] - ) + # Profile is created by signal, so we update it + profile, created = Profile.objects.get_or_create(user=user) + profile.role = self.cleaned_data['role'] + profile.phone_number = self.cleaned_data['phone_number'] + profile.country = self.cleaned_data['country'] + profile.governate = self.cleaned_data['governate'] + profile.city = self.cleaned_data['city'] + profile.save() return user class ParcelForm(forms.ModelForm): class Meta: model = Parcel - fields = ['description', 'weight', 'pickup_address', 'delivery_address', 'receiver_name', 'receiver_phone'] + fields = [ + 'description', 'weight', + 'pickup_country', 'pickup_governate', 'pickup_city', 'pickup_address', + 'delivery_country', 'delivery_governate', 'delivery_city', 'delivery_address', + 'receiver_name', 'receiver_phone' + ] widgets = { 'description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control', 'placeholder': _('What are you sending?')}), 'weight': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}), - 'pickup_address': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('123 Street, City')}), - 'delivery_address': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('456 Avenue, City')}), + + 'pickup_country': forms.Select(attrs={'class': 'form-control'}), + 'pickup_governate': forms.Select(attrs={'class': 'form-control'}), + 'pickup_city': forms.Select(attrs={'class': 'form-control'}), + 'pickup_address': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Street/Building')}), + + 'delivery_country': forms.Select(attrs={'class': 'form-control'}), + 'delivery_governate': forms.Select(attrs={'class': 'form-control'}), + 'delivery_city': forms.Select(attrs={'class': 'form-control'}), + 'delivery_address': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Street/Building')}), + 'receiver_name': forms.TextInput(attrs={'class': 'form-control'}), 'receiver_phone': forms.TextInput(attrs={'class': 'form-control'}), } labels = { 'description': _('Package Description'), 'weight': _('Weight (kg)'), - 'pickup_address': _('Pickup Address'), - 'delivery_address': _('Delivery Address'), + 'pickup_country': _('Pickup Country'), + 'pickup_governate': _('Pickup Governate'), + 'pickup_city': _('Pickup City'), + 'pickup_address': _('Pickup Address (Street/Building)'), + 'delivery_country': _('Delivery Country'), + 'delivery_governate': _('Delivery Governate'), + 'delivery_city': _('Delivery City'), + 'delivery_address': _('Delivery Address (Street/Building)'), 'receiver_name': _('Receiver Name'), 'receiver_phone': _('Receiver Phone'), - } + } \ No newline at end of file diff --git a/core/migrations/0003_city_country_parcel_delivery_city_parcel_pickup_city_and_more.py b/core/migrations/0003_city_country_parcel_delivery_city_parcel_pickup_city_and_more.py new file mode 100644 index 0000000..f4721b6 --- /dev/null +++ b/core/migrations/0003_city_country_parcel_delivery_city_parcel_pickup_city_and_more.py @@ -0,0 +1,98 @@ +# Generated by Django 5.2.7 on 2026-01-25 07:39 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_alter_parcel_options_alter_profile_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='City', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='Name')), + ], + options={ + 'verbose_name': 'City', + 'verbose_name_plural': 'Cities', + }, + ), + migrations.CreateModel( + name='Country', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='Name')), + ], + options={ + 'verbose_name': 'Country', + 'verbose_name_plural': 'Countries', + }, + ), + migrations.AddField( + model_name='parcel', + name='delivery_city', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='delivery_parcels', to='core.city', verbose_name='Delivery City'), + ), + migrations.AddField( + model_name='parcel', + name='pickup_city', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pickup_parcels', to='core.city', verbose_name='Pickup City'), + ), + migrations.AddField( + model_name='profile', + name='city', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.city', verbose_name='City'), + ), + migrations.AddField( + model_name='parcel', + name='delivery_country', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='delivery_parcels', to='core.country', verbose_name='Delivery Country'), + ), + migrations.AddField( + model_name='parcel', + name='pickup_country', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pickup_parcels', to='core.country', verbose_name='Pickup Country'), + ), + migrations.AddField( + model_name='profile', + name='country', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.country', verbose_name='Country'), + ), + migrations.CreateModel( + name='Governate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='Name')), + ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.country', verbose_name='Country')), + ], + options={ + 'verbose_name': 'Governate', + 'verbose_name_plural': 'Governates', + }, + ), + migrations.AddField( + model_name='city', + name='governate', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.governate', verbose_name='Governate'), + ), + migrations.AddField( + model_name='parcel', + name='delivery_governate', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='delivery_parcels', to='core.governate', verbose_name='Delivery Governate'), + ), + migrations.AddField( + model_name='parcel', + name='pickup_governate', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pickup_parcels', to='core.governate', verbose_name='Pickup Governate'), + ), + migrations.AddField( + model_name='profile', + name='governate', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.governate', verbose_name='Governate'), + ), + ] diff --git a/core/migrations/__pycache__/0003_city_country_parcel_delivery_city_parcel_pickup_city_and_more.cpython-311.pyc b/core/migrations/__pycache__/0003_city_country_parcel_delivery_city_parcel_pickup_city_and_more.cpython-311.pyc new file mode 100644 index 0000000..2eca790 Binary files /dev/null and b/core/migrations/__pycache__/0003_city_country_parcel_delivery_city_parcel_pickup_city_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 4d7b7fe..5d7afec 100644 --- a/core/models.py +++ b/core/models.py @@ -1,8 +1,42 @@ from django.db import models from django.contrib.auth.models import User from django.utils.translation import gettext_lazy as _ +from django.db.models.signals import post_save +from django.dispatch import receiver import uuid +class Country(models.Model): + name = models.CharField(_('Name'), max_length=100) + + def __str__(self): + return self.name + + class Meta: + verbose_name = _('Country') + verbose_name_plural = _('Countries') + +class Governate(models.Model): + country = models.ForeignKey(Country, on_delete=models.CASCADE, verbose_name=_('Country')) + name = models.CharField(_('Name'), max_length=100) + + def __str__(self): + return f"{self.name} ({self.country.name})" + + class Meta: + verbose_name = _('Governate') + verbose_name_plural = _('Governates') + +class City(models.Model): + governate = models.ForeignKey(Governate, on_delete=models.CASCADE, verbose_name=_('Governate')) + name = models.CharField(_('Name'), max_length=100) + + def __str__(self): + return f"{self.name} ({self.governate.name})" + + class Meta: + verbose_name = _('City') + verbose_name_plural = _('Cities') + class Profile(models.Model): ROLE_CHOICES = ( ('shipper', _('Shipper')), @@ -11,6 +45,10 @@ class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name=_('User')) role = models.CharField(_('Role'), max_length=20, choices=ROLE_CHOICES, default='shipper') phone_number = models.CharField(_('Phone Number'), max_length=20, blank=True) + + country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Country')) + governate = models.ForeignKey(Governate, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Governate')) + city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('City')) def __str__(self): return f"{self.user.username} - {self.get_role_display()}" @@ -19,6 +57,16 @@ class Profile(models.Model): verbose_name = _('Profile') verbose_name_plural = _('Profiles') +@receiver(post_save, sender=User) +def create_user_profile(sender, instance, created, **kwargs): + if created: + Profile.objects.get_or_create(user=instance) + +@receiver(post_save, sender=User) +def save_user_profile(sender, instance, **kwargs): + if hasattr(instance, 'profile'): + instance.profile.save() + class Parcel(models.Model): STATUS_CHOICES = ( ('pending', _('Pending Pickup')), @@ -35,7 +83,16 @@ class Parcel(models.Model): description = models.TextField(_('Description')) weight = models.DecimalField(_('Weight (kg)'), max_digits=5, decimal_places=2, help_text=_("Weight in kg")) + # Pickup Location + pickup_country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True, blank=True, related_name='pickup_parcels', verbose_name=_('Pickup Country')) + pickup_governate = models.ForeignKey(Governate, on_delete=models.SET_NULL, null=True, blank=True, related_name='pickup_parcels', verbose_name=_('Pickup Governate')) + pickup_city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True, blank=True, related_name='pickup_parcels', verbose_name=_('Pickup City')) pickup_address = models.CharField(_('Pickup Address'), max_length=255) + + # Delivery Location + delivery_country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True, blank=True, related_name='delivery_parcels', verbose_name=_('Delivery Country')) + delivery_governate = models.ForeignKey(Governate, on_delete=models.SET_NULL, null=True, blank=True, related_name='delivery_parcels', verbose_name=_('Delivery Governate')) + delivery_city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True, blank=True, related_name='delivery_parcels', verbose_name=_('Delivery City')) delivery_address = models.CharField(_('Delivery Address'), max_length=255) receiver_name = models.CharField(_('Receiver Name'), max_length=100) diff --git a/core/templates/base.html b/core/templates/base.html index 5fc0e5a..cc5425d 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -65,6 +65,11 @@ + {% if user.is_staff %} + + {% endif %} @@ -106,7 +111,7 @@