changing profile

This commit is contained in:
Flatlogic Bot 2026-01-25 03:46:27 +00:00
parent 7bc32398ed
commit b4a62485fb
13 changed files with 885 additions and 364 deletions

View File

@ -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

View File

@ -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),
),
]

View File

@ -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":

View File

@ -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 %}

View File

@ -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"),
]

View File

@ -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