adding user profile
This commit is contained in:
parent
efa4c54412
commit
7bc32398ed
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -215,3 +215,44 @@ class CustomLoginForm(AuthenticationForm):
|
|||||||
if not isinstance(field.widget, (forms.RadioSelect, forms.CheckboxInput)):
|
if not isinstance(field.widget, (forms.RadioSelect, forms.CheckboxInput)):
|
||||||
field.widget.attrs.update({"class": "form-control"})
|
field.widget.attrs.update({"class": "form-control"})
|
||||||
|
|
||||||
|
class ProfileForm(forms.ModelForm):
|
||||||
|
first_name = forms.CharField(max_length=150, required=False, widget=forms.TextInput(attrs={'class': 'form-control'}))
|
||||||
|
last_name = forms.CharField(max_length=150, required=False, widget=forms.TextInput(attrs={'class': 'form-control'}))
|
||||||
|
email = forms.EmailField(required=False, widget=forms.EmailInput(attrs={'class': 'form-control'}))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Profile
|
||||||
|
fields = ['profile_picture', 'phone_number', 'country_code']
|
||||||
|
widgets = {
|
||||||
|
'profile_picture': forms.FileInput(attrs={'class': 'form-control'}),
|
||||||
|
'phone_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'country_code': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
user = kwargs.pop('user', None)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Dynamic country codes from the database
|
||||||
|
countries = Country.objects.all()
|
||||||
|
if countries.exists():
|
||||||
|
self.fields['country_code'].widget = forms.Select(
|
||||||
|
attrs={'class': 'form-select'},
|
||||||
|
choices=[(c.code, str(c)) for c in countries]
|
||||||
|
)
|
||||||
|
|
||||||
|
if user:
|
||||||
|
self.fields['first_name'].initial = user.first_name
|
||||||
|
self.fields['last_name'].initial = user.last_name
|
||||||
|
self.fields['email'].initial = user.email
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
profile = super().save(commit=False)
|
||||||
|
user = profile.user
|
||||||
|
user.first_name = self.cleaned_data['first_name']
|
||||||
|
user.last_name = self.cleaned_data['last_name']
|
||||||
|
user.email = self.cleaned_data['email']
|
||||||
|
if commit:
|
||||||
|
user.save()
|
||||||
|
profile.save()
|
||||||
|
return profile
|
||||||
18
core/migrations/0028_profile_profile_picture.py
Normal file
18
core/migrations/0028_profile_profile_picture.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-01-25 03:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0027_otpcode_email_alter_otpcode_phone_number'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='profile_picture',
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to='profiles/', verbose_name='Profile Picture'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
@ -65,6 +65,10 @@ class Profile(models.Model):
|
|||||||
subscription_plan = models.CharField(max_length=20, choices=SUBSCRIPTION_CHOICES, default='NONE')
|
subscription_plan = models.CharField(max_length=20, choices=SUBSCRIPTION_CHOICES, default='NONE')
|
||||||
subscription_expiry = models.DateField(null=True, blank=True)
|
subscription_expiry = models.DateField(null=True, blank=True)
|
||||||
is_subscription_active = models.BooleanField(default=False)
|
is_subscription_active = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# New Profile Picture field
|
||||||
|
profile_picture = models.ImageField(_('Profile Picture'), upload_to='profiles/', blank=True, null=True)
|
||||||
|
|
||||||
def is_expired(self):
|
def is_expired(self):
|
||||||
if self.subscription_plan == "NONE":
|
if self.subscription_plan == "NONE":
|
||||||
return False
|
return False
|
||||||
@ -73,10 +77,7 @@ class Profile(models.Model):
|
|||||||
if not self.subscription_expiry:
|
if not self.subscription_expiry:
|
||||||
return True
|
return True
|
||||||
return self.subscription_expiry < timezone.now().date()
|
return self.subscription_expiry < timezone.now().date()
|
||||||
if not self.subscription_expiry:
|
|
||||||
return 0
|
|
||||||
delta = self.subscription_expiry - timezone.now().date()
|
|
||||||
return delta.days
|
|
||||||
country_code = models.CharField(max_length=5, blank=True, default="966")
|
country_code = models.CharField(max_length=5, blank=True, default="966")
|
||||||
phone_number = models.CharField(max_length=20, unique=True, null=True) # Changed to unique and nullable for migration safety
|
phone_number = models.CharField(max_length=20, unique=True, null=True) # Changed to unique and nullable for migration safety
|
||||||
|
|
||||||
@ -91,6 +92,40 @@ class Profile(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user.username} - {self.role}"
|
return f"{self.user.username} - {self.role}"
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.profile_picture:
|
||||||
|
self.profile_picture = self.compress_image(self.profile_picture)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def compress_image(self, image_field):
|
||||||
|
if not image_field:
|
||||||
|
return image_field
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check file extension
|
||||||
|
ext = os.path.splitext(image_field.name)[1].lower()
|
||||||
|
if ext not in ['.jpg', '.jpeg', '.png', '.webp']:
|
||||||
|
return image_field
|
||||||
|
|
||||||
|
img = Image.open(image_field)
|
||||||
|
|
||||||
|
if img.mode != 'RGB':
|
||||||
|
img = img.convert('RGB')
|
||||||
|
|
||||||
|
# Resize if too large
|
||||||
|
max_size = (500, 500)
|
||||||
|
img.thumbnail(max_size, Image.LANCZOS)
|
||||||
|
|
||||||
|
output = BytesIO()
|
||||||
|
img.save(output, format='JPEG', quality=80, optimize=True)
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
new_name = os.path.splitext(image_field.name)[0] + '.jpg'
|
||||||
|
return File(output, name=new_name)
|
||||||
|
except Exception as e:
|
||||||
|
# Not an image or other error, return as is
|
||||||
|
return image_field
|
||||||
|
|
||||||
class OTPCode(models.Model):
|
class OTPCode(models.Model):
|
||||||
phone_number = models.CharField(max_length=20, null=True, blank=True)
|
phone_number = models.CharField(max_length=20, null=True, blank=True)
|
||||||
@ -531,4 +566,4 @@ class Testimonial(models.Model):
|
|||||||
def display_content(self):
|
def display_content(self):
|
||||||
if get_language() == 'ar' and self.content_ar:
|
if get_language() == 'ar' and self.content_ar:
|
||||||
return self.content_ar
|
return self.content_ar
|
||||||
return self.content
|
return self.content
|
||||||
|
|||||||
@ -37,6 +37,7 @@
|
|||||||
.breadcrumb-container { background: #fff; border-bottom: 1px solid #eee; padding: 0.5rem 0; margin-bottom: 0; }
|
.breadcrumb-container { background: #fff; border-bottom: 1px solid #eee; padding: 0.5rem 0; margin-bottom: 0; }
|
||||||
.breadcrumb-item a { text-decoration: none; color: #6c757d; }
|
.breadcrumb-item a { text-decoration: none; color: #6c757d; }
|
||||||
.breadcrumb-item.active { color: #2196F3; font-weight: 600; }
|
.breadcrumb-item.active { color: #2196F3; font-weight: 600; }
|
||||||
|
.nav-profile-pic { width: 30px; height: 30px; object-fit: cover; border-radius: 50%; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@ -99,19 +100,34 @@
|
|||||||
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<li class="nav-item dropdown ms-lg-3">
|
<li class="nav-item dropdown ms-lg-3">
|
||||||
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown">
|
<a class="nav-link dropdown-toggle d-flex align-items-center" href="#" data-bs-toggle="dropdown">
|
||||||
<i class="fa-solid fa-user-circle me-1"></i> {{ user.username }}
|
{% if user.profile.profile_picture %}
|
||||||
|
<img src="{{ user.profile.profile_picture.url }}" alt="{{ user.username }}" class="nav-profile-pic me-2">
|
||||||
|
{% else %}
|
||||||
|
<i class="fa-solid fa-user-circle me-1"></i>
|
||||||
|
{% endif %}
|
||||||
|
{{ user.username }}
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{% url 'profile' %}">
|
||||||
|
<i class="fa-solid fa-user-gear me-2"></i>{% trans "My Profile" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'admin:index' %}">{% trans "Admin Panel" %}</a>
|
<a class="dropdown-item" href="{% url 'admin:index' %}">
|
||||||
|
<i class="fa-solid fa-lock me-2"></i>{% trans "Admin Panel" %}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li>
|
<li>
|
||||||
<form action="{% url 'logout' %}" method="post">
|
<form action="{% url 'logout' %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button type="submit" class="dropdown-item">{% trans "Logout" %}</button>
|
<button type="submit" class="dropdown-item">
|
||||||
|
<i class="fa-solid fa-right-from-bracket me-2"></i>{% trans "Logout" %}
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
137
core/templates/core/profile.html
Normal file
137
core/templates/core/profile.html
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "My Profile" %} - {{ app_settings.app_name|default:"MASAR CARGO" }}{% endblock %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<div class="breadcrumb-container">
|
||||||
|
<div class="container">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb mb-0">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'home' %}">{% trans "Home" %}</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">{% trans "My Profile" %}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container my-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card shadow-sm border-0">
|
||||||
|
<div class="card-header bg-primary text-white py-3">
|
||||||
|
<h4 class="mb-0"><i class="fa-solid fa-user-edit me-2"></i>{% trans "Edit Profile" %}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
{% if profile.profile_picture %}
|
||||||
|
<img src="{{ profile.profile_picture.url }}" alt="{{ user.username }}" class="rounded-circle img-thumbnail mb-3" style="width: 150px; height: 150px; object-fit: cover;">
|
||||||
|
{% else %}
|
||||||
|
<div class="rounded-circle bg-light d-inline-flex align-items-center justify-content-center mb-3" style="width: 150px; height: 150px; border: 2px dashed #ddd;">
|
||||||
|
<i class="fa-solid fa-user fa-4x text-muted"></i>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<p class="text-muted small">{% trans "Update your profile picture" %}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-bold">{% trans "First Name" %}</label>
|
||||||
|
{{ form.first_name }}
|
||||||
|
{% if form.first_name.errors %}
|
||||||
|
<div class="text-danger small">{{ form.first_name.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-bold">{% trans "Last Name" %}</label>
|
||||||
|
{{ form.last_name }}
|
||||||
|
{% if form.last_name.errors %}
|
||||||
|
<div class="text-danger small">{{ form.last_name.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold">{% trans "Email" %}</label>
|
||||||
|
{{ form.email }}
|
||||||
|
{% if form.email.errors %}
|
||||||
|
<div class="text-danger small">{{ form.email.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label fw-bold">{% trans "Country Code" %}</label>
|
||||||
|
{{ form.country_code }}
|
||||||
|
{% if form.country_code.errors %}
|
||||||
|
<div class="text-danger small">{{ form.country_code.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8 mb-3">
|
||||||
|
<label class="form-label fw-bold">{% trans "Phone Number" %}</label>
|
||||||
|
{{ form.phone_number }}
|
||||||
|
{% if form.phone_number.errors %}
|
||||||
|
<div class="text-danger small">{{ form.phone_number.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-bold">{% trans "Profile Picture" %}</label>
|
||||||
|
{{ form.profile_picture }}
|
||||||
|
{% if form.profile_picture.errors %}
|
||||||
|
<div class="text-danger small">{{ form.profile_picture.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<a href="{% url 'dashboard' %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fa-solid fa-arrow-left me-1"></i> {% trans "Back to Dashboard" %}
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary px-4">
|
||||||
|
<i class="fa-solid fa-save me-1"></i> {% trans "Save Changes" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-4 shadow-sm border-0">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="fw-bold mb-3">{% trans "Account Information" %}</h5>
|
||||||
|
<div class="row small">
|
||||||
|
<div class="col-sm-4 text-muted">{% trans "Username" %}</div>
|
||||||
|
<div class="col-sm-8 mb-2">{{ user.username }}</div>
|
||||||
|
|
||||||
|
<div class="col-sm-4 text-muted">{% trans "Role" %}</div>
|
||||||
|
<div class="col-sm-8 mb-2">
|
||||||
|
{% if profile.role == 'SHIPPER' %}
|
||||||
|
<span class="badge bg-info">{% trans "Shipper" %}</span>
|
||||||
|
{% elif profile.role == 'TRUCK_OWNER' %}
|
||||||
|
<span class="badge bg-success">{% trans "Truck Owner" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">{{ profile.role }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-4 text-muted">{% trans "Subscription" %}</div>
|
||||||
|
<div class="col-sm-8 mb-2">
|
||||||
|
{{ profile.get_subscription_plan_display }}
|
||||||
|
{% if profile.subscription_expiry %}
|
||||||
|
<br><small class="text-muted">{% trans "Expires on:" %} {{ profile.subscription_expiry }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@ -10,6 +10,7 @@ urlpatterns = [
|
|||||||
path("verify-otp-login/", views.verify_otp_login, name="verify_otp_login"),
|
path("verify-otp-login/", views.verify_otp_login, name="verify_otp_login"),
|
||||||
path("logout/", auth_views.LogoutView.as_view(), name="logout"),
|
path("logout/", auth_views.LogoutView.as_view(), name="logout"),
|
||||||
path("dashboard/", views.dashboard, name="dashboard"),
|
path("dashboard/", views.dashboard, name="dashboard"),
|
||||||
|
path("profile/", views.profile_view, name="profile"),
|
||||||
path("truck/register/", views.truck_register, name="truck_register"),
|
path("truck/register/", views.truck_register, name="truck_register"),
|
||||||
path("truck/<int:truck_id>/edit/", views.edit_truck, name="edit_truck"),
|
path("truck/<int:truck_id>/edit/", views.edit_truck, name="edit_truck"),
|
||||||
path("truck/<int:truck_id>/approve/", views.approve_truck, name="approve_truck"),
|
path("truck/<int:truck_id>/approve/", views.approve_truck, name="approve_truck"),
|
||||||
|
|||||||
@ -13,7 +13,7 @@ from .forms import (
|
|||||||
CustomLoginForm,
|
CustomLoginForm,
|
||||||
TruckForm, ShipmentForm, BidForm, UserRegistrationForm,
|
TruckForm, ShipmentForm, BidForm, UserRegistrationForm,
|
||||||
OTPVerifyForm, ShipperOfferForm, RenewSubscriptionForm, AppSettingForm,
|
OTPVerifyForm, ShipperOfferForm, RenewSubscriptionForm, AppSettingForm,
|
||||||
ContactForm
|
ContactForm, ProfileForm
|
||||||
)
|
)
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
@ -239,6 +239,20 @@ def verify_otp_login(request):
|
|||||||
|
|
||||||
return render(request, 'registration/verify_otp.html', {'form': form, 'purpose': 'login', 'otp_method': otp_method})
|
return render(request, 'registration/verify_otp.html', {'form': form, 'purpose': 'login', 'otp_method': otp_method})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def profile_view(request):
|
||||||
|
profile = request.user.profile
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = ProfileForm(request.POST, request.FILES, instance=profile, user=request.user)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
messages.success(request, _("Profile updated successfully!"))
|
||||||
|
return redirect('profile')
|
||||||
|
else:
|
||||||
|
form = ProfileForm(instance=profile, user=request.user)
|
||||||
|
|
||||||
|
return render(request, 'core/profile.html', {'form': form, 'profile': profile})
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def dashboard(request):
|
def dashboard(request):
|
||||||
profile, created = Profile.objects.get_or_create(user=request.user)
|
profile, created = Profile.objects.get_or_create(user=request.user)
|
||||||
@ -863,4 +877,4 @@ def chat_api(request):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Chat API Error: {str(e)}")
|
logger.error(f"Chat API Error: {str(e)}")
|
||||||
return JsonResponse({'success': False, 'error': str(e)})
|
return JsonResponse({'success': False, 'error': str(e)})
|
||||||
Binary file not shown.
@ -2126,3 +2126,31 @@ msgstr "لقد أرسلنا رمز تحقق إلى بريدك الإلكترون
|
|||||||
|
|
||||||
msgid "We have sent a verification code to your email address. Please enter it below to log in."
|
msgid "We have sent a verification code to your email address. Please enter it below to log in."
|
||||||
msgstr "لقد أرسلنا رمز تحقق إلى بريدك الإلكتروني. يرجى إدخاله أدناه لتسجيل الدخول."
|
msgstr "لقد أرسلنا رمز تحقق إلى بريدك الإلكتروني. يرجى إدخاله أدناه لتسجيل الدخول."
|
||||||
|
|
||||||
|
msgid "My Profile"
|
||||||
|
msgstr "ملفي الشخصي"
|
||||||
|
|
||||||
|
msgid "Edit Profile"
|
||||||
|
msgstr "تعديل الملف الشخصي"
|
||||||
|
|
||||||
|
msgid "Update your profile picture"
|
||||||
|
msgstr "تحديث صورة الملف الشخصي"
|
||||||
|
|
||||||
|
msgid "Profile Picture"
|
||||||
|
msgstr "صورة الملف الشخصي"
|
||||||
|
|
||||||
|
msgid "Save Changes"
|
||||||
|
msgstr "حفظ التغييرات"
|
||||||
|
|
||||||
|
msgid "Account Information"
|
||||||
|
msgstr "معلومات الحساب"
|
||||||
|
|
||||||
|
msgid "Expires on:"
|
||||||
|
msgstr "تنتهي في:"
|
||||||
|
|
||||||
|
msgid "First Name"
|
||||||
|
msgstr "الاسم الأول"
|
||||||
|
|
||||||
|
msgid "Last Name"
|
||||||
|
msgstr "اسم العائلة"
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user