diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a145697..39feadc 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index 3def7a2..e6a472d 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 6d264ac..20493b5 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 78274b5..e6c2226 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index bde313b..b16cdd3 100644 --- a/core/admin.py +++ b/core/admin.py @@ -149,6 +149,10 @@ class PlatformProfileAdmin(admin.ModelAdmin): fieldsets += ((_('Tools'), {'fields': ('test_connection_link',)}),) return fieldsets +class CountryAdmin(admin.ModelAdmin): + list_display = ('name_en', 'name_ar', 'phone_code') + search_fields = ('name_en', 'name_ar', 'phone_code') + class TestimonialAdmin(admin.ModelAdmin): list_display = ('name_en', 'role_en', 'is_active', 'created_at') list_filter = ('is_active', 'created_at') @@ -158,7 +162,7 @@ class TestimonialAdmin(admin.ModelAdmin): admin.site.unregister(User) admin.site.register(User, CustomUserAdmin) admin.site.register(Parcel, ParcelAdmin) -admin.site.register(Country) +admin.site.register(Country, CountryAdmin) admin.site.register(Governate) admin.site.register(City) admin.site.register(PlatformProfile, PlatformProfileAdmin) diff --git a/core/forms.py b/core/forms.py index 2500420..dfaccd8 100644 --- a/core/forms.py +++ b/core/forms.py @@ -14,7 +14,10 @@ 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_code = forms.ModelChoiceField(queryset=Country.objects.none(), label=_("Code"), required=False, widget=forms.Select(attrs={'class': 'form-control'})) phone_number = forms.CharField(max_length=20, label=_("Phone Number")) + verification_method = forms.ChoiceField(choices=[('email', _('Email')), ('whatsapp', _('WhatsApp'))], label=_("Verify via"), widget=forms.RadioSelect, initial='email') country = forms.ModelChoiceField(queryset=Country.objects.all(), required=False, label=_("Country")) @@ -36,12 +39,17 @@ class UserRegistrationForm(forms.ModelForm): lang = get_language() name_field = 'name_ar' if lang == 'ar' else 'name_en' + # Phone Code setup + self.fields['phone_code'].queryset = Country.objects.exclude(phone_code='').order_by(name_field) + self.fields['phone_code'].label_from_instance = lambda obj: f"{obj.phone_code} ({obj.name})" + self.fields['country'].queryset = Country.objects.all().order_by(name_field) # Default Country logic oman = Country.objects.filter(name_en='Oman').first() if oman: self.fields['country'].initial = oman + self.fields['phone_code'].initial = oman if 'country' in self.data: try: @@ -70,6 +78,17 @@ class UserRegistrationForm(forms.ModelForm): raise forms.ValidationError(_("Passwords don't match")) return password_confirm + def clean(self): + cleaned_data = super().clean() + phone_code = cleaned_data.get('phone_code') + phone_number = cleaned_data.get('phone_number') + + if phone_code and phone_number: + # If user didn't type the code in the phone number input, prepend it + if not phone_number.startswith(phone_code.phone_code): + cleaned_data['phone_number'] = f"{phone_code.phone_code}{phone_number}" + return cleaned_data + def save(self, commit=True): user = super().save(commit=False) user.set_password(self.cleaned_data['password']) @@ -90,7 +109,9 @@ class UserProfileForm(forms.ModelForm): last_name = forms.CharField(label=_("Last Name"), max_length=150, widget=forms.TextInput(attrs={'class': 'form-control'})) email = forms.EmailField(label=_("Email"), widget=forms.EmailInput(attrs={'class': 'form-control'})) + phone_code = forms.ModelChoiceField(queryset=Country.objects.none(), label=_("Code"), required=False, widget=forms.Select(attrs={'class': 'form-control'})) phone_number = forms.CharField(label=_("Phone Number"), max_length=20, widget=forms.TextInput(attrs={'class': 'form-control'})) + address = forms.CharField(label=_("Address"), required=False, widget=forms.TextInput(attrs={'class': 'form-control'})) profile_picture = forms.ImageField(label=_("Profile Picture"), required=False, widget=forms.FileInput(attrs={'class': 'form-control'})) @@ -125,10 +146,25 @@ class UserProfileForm(forms.ModelForm): lang = get_language() name_field = 'name_ar' if lang == 'ar' else 'name_en' - self.fields['country'].queryset = Country.objects.all().order_by(name_field) + # Phone Code setup + self.fields['phone_code'].queryset = Country.objects.exclude(phone_code='').order_by(name_field) + self.fields['phone_code'].label_from_instance = lambda obj: f"{obj.phone_code} ({obj.name})" # Default Country logic (Oman) oman = Country.objects.filter(name_en='Oman').first() + if oman: + self.fields['phone_code'].initial = oman + + # Initial splitting of phone number + if self.instance.pk and self.instance.phone_number: + for country in Country.objects.exclude(phone_code=''): + if self.instance.phone_number.startswith(country.phone_code): + self.fields['phone_code'].initial = country + # Strip code from display + self.fields['phone_number'].initial = self.instance.phone_number[len(country.phone_code):] + break + + self.fields['country'].queryset = Country.objects.all().order_by(name_field) # Initial QS setup self.fields['governate'].queryset = Governate.objects.none() @@ -154,7 +190,19 @@ class UserProfileForm(forms.ModelForm): elif self.instance.pk and self.instance.governate: self.fields['city'].queryset = self.instance.governate.city_set.order_by(name_field) + def clean(self): + cleaned_data = super().clean() + phone_code = cleaned_data.get('phone_code') + phone_number = cleaned_data.get('phone_number') + + if phone_code and phone_number: + if not phone_number.startswith(phone_code.phone_code): + cleaned_data['phone_number'] = f"{phone_code.phone_code}{phone_number}" + return cleaned_data + class ParcelForm(forms.ModelForm): + receiver_phone_code = forms.ModelChoiceField(queryset=Country.objects.none(), label=_("Receiver Code"), required=False, widget=forms.Select(attrs={'class': 'form-control'})) + class Meta: model = Parcel fields = [ @@ -202,16 +250,29 @@ class ParcelForm(forms.ModelForm): lang = get_language() name_field = 'name_ar' if lang == 'ar' else 'name_en' - # Set querysets for countries - self.fields['pickup_country'].queryset = Country.objects.all().order_by(name_field) - self.fields['delivery_country'].queryset = Country.objects.all().order_by(name_field) + # Phone Code setup + self.fields['receiver_phone_code'].queryset = Country.objects.exclude(phone_code='').order_by(name_field) + self.fields['receiver_phone_code'].label_from_instance = lambda obj: f"{obj.phone_code} ({obj.name})" # Default Country logic oman = Country.objects.filter(name_en='Oman').first() if oman: + self.fields['receiver_phone_code'].initial = oman self.fields['pickup_country'].initial = oman self.fields['delivery_country'].initial = oman + # Initial splitting of phone number (if editing) + if self.instance.pk and self.instance.receiver_phone: + for country in Country.objects.exclude(phone_code=''): + if self.instance.receiver_phone.startswith(country.phone_code): + self.fields['receiver_phone_code'].initial = country + self.fields['receiver_phone'].initial = self.instance.receiver_phone[len(country.phone_code):] + break + + # Set querysets for countries + self.fields['pickup_country'].queryset = Country.objects.all().order_by(name_field) + self.fields['delivery_country'].queryset = Country.objects.all().order_by(name_field) + # Pickup self.fields['pickup_governate'].queryset = Governate.objects.none() self.fields['pickup_city'].queryset = City.objects.none() @@ -251,3 +312,13 @@ class ParcelForm(forms.ModelForm): self.fields['delivery_city'].queryset = City.objects.filter(governate_id=gov_id).order_by(name_field) except (ValueError, TypeError): pass + + def clean(self): + cleaned_data = super().clean() + phone_code = cleaned_data.get('receiver_phone_code') + phone_number = cleaned_data.get('receiver_phone') + + if phone_code and phone_number: + if not phone_number.startswith(phone_code.phone_code): + cleaned_data['receiver_phone'] = f"{phone_code.phone_code}{phone_number}" + return cleaned_data \ No newline at end of file diff --git a/core/migrations/0016_country_phone_code.py b/core/migrations/0016_country_phone_code.py new file mode 100644 index 0000000..6fe1b2e --- /dev/null +++ b/core/migrations/0016_country_phone_code.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-01-25 17:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0015_testimonial'), + ] + + operations = [ + migrations.AddField( + model_name='country', + name='phone_code', + field=models.CharField(blank=True, help_text='e.g. +968', max_length=10, verbose_name='Phone Code'), + ), + ] diff --git a/core/migrations/__pycache__/0016_country_phone_code.cpython-311.pyc b/core/migrations/__pycache__/0016_country_phone_code.cpython-311.pyc new file mode 100644 index 0000000..7193d89 Binary files /dev/null and b/core/migrations/__pycache__/0016_country_phone_code.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index fb8a8a6..7f57570 100644 --- a/core/models.py +++ b/core/models.py @@ -11,6 +11,7 @@ import uuid class Country(models.Model): name_en = models.CharField(_('Name (English)'), max_length=100) name_ar = models.CharField(_('Name (Arabic)'), max_length=100) + phone_code = models.CharField(_('Phone Code'), max_length=10, blank=True, help_text=_("e.g. +968")) @property def name(self): @@ -19,7 +20,7 @@ class Country(models.Model): return self.name_en def __str__(self): - return self.name + return f"{self.name} ({self.phone_code})" if self.phone_code else self.name class Meta: verbose_name = _('Country') @@ -240,4 +241,4 @@ class Testimonial(models.Model): class Meta: verbose_name = _('Testimonial') verbose_name_plural = _('Testimonials') - ordering = ['-created_at'] + ordering = ['-created_at'] \ No newline at end of file diff --git a/core/templates/core/emails/base_email.html b/core/templates/core/emails/base_email.html new file mode 100644 index 0000000..28e6343 --- /dev/null +++ b/core/templates/core/emails/base_email.html @@ -0,0 +1,93 @@ +{% load i18n core_tags %} +{% get_platform_profile as platform %} + + +
+ + + + + +{% trans "Hello," %}
+ +{% trans "You are receiving this email because you requested a password reset for your account at" %} {{ site_name }}.
+ +{% trans "Please click the button below to choose a new password:" %}
+ + + +{% trans "If the button above doesn't work, verify that you entered your username correctly and try pasting this link into your browser:" %}
+ +{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
+ +{% trans "Your username is:" %} {{ user.get_username }}
+ +{% trans "If you did not request this, please ignore this email." %}
+ +{% trans "Thanks," %}
{% trans "The" %} {{ site_name }} {% trans "Team" %}
{% trans "Please login to your account" %}
+{% trans "Don't have an account?" %} {% trans "Register here" %}
-+ {% trans "Your password has been set. You may go ahead and log in now." %} +
+ + {% trans "Log In" %} + +{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}
++ {% trans "We've sent you instructions on how to reset your password. If an account exists with the email you entered, you will receive them shortly." %} +
++ {% trans "If you don't receive an email, please check your spam folder." %} +
+ + {% trans "Return to Login" %} + +{% trans "Enter your email address and we'll send you a link to reset your password." %}
+