user profile
This commit is contained in:
parent
59204ba309
commit
0bccc28caf
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -84,6 +84,75 @@ class UserRegistrationForm(forms.ModelForm):
|
|||||||
profile.save()
|
profile.save()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
class UserProfileForm(forms.ModelForm):
|
||||||
|
first_name = forms.CharField(label=_("First Name"), max_length=150, widget=forms.TextInput(attrs={'class': 'form-control'}))
|
||||||
|
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_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'}))
|
||||||
|
|
||||||
|
otp_method = forms.ChoiceField(
|
||||||
|
choices=[('email', _('Email')), ('whatsapp', _('WhatsApp'))],
|
||||||
|
label=_('Verify changes via'),
|
||||||
|
widget=forms.RadioSelect,
|
||||||
|
initial='email'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Profile
|
||||||
|
fields = ['profile_picture', 'phone_number', 'address', 'country', 'governate', 'city']
|
||||||
|
widgets = {
|
||||||
|
'country': forms.Select(attrs={'class': 'form-control'}),
|
||||||
|
'governate': forms.Select(attrs={'class': 'form-control'}),
|
||||||
|
'city': forms.Select(attrs={'class': 'form-control'}),
|
||||||
|
}
|
||||||
|
labels = {
|
||||||
|
'country': _('Country'),
|
||||||
|
'governate': _('Governate'),
|
||||||
|
'city': _('City'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if self.instance.user:
|
||||||
|
self.fields['first_name'].initial = self.instance.user.first_name
|
||||||
|
self.fields['last_name'].initial = self.instance.user.last_name
|
||||||
|
self.fields['email'].initial = self.instance.user.email
|
||||||
|
|
||||||
|
lang = get_language()
|
||||||
|
name_field = 'name_ar' if lang == 'ar' else 'name_en'
|
||||||
|
|
||||||
|
self.fields['country'].queryset = Country.objects.all().order_by(name_field)
|
||||||
|
|
||||||
|
# Default Country logic (Oman)
|
||||||
|
oman = Country.objects.filter(name_en='Oman').first()
|
||||||
|
|
||||||
|
# Initial QS setup
|
||||||
|
self.fields['governate'].queryset = Governate.objects.none()
|
||||||
|
self.fields['city'].queryset = City.objects.none()
|
||||||
|
|
||||||
|
if 'country' in self.data:
|
||||||
|
try:
|
||||||
|
country_id = int(self.data.get('country'))
|
||||||
|
self.fields['governate'].queryset = Governate.objects.filter(country_id=country_id).order_by(name_field)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
elif self.instance.pk and self.instance.country:
|
||||||
|
self.fields['governate'].queryset = self.instance.country.governate_set.order_by(name_field)
|
||||||
|
elif oman:
|
||||||
|
self.fields['governate'].queryset = Governate.objects.filter(country=oman).order_by(name_field)
|
||||||
|
|
||||||
|
if 'governate' in self.data:
|
||||||
|
try:
|
||||||
|
gov_id = int(self.data.get('governate'))
|
||||||
|
self.fields['city'].queryset = City.objects.filter(governate_id=gov_id).order_by(name_field)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
elif self.instance.pk and self.instance.governate:
|
||||||
|
self.fields['city'].queryset = self.instance.governate.city_set.order_by(name_field)
|
||||||
|
|
||||||
class ParcelForm(forms.ModelForm):
|
class ParcelForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Parcel
|
model = Parcel
|
||||||
@ -180,4 +249,4 @@ class ParcelForm(forms.ModelForm):
|
|||||||
gov_id = int(self.data.get('delivery_governate'))
|
gov_id = int(self.data.get('delivery_governate'))
|
||||||
self.fields['delivery_city'].queryset = City.objects.filter(governate_id=gov_id).order_by(name_field)
|
self.fields['delivery_city'].queryset = City.objects.filter(governate_id=gov_id).order_by(name_field)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-01-25 11:40
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0008_platformprofile_privacy_policy_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='address',
|
||||||
|
field=models.CharField(blank=True, max_length=255, verbose_name='Address'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='profile_picture',
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to='profile_pics/', verbose_name='Profile Picture'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OTPVerification',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('code', models.CharField(max_length=6)),
|
||||||
|
('purpose', models.CharField(choices=[('profile_update', 'Profile Update'), ('password_reset', 'Password Reset')], default='profile_update', max_length=20)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('is_verified', models.BooleanField(default=False)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
from django.utils import timezone
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
class Country(models.Model):
|
class Country(models.Model):
|
||||||
@ -67,6 +68,8 @@ class Profile(models.Model):
|
|||||||
user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name=_('User'))
|
user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name=_('User'))
|
||||||
role = models.CharField(_('Role'), max_length=20, choices=ROLE_CHOICES, default='shipper')
|
role = models.CharField(_('Role'), max_length=20, choices=ROLE_CHOICES, default='shipper')
|
||||||
phone_number = models.CharField(_('Phone Number'), max_length=20, blank=True)
|
phone_number = models.CharField(_('Phone Number'), max_length=20, blank=True)
|
||||||
|
profile_picture = models.ImageField(_('Profile Picture'), upload_to='profile_pics/', blank=True, null=True)
|
||||||
|
address = models.CharField(_('Address'), max_length=255, blank=True)
|
||||||
|
|
||||||
country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Country'))
|
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'))
|
governate = models.ForeignKey(Governate, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Governate'))
|
||||||
@ -162,4 +165,19 @@ class PlatformProfile(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _('Platform Profile')
|
verbose_name = _('Platform Profile')
|
||||||
verbose_name_plural = _('Platform Profile')
|
verbose_name_plural = _('Platform Profile')
|
||||||
|
|
||||||
|
class OTPVerification(models.Model):
|
||||||
|
PURPOSE_CHOICES = (
|
||||||
|
('profile_update', _('Profile Update')),
|
||||||
|
('password_reset', _('Password Reset')),
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
code = models.CharField(max_length=6)
|
||||||
|
purpose = models.CharField(max_length=20, choices=PURPOSE_CHOICES, default='profile_update')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
is_verified = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def is_valid(self):
|
||||||
|
# OTP valid for 10 minutes
|
||||||
|
return self.created_at >= timezone.now() - timezone.timedelta(minutes=10)
|
||||||
|
|||||||
@ -87,7 +87,7 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<span class="nav-link text-white-50">{% trans "Hello" %}, {{ user.username }}</span>
|
<a href="{% url 'profile' %}" class="nav-link text-white-50">{% trans "Hello" %}, {{ user.username }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<form action="{% url 'logout' %}" method="post" class="d-inline">
|
<form action="{% url 'logout' %}" method="post" class="d-inline">
|
||||||
|
|||||||
94
core/templates/core/edit_profile.html
Normal file
94
core/templates/core/edit_profile.html
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Edit Profile" %} | masarX{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="py-5 bg-light" style="min-height: 80vh;">
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card shadow-sm border-0">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<h2 class="fw-bold mb-4 text-center">{% trans "Edit Profile" %}</h2>
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||||
|
{{ field }}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<div class="form-text small">{{ field.help_text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="text-danger small">{{ field.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="d-flex gap-2 mt-4">
|
||||||
|
<a href="{% url 'profile' %}" class="btn btn-outline-secondary w-50 py-2">{% trans "Cancel" %}</a>
|
||||||
|
<button type="submit" class="btn btn-masarx-primary w-50 py-2">{% trans "Save & Verify" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const countrySelect = document.getElementById('id_country');
|
||||||
|
const governateSelect = document.getElementById('id_governate');
|
||||||
|
const citySelect = document.getElementById('id_city');
|
||||||
|
|
||||||
|
countrySelect.addEventListener('change', function() {
|
||||||
|
const countryId = this.value;
|
||||||
|
governateSelect.innerHTML = '<option value="">{% trans "Select Governate" %}</option>';
|
||||||
|
citySelect.innerHTML = '<option value="">{% trans "Select City" %}</option>';
|
||||||
|
|
||||||
|
if (countryId) {
|
||||||
|
fetch(`{% url 'get_governates' %}?country_id=${countryId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
data.forEach(gov => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = gov.id;
|
||||||
|
option.textContent = gov.name;
|
||||||
|
governateSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
governateSelect.addEventListener('change', function() {
|
||||||
|
const governateId = this.value;
|
||||||
|
citySelect.innerHTML = '<option value="">{% trans "Select City" %}</option>';
|
||||||
|
|
||||||
|
if (governateId) {
|
||||||
|
fetch(`{% url 'get_cities' %}?governate_id=${governateId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
data.forEach(city => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = city.id;
|
||||||
|
option.textContent = city.name;
|
||||||
|
citySelect.appendChild(option);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-masarx-primary {
|
||||||
|
background-color: var(--accent-orange);
|
||||||
|
border-color: var(--accent-orange);
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
71
core/templates/core/profile.html
Normal file
71
core/templates/core/profile.html
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n static %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "My Profile" %} | masarX{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="py-5 bg-light" style="min-height: 80vh;">
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card shadow-sm border-0">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="fw-bold mb-0">{% trans "My Profile" %}</h2>
|
||||||
|
<a href="{% url 'edit_profile' %}" class="btn btn-masarx-primary">
|
||||||
|
{% trans "Edit Profile" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mb-5">
|
||||||
|
{% if profile.profile_picture %}
|
||||||
|
<img src="{{ profile.profile_picture.url }}" alt="Profile" class="rounded-circle img-thumbnail" style="width: 150px; height: 150px; object-fit: cover;">
|
||||||
|
{% else %}
|
||||||
|
<div class="rounded-circle bg-secondary d-flex align-items-center justify-content-center mx-auto" style="width: 150px; height: 150px;">
|
||||||
|
<span class="text-white fs-1">{{ profile.user.first_name|first|upper }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<h3 class="mt-3">{{ profile.user.get_full_name }}</h3>
|
||||||
|
<p class="text-muted">{{ profile.get_role_display }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="text-muted small text-uppercase">{% trans "Email" %}</label>
|
||||||
|
<p class="fw-semibold">{{ profile.user.email }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="text-muted small text-uppercase">{% trans "Phone" %}</label>
|
||||||
|
<p class="fw-semibold">{{ profile.phone_number|default:"-" }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="text-muted small text-uppercase">{% trans "Location" %}</label>
|
||||||
|
<p class="fw-semibold">
|
||||||
|
{{ profile.city.name|default:"" }}
|
||||||
|
{% if profile.city and profile.governate %}, {% endif %}
|
||||||
|
{{ profile.governate.name|default:"" }}
|
||||||
|
{% if profile.governate and profile.country %}, {% endif %}
|
||||||
|
{{ profile.country.name|default:"" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="text-muted small text-uppercase">{% trans "Address" %}</label>
|
||||||
|
<p class="fw-semibold">{{ profile.address|default:"-" }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<style>
|
||||||
|
.btn-masarx-primary {
|
||||||
|
background-color: var(--accent-orange);
|
||||||
|
border-color: var(--accent-orange);
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
44
core/templates/core/verify_otp.html
Normal file
44
core/templates/core/verify_otp.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Verify Profile Update" %} | masarX{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="py-5 bg-light" style="min-height: 80vh;">
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow-sm border-0">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<h2 class="fw-bold mb-4 text-center">{% trans "Verification Required" %}</h2>
|
||||||
|
<p class="text-center text-muted mb-4">
|
||||||
|
{% trans "We have sent a verification code to your selected contact method. Please enter it below to save your changes." %}
|
||||||
|
</p>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-semibold" for="code">{% trans "Verification Code" %}</label>
|
||||||
|
<input type="text" name="code" id="code" class="form-control text-center fs-3 tracking-widest" maxlength="6" required placeholder="000000">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-masarx-primary w-100 py-2">{% trans "Verify & Save" %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-masarx-primary {
|
||||||
|
background-color: var(--accent-orange);
|
||||||
|
border-color: var(--accent-orange);
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.tracking-widest {
|
||||||
|
letter-spacing: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
@ -21,4 +21,8 @@ urlpatterns = [
|
|||||||
path('privacy-policy/', views.privacy_policy, name='privacy_policy'),
|
path('privacy-policy/', views.privacy_policy, name='privacy_policy'),
|
||||||
path('terms-conditions/', views.terms_conditions, name='terms_conditions'),
|
path('terms-conditions/', views.terms_conditions, name='terms_conditions'),
|
||||||
path('contact/', views.contact_view, name='contact'),
|
path('contact/', views.contact_view, name='contact'),
|
||||||
]
|
|
||||||
|
path('profile/', views.profile_view, name='profile'),
|
||||||
|
path('profile/edit/', views.edit_profile_view, name='edit_profile'),
|
||||||
|
path('profile/verify-otp/', views.verify_otp_view, name='verify_otp'),
|
||||||
|
]
|
||||||
|
|||||||
120
core/views.py
120
core/views.py
@ -2,8 +2,8 @@ from django.shortcuts import render, redirect, get_object_or_404
|
|||||||
from django.contrib.auth import login, authenticate, logout
|
from django.contrib.auth import login, authenticate, logout
|
||||||
from django.contrib.auth.forms import AuthenticationForm
|
from django.contrib.auth.forms import AuthenticationForm
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from .models import Parcel, Profile, Country, Governate, City
|
from .models import Parcel, Profile, Country, Governate, City, OTPVerification
|
||||||
from .forms import UserRegistrationForm, ParcelForm, ContactForm
|
from .forms import UserRegistrationForm, ParcelForm, ContactForm, UserProfileForm
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@ -11,11 +11,15 @@ from django.http import JsonResponse
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from .payment_utils import ThawaniPay
|
from .payment_utils import ThawaniPay
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.mail import send_mail
|
||||||
|
import random
|
||||||
|
import string
|
||||||
from .whatsapp_utils import (
|
from .whatsapp_utils import (
|
||||||
notify_shipment_created,
|
notify_shipment_created,
|
||||||
notify_payment_received,
|
notify_payment_received,
|
||||||
notify_driver_assigned,
|
notify_driver_assigned,
|
||||||
notify_status_change
|
notify_status_change,
|
||||||
|
send_whatsapp_message
|
||||||
)
|
)
|
||||||
from .mail import send_contact_message
|
from .mail import send_contact_message
|
||||||
|
|
||||||
@ -207,4 +211,112 @@ def contact_view(request):
|
|||||||
return redirect('contact')
|
return redirect('contact')
|
||||||
else:
|
else:
|
||||||
form = ContactForm()
|
form = ContactForm()
|
||||||
return render(request, 'core/contact.html', {'form': form})
|
return render(request, 'core/contact.html', {'form': form})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def profile_view(request):
|
||||||
|
return render(request, 'core/profile.html', {'profile': request.user.profile})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def edit_profile_view(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = UserProfileForm(request.POST, request.FILES, instance=request.user.profile)
|
||||||
|
if form.is_valid():
|
||||||
|
# 1. Handle Image immediately (easier than session storage)
|
||||||
|
if 'profile_picture' in request.FILES:
|
||||||
|
request.user.profile.profile_picture = request.FILES['profile_picture']
|
||||||
|
request.user.profile.save()
|
||||||
|
|
||||||
|
# 2. Store other data in session for verification
|
||||||
|
data = form.cleaned_data
|
||||||
|
# Remove objects that can't be serialized or we've already handled
|
||||||
|
safe_data = {
|
||||||
|
'first_name': data['first_name'],
|
||||||
|
'last_name': data['last_name'],
|
||||||
|
'email': data['email'],
|
||||||
|
'phone_number': data['phone_number'],
|
||||||
|
'address': data['address'],
|
||||||
|
'country_id': data['country'].id if data['country'] else None,
|
||||||
|
'governate_id': data['governate'].id if data['governate'] else None,
|
||||||
|
'city_id': data['city'].id if data['city'] else None,
|
||||||
|
}
|
||||||
|
request.session['pending_profile_update'] = safe_data
|
||||||
|
|
||||||
|
# 3. Generate OTP
|
||||||
|
code = ''.join(random.choices(string.digits, k=6))
|
||||||
|
OTPVerification.objects.create(user=request.user, code=code, purpose='profile_update')
|
||||||
|
|
||||||
|
# 4. Send OTP
|
||||||
|
method = data.get('otp_method', 'email')
|
||||||
|
if method == 'whatsapp':
|
||||||
|
# Use current phone if available, else new phone
|
||||||
|
phone = request.user.profile.phone_number or data['phone_number']
|
||||||
|
send_whatsapp_message(phone, f"Your verification code is: {code}")
|
||||||
|
messages.info(request, _("Verification code sent to WhatsApp."))
|
||||||
|
else:
|
||||||
|
# Default to email
|
||||||
|
send_mail(
|
||||||
|
_('Verification Code'),
|
||||||
|
f'Your verification code is: {code}',
|
||||||
|
settings.DEFAULT_FROM_EMAIL,
|
||||||
|
[request.user.email],
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
messages.info(request, _("Verification code sent to email."))
|
||||||
|
|
||||||
|
return redirect('verify_otp')
|
||||||
|
else:
|
||||||
|
form = UserProfileForm(instance=request.user.profile)
|
||||||
|
|
||||||
|
return render(request, 'core/edit_profile.html', {'form': form})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def verify_otp_view(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
code = request.POST.get('code')
|
||||||
|
try:
|
||||||
|
otp = OTPVerification.objects.filter(
|
||||||
|
user=request.user,
|
||||||
|
code=code,
|
||||||
|
purpose='profile_update',
|
||||||
|
is_verified=False
|
||||||
|
).latest('created_at')
|
||||||
|
|
||||||
|
if otp.is_valid():
|
||||||
|
# Apply changes
|
||||||
|
data = request.session.get('pending_profile_update')
|
||||||
|
if data:
|
||||||
|
# Update User
|
||||||
|
request.user.first_name = data['first_name']
|
||||||
|
request.user.last_name = data['last_name']
|
||||||
|
request.user.email = data['email']
|
||||||
|
request.user.save()
|
||||||
|
|
||||||
|
# Update Profile
|
||||||
|
profile = request.user.profile
|
||||||
|
profile.phone_number = data['phone_number']
|
||||||
|
profile.address = data['address']
|
||||||
|
if data.get('country_id'):
|
||||||
|
profile.country_id = data['country_id']
|
||||||
|
if data.get('governate_id'):
|
||||||
|
profile.governate_id = data['governate_id']
|
||||||
|
if data.get('city_id'):
|
||||||
|
profile.city_id = data['city_id']
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
otp.is_verified = True
|
||||||
|
otp.save()
|
||||||
|
del request.session['pending_profile_update']
|
||||||
|
|
||||||
|
messages.success(request, _("Profile updated successfully!"))
|
||||||
|
return redirect('profile')
|
||||||
|
else:
|
||||||
|
messages.error(request, _("Session expired. Please try again."))
|
||||||
|
return redirect('edit_profile')
|
||||||
|
else:
|
||||||
|
messages.error(request, _("Invalid or expired code."))
|
||||||
|
except OTPVerification.DoesNotExist:
|
||||||
|
messages.error(request, _("Invalid code."))
|
||||||
|
|
||||||
|
return render(request, 'core/verify_otp.html')
|
||||||
Binary file not shown.
@ -688,4 +688,72 @@ msgstr "تم إرسال رسالتك بنجاح!"
|
|||||||
|
|
||||||
#: core/views.py:206
|
#: core/views.py:206
|
||||||
msgid "There was an error sending your message. Please try again later."
|
msgid "There was an error sending your message. Please try again later."
|
||||||
msgstr "حدث خطأ أثناء إرسال رسالتك. يرجى المحاولة مرة أخرى لاحقاً."
|
msgstr "حدث خطأ أثناء إرسال رسالتك. يرجى المحاولة مرة أخرى لاحقاً."
|
||||||
|
|
||||||
|
#: core/forms.py:new
|
||||||
|
msgid "Profile Picture"
|
||||||
|
msgstr "الصورة الشخصية"
|
||||||
|
|
||||||
|
#: core/forms.py:new
|
||||||
|
msgid "Verify changes via"
|
||||||
|
msgstr "التحقق من التغييرات عبر"
|
||||||
|
|
||||||
|
#: core/templates/core/profile.html:new
|
||||||
|
msgid "My Profile"
|
||||||
|
msgstr "ملفي الشخصي"
|
||||||
|
|
||||||
|
#: core/templates/core/profile.html:new
|
||||||
|
msgid "Edit Profile"
|
||||||
|
msgstr "تعديل الملف الشخصي"
|
||||||
|
|
||||||
|
#: core/templates/core/edit_profile.html:new
|
||||||
|
msgid "Save & Verify"
|
||||||
|
msgstr "حفظ وتحقق"
|
||||||
|
|
||||||
|
#: core/templates/core/verify_otp.html:new
|
||||||
|
msgid "Verification Required"
|
||||||
|
msgstr "التحقق مطلوب"
|
||||||
|
|
||||||
|
#: core/templates/core/verify_otp.html:new
|
||||||
|
msgid "We have sent a verification code to your selected contact method. Please enter it below to save your changes."
|
||||||
|
msgstr "لقد أرسلنا رمز التحقق إلى وسيلة الاتصال المحددة. يرجى إدخاله أدناه لحفظ تغييراتك."
|
||||||
|
|
||||||
|
#: core/templates/core/verify_otp.html:new
|
||||||
|
msgid "Verification Code"
|
||||||
|
msgstr "رمز التحقق"
|
||||||
|
|
||||||
|
#: core/templates/core/verify_otp.html:new
|
||||||
|
msgid "Verify & Save"
|
||||||
|
msgstr "تحقق وحفظ"
|
||||||
|
|
||||||
|
#: core/views.py:new
|
||||||
|
msgid "Profile updated successfully!"
|
||||||
|
msgstr "تم تحديث الملف الشخصي بنجاح!"
|
||||||
|
|
||||||
|
#: core/views.py:new
|
||||||
|
msgid "Invalid or expired code."
|
||||||
|
msgstr "الرمز غير صالح أو منتهي الصلاحية."
|
||||||
|
|
||||||
|
#: core/views.py:new
|
||||||
|
msgid "Verification code sent to WhatsApp."
|
||||||
|
msgstr "تم إرسال رمز التحقق إلى واتساب."
|
||||||
|
|
||||||
|
#: core/views.py:new
|
||||||
|
msgid "Verification code sent to email."
|
||||||
|
msgstr "تم إرسال رمز التحقق إلى البريد الإلكتروني."
|
||||||
|
|
||||||
|
#: core/views.py:new
|
||||||
|
msgid "Session expired. Please try again."
|
||||||
|
msgstr "انتهت صلاحية الجلسة. يرجى المحاولة مرة أخرى."
|
||||||
|
|
||||||
|
#: core/views.py:new
|
||||||
|
msgid "Invalid code."
|
||||||
|
msgstr "رمز غير صالح."
|
||||||
|
|
||||||
|
#: core/models.py:new
|
||||||
|
msgid "Profile Update"
|
||||||
|
msgstr "تحديث الملف الشخصي"
|
||||||
|
|
||||||
|
#: core/models.py:new
|
||||||
|
msgid "Password Reset"
|
||||||
|
msgstr "إعادة تعيين كلمة المرور"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user