changing profile
This commit is contained in:
parent
7bc32398ed
commit
b4a62485fb
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -222,11 +222,13 @@ class ProfileForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = ['profile_picture', 'phone_number', 'country_code']
|
||||
fields = ['profile_picture', 'phone_number', 'country_code', 'email_verified', 'phone_verified']
|
||||
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'}),
|
||||
'email_verified': forms.HiddenInput(),
|
||||
'phone_verified': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -246,6 +248,15 @@ class ProfileForm(forms.ModelForm):
|
||||
self.fields['last_name'].initial = user.last_name
|
||||
self.fields['email'].initial = user.email
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
email_verified = cleaned_data.get('email_verified')
|
||||
phone_verified = cleaned_data.get('phone_verified')
|
||||
|
||||
if not email_verified and not phone_verified:
|
||||
raise forms.ValidationError(_("At least one contact method (Email or Phone) must be verified."))
|
||||
return cleaned_data
|
||||
|
||||
def save(self, commit=True):
|
||||
profile = super().save(commit=False)
|
||||
user = profile.user
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-25 03:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0028_profile_profile_picture'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='email_verified',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='phone_verified',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -68,6 +68,8 @@ class Profile(models.Model):
|
||||
|
||||
# New Profile Picture field
|
||||
profile_picture = models.ImageField(_('Profile Picture'), upload_to='profiles/', blank=True, null=True)
|
||||
email_verified = models.BooleanField(default=False)
|
||||
phone_verified = models.BooleanField(default=False)
|
||||
|
||||
def is_expired(self):
|
||||
if self.subscription_plan == "NONE":
|
||||
|
||||
@ -25,8 +25,10 @@
|
||||
<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">
|
||||
<form method="post" enctype="multipart/form-data" id="profileForm">
|
||||
{% csrf_token %}
|
||||
{{ form.email_verified }}
|
||||
{{ form.phone_verified }}
|
||||
|
||||
<div class="text-center mb-4">
|
||||
{% if profile.profile_picture %}
|
||||
@ -58,7 +60,18 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">{% trans "Email" %}</label>
|
||||
{{ form.email }}
|
||||
<div class="input-group">
|
||||
{{ form.email }}
|
||||
<button class="btn btn-outline-secondary" type="button" id="verifyEmailBtn">
|
||||
<span id="emailStatus">
|
||||
{% if profile.email_verified %}
|
||||
<i class="fa-solid fa-check-circle text-success"></i> {% trans "Verified" %}
|
||||
{% else %}
|
||||
{% trans "Verify" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{% if form.email.errors %}
|
||||
<div class="text-danger small">{{ form.email.errors }}</div>
|
||||
{% endif %}
|
||||
@ -74,7 +87,18 @@
|
||||
</div>
|
||||
<div class="col-md-8 mb-3">
|
||||
<label class="form-label fw-bold">{% trans "Phone Number" %}</label>
|
||||
{{ form.phone_number }}
|
||||
<div class="input-group">
|
||||
{{ form.phone_number }}
|
||||
<button class="btn btn-outline-secondary" type="button" id="verifyPhoneBtn">
|
||||
<span id="phoneStatus">
|
||||
{% if profile.phone_verified %}
|
||||
<i class="fa-solid fa-check-circle text-success"></i> {% trans "Verified" %}
|
||||
{% else %}
|
||||
{% trans "Verify" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{% if form.phone_number.errors %}
|
||||
<div class="text-danger small">{{ form.phone_number.errors }}</div>
|
||||
{% endif %}
|
||||
@ -95,7 +119,7 @@
|
||||
<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">
|
||||
<button type="submit" class="btn btn-primary px-4" id="saveProfileBtn">
|
||||
<i class="fa-solid fa-save me-1"></i> {% trans "Save Changes" %}
|
||||
</button>
|
||||
</div>
|
||||
@ -134,4 +158,228 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OTP Modal -->
|
||||
<div class="modal fade" id="otpModal" tabindex="-1" aria-labelledby="otpModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="otpModalLabel">{% trans "Enter OTP" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="small text-muted" id="otpSentMsg"></p>
|
||||
<input type="text" id="otpInput" class="form-control text-center" maxlength="6" placeholder="123456" style="letter-spacing: 5px; font-size: 1.5rem; font-weight: bold;">
|
||||
<div id="otpError" class="text-danger small mt-2 d-none"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||
<button type="button" class="btn btn-primary" id="confirmOtpBtn">{% trans "Verify" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const emailInput = document.getElementById('id_email');
|
||||
const phoneInput = document.getElementById('id_phone_number');
|
||||
const countryCodeInput = document.getElementById('id_country_code');
|
||||
const emailVerifiedInput = document.getElementById('id_email_verified');
|
||||
const phoneVerifiedInput = document.getElementById('id_phone_verified');
|
||||
const verifyEmailBtn = document.getElementById('verifyEmailBtn');
|
||||
const verifyPhoneBtn = document.getElementById('verifyPhoneBtn');
|
||||
const emailStatus = document.getElementById('emailStatus');
|
||||
const phoneStatus = document.getElementById('phoneStatus');
|
||||
const saveBtn = document.getElementById('saveProfileBtn');
|
||||
|
||||
const otpModal = new bootstrap.Modal(document.getElementById('otpModal'));
|
||||
const otpInput = document.getElementById('otpInput');
|
||||
const confirmOtpBtn = document.getElementById('confirmOtpBtn');
|
||||
const otpError = document.getElementById('otpError');
|
||||
const otpSentMsg = document.getElementById('otpSentMsg');
|
||||
|
||||
let currentVerificationMethod = '';
|
||||
let currentVerificationValue = '';
|
||||
|
||||
const initialEmail = emailInput.value;
|
||||
const initialPhone = phoneInput.value;
|
||||
const initialCountryCode = countryCodeInput.value;
|
||||
|
||||
function updateSaveButtonState() {
|
||||
const isEmailVerified = emailVerifiedInput.value === 'True';
|
||||
const isPhoneVerified = phoneVerifiedInput.value === 'True';
|
||||
|
||||
// Enable if at least one is verified
|
||||
if (isEmailVerified || isPhoneVerified) {
|
||||
saveBtn.disabled = false;
|
||||
} else {
|
||||
saveBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
emailInput.addEventListener('input', function() {
|
||||
if (this.value !== initialEmail) {
|
||||
emailVerifiedInput.value = 'False';
|
||||
emailStatus.innerHTML = '{% trans "Verify" %}';
|
||||
verifyEmailBtn.classList.remove('btn-success');
|
||||
verifyEmailBtn.classList.add('btn-outline-secondary');
|
||||
} else {
|
||||
if ("{{ profile.email_verified|yesno:'true,false' }}" === "true") {
|
||||
emailVerifiedInput.value = 'True';
|
||||
emailStatus.innerHTML = '<i class="fa-solid fa-check-circle text-success"></i> {% trans "Verified" %}';
|
||||
}
|
||||
}
|
||||
updateSaveButtonState();
|
||||
});
|
||||
|
||||
phoneInput.addEventListener('input', function() {
|
||||
if (this.value !== initialPhone) {
|
||||
phoneVerifiedInput.value = 'False';
|
||||
phoneStatus.innerHTML = '{% trans "Verify" %}';
|
||||
verifyPhoneBtn.classList.remove('btn-success');
|
||||
verifyPhoneBtn.classList.add('btn-outline-secondary');
|
||||
} else {
|
||||
if ("{{ profile.phone_verified|yesno:'true,false' }}" === "true" && countryCodeInput.value === initialCountryCode) {
|
||||
phoneVerifiedInput.value = 'True';
|
||||
phoneStatus.innerHTML = '<i class="fa-solid fa-check-circle text-success"></i> {% trans "Verified" %}';
|
||||
}
|
||||
}
|
||||
updateSaveButtonState();
|
||||
});
|
||||
|
||||
countryCodeInput.addEventListener('change', function() {
|
||||
if (this.value !== initialCountryCode) {
|
||||
phoneVerifiedInput.value = 'False';
|
||||
phoneStatus.innerHTML = '{% trans "Verify" %}';
|
||||
} else {
|
||||
if ("{{ profile.phone_verified|yesno:'true,false' }}" === "true" && phoneInput.value === initialPhone) {
|
||||
phoneVerifiedInput.value = 'True';
|
||||
phoneStatus.innerHTML = '<i class="fa-solid fa-check-circle text-success"></i> {% trans "Verified" %}';
|
||||
}
|
||||
}
|
||||
updateSaveButtonState();
|
||||
});
|
||||
|
||||
verifyEmailBtn.addEventListener('click', function() {
|
||||
if (emailVerifiedInput.value === 'True') return;
|
||||
|
||||
const email = emailInput.value;
|
||||
if (!email) return;
|
||||
|
||||
verifyEmailBtn.disabled = true;
|
||||
verifyEmailBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
|
||||
|
||||
fetch("{% url 'send_otp_profile' %}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
method: 'email',
|
||||
value: email
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
verifyEmailBtn.disabled = false;
|
||||
verifyEmailBtn.innerHTML = '{% trans "Verify" %}';
|
||||
if (data.success) {
|
||||
currentVerificationMethod = 'email';
|
||||
currentVerificationValue = email;
|
||||
otpSentMsg.innerText = '{% trans "A code was sent to" %} ' + email;
|
||||
otpInput.value = '';
|
||||
otpError.classList.add('d-none');
|
||||
otpModal.show();
|
||||
} else {
|
||||
alert(data.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
verifyPhoneBtn.addEventListener('click', function() {
|
||||
if (phoneVerifiedInput.value === 'True') return;
|
||||
|
||||
const phone = phoneInput.value;
|
||||
const countryCode = countryCodeInput.value;
|
||||
if (!phone) return;
|
||||
|
||||
verifyPhoneBtn.disabled = true;
|
||||
verifyPhoneBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
|
||||
|
||||
fetch("{% url 'send_otp_profile' %}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
method: 'phone',
|
||||
value: phone,
|
||||
country_code: countryCode
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
verifyPhoneBtn.disabled = false;
|
||||
verifyPhoneBtn.innerHTML = '{% trans "Verify" %}';
|
||||
if (data.success) {
|
||||
currentVerificationMethod = 'phone';
|
||||
currentVerificationValue = phone;
|
||||
otpSentMsg.innerText = '{% trans "A code was sent to WhatsApp number" %} ' + countryCode + phone;
|
||||
otpInput.value = '';
|
||||
otpError.classList.add('d-none');
|
||||
otpModal.show();
|
||||
} else {
|
||||
alert(data.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
confirmOtpBtn.addEventListener('click', function() {
|
||||
const otp = otpInput.value;
|
||||
if (otp.length !== 6) return;
|
||||
|
||||
confirmOtpBtn.disabled = true;
|
||||
confirmOtpBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
|
||||
|
||||
fetch("{% url 'verify_otp_profile' %}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
method: currentVerificationMethod,
|
||||
value: currentVerificationValue,
|
||||
country_code: countryCodeInput.value,
|
||||
code: otp
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
confirmOtpBtn.disabled = false;
|
||||
confirmOtpBtn.innerHTML = '{% trans "Verify" %}';
|
||||
if (data.success) {
|
||||
if (currentVerificationMethod === 'email') {
|
||||
emailVerifiedInput.value = 'True';
|
||||
emailStatus.innerHTML = '<i class="fa-solid fa-check-circle text-success"></i> {% trans "Verified" %}';
|
||||
} else {
|
||||
phoneVerifiedInput.value = 'True';
|
||||
phoneStatus.innerHTML = '<i class="fa-solid fa-check-circle text-success"></i> {% trans "Verified" %}';
|
||||
}
|
||||
otpModal.hide();
|
||||
updateSaveButtonState();
|
||||
} else {
|
||||
otpError.innerText = data.error;
|
||||
otpError.classList.remove('d-none');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initial check
|
||||
updateSaveButtonState();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -35,4 +35,6 @@ urlpatterns = [
|
||||
path("admin/refund/<str:receipt_number>/", views.issue_refund, name="issue_refund"),
|
||||
path("admin/settings/", views.admin_app_settings, name="admin_app_settings"),
|
||||
path("api/chat/", views.chat_api, name="chat_api"),
|
||||
path("profile/send-otp/", views.send_otp_profile, name="send_otp_profile"),
|
||||
path("profile/verify-otp/", views.verify_otp_profile, name="verify_otp_profile"),
|
||||
]
|
||||
@ -877,4 +877,65 @@ def chat_api(request):
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Chat API Error: {str(e)}")
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
def send_otp_profile(request):
|
||||
if request.method != 'POST':
|
||||
return JsonResponse({'success': False, 'error': 'Method not allowed'}, status=405)
|
||||
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
method = data.get('method') # 'email' or 'phone'
|
||||
value = data.get('value')
|
||||
|
||||
if method == 'email':
|
||||
otp = OTPCode.generate_code(email=value)
|
||||
if send_otp_email(value, otp.code):
|
||||
return JsonResponse({'success': True})
|
||||
else:
|
||||
return JsonResponse({'success': False, 'error': _('Failed to send email')})
|
||||
elif method == 'phone':
|
||||
country_code = data.get('country_code', '966')
|
||||
full_phone = f"{country_code}{value}"
|
||||
otp = OTPCode.generate_code(phone_number=full_phone)
|
||||
msg = _("Your verification code is: %(code)s") % {"code": otp.code}
|
||||
if send_whatsapp_message(full_phone, msg):
|
||||
return JsonResponse({'success': True})
|
||||
else:
|
||||
return JsonResponse({'success': False, 'error': _('Failed to send WhatsApp message')})
|
||||
|
||||
return JsonResponse({'success': False, 'error': 'Invalid method'})
|
||||
except Exception as e:
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
def verify_otp_profile(request):
|
||||
if request.method != 'POST':
|
||||
return JsonResponse({'success': False, 'error': 'Method not allowed'}, status=405)
|
||||
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
method = data.get('method')
|
||||
value = data.get('value')
|
||||
code = data.get('code')
|
||||
|
||||
if method == 'email':
|
||||
otp_record = OTPCode.objects.filter(email=value, code=code, is_used=False).last()
|
||||
elif method == 'phone':
|
||||
country_code = data.get('country_code', '966')
|
||||
full_phone = f"{country_code}{value}"
|
||||
otp_record = OTPCode.objects.filter(phone_number=full_phone, code=code, is_used=False).last()
|
||||
else:
|
||||
return JsonResponse({'success': False, 'error': 'Invalid method'})
|
||||
|
||||
if otp_record and otp_record.is_valid():
|
||||
otp_record.is_used = True
|
||||
otp_record.save()
|
||||
return JsonResponse({'success': True})
|
||||
else:
|
||||
return JsonResponse({'success': False, 'error': _('Invalid or expired code')})
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user