"""
This module implements the complete user authentication and account management workflow for the dingx
logistics platform. It handles user registration, activation, login, password management (recovery and
change), and session management, all integrated with the Odoo ERP backend via XMLRPC.
**Business Context:**
The account app is the entry point for all user interactions with the dingx platform. Users must
create an account and authenticate to access the logistics dashboard where they manage their physical
belongings. The system ensures secure user onboarding with email verification and provides self-service
password management to maintain user autonomy.
**Key Business Workflows:**
1. **User Authentication:**
- :func:`login`: Login with username and password validated against Odoo
- Session created with user credentials (user_id, name, login, email, partner_id, language)
- Successful login redirects to dashboard overview
- Failed login shows validation error
- :func:`logout`: User logout clears session and cache (session data includes: user_id, name, login, email, partner_id, language; cache cleared per partner_id for data consistency)
2. **User Registration & Activation:**
- :func:`register`: New users register with name, email, login, and password
- System validates uniqueness against Odoo database
- Activation email sent with unique token
- :func:`register_done`: Registration confirmation page
- :func:`user_activate`: User clicks activation link to verify email and activate account
- :func:`user_activate_done`: Account activation success page
- :func:`user_activate_fail`: Account activation failure page
3. **Password Recovery:**
- :func:`password_recovery`: User requests password reset via email
- Reset token generated and sent via email
- System maintains security by not revealing if email exists
- :func:`password_recovery_done`: Reset email sent confirmation page
- :func:`reset`: Click on reset link validates token
- :func:`password_renew`: User sets new password after reset
- :func:`password_renew_done`: Password reset success page
- :func:`password_renew_fail`: Password reset failure page
4. **Password Change:**
- :func:`password_change`: Authenticated users can change their password
- Current password validation required
- New password confirmation enforced
- Updates synchronized with Odoo database
- :func:`password_change_done`: Password change success page
**Security Features:**
- Email-based account activation
- Token-based password reset
- Session-based authentication
- Current password validation for password changes
- No information disclosure about email existence
- Cache invalidation on logout
**Integration:**
All views communicate with the Odoo ERP backend via XMLRPC through the account forms,
ensuring centralized user management and data consistency across the dingx ecosystem.
**Email Notifications:**
- Account activation link (user_activate)
- Password reset link (password_recovery)
- All emails configured via Django settings.DEFAULT_FROM_EMAIL
"""
# Initialize Django settings if running standalone (e.g., in Thonny IDE)
# This allows the module to be imported for inspection without running Django server
import sys
import os
if __name__ == "__main__" or 'django.conf.settings' not in sys.modules or not hasattr(sys.modules.get('django.conf.settings', None), 'configured'):
import django
from django.conf import settings
if not settings.configured:
# Add the project root to Python path
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if project_root not in sys.path:
sys.path.insert(0, project_root)
# Set the Django settings module
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.settings')
# Setup Django
django.setup()
from django.shortcuts import redirect, render
from django.urls import reverse
from django.core.mail import send_mail
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.conf import settings
# used for internationalization (i18n) and localization (l10n)
from django.utils import translation
# for the use of the gettext function to mark text for translation
from django.utils.translation import gettext as _
# set the desired language for templates
from django.utils.translation import activate, deactivate
# provides a caching framework to store and retrieve data efficiently
from django.core.cache import cache
# for exception handling and debugging
import traceback
# Handle both relative imports (normal Django) and absolute imports (standalone/Thonny)
try:
from .forms import LoginForm
from .forms import RegisterForm
from .forms import User_ActivateForm
from .forms import Password_RecoveryForm
from .forms import ResetForm
from .forms import Password_RenewForm
from .forms import Password_ChangeForm
except ImportError:
# Fallback for standalone execution (e.g., in Thonny IDE)
from account.forms import LoginForm
from account.forms import RegisterForm
from account.forms import User_ActivateForm
from account.forms import Password_RecoveryForm
from account.forms import ResetForm
from account.forms import Password_RenewForm
from account.forms import Password_ChangeForm
[docs]
def login(request):
"""
Check the credentials of a user.
The user will be logged in if the credentials are valid.
Otherwise the "login" form will be reloaded.
"""
if request.method == 'POST':
form = LoginForm(request.POST)
try:
if form.is_valid():
login = form.cleaned_data['login']
result = form.get_credentials(login)
if result[0] == 'True':
try:
request.session.delete()
except:
pass
request.session.create()
request.session['user_id'] = result[1]
request.session['name'] = result[2]
request.session['login'] = result[3]
request.session['email'] = result[4]
request.session['partner_id'] = result[5]
request.session['language'] = result[6]
# show all objects on the dashboard (= 'overview')
return redirect('overview', place = "all")
else:
error_message = ValidationError
messages.error(request, error_message)
except ValidationError as error_message:
messages.error(request, error_message.message)
else:
form = LoginForm()
return render(request, 'login.html', {'form': form})
[docs]
def register(request):
"""
Register the user.
If successful, an activate mail will be sent.
"""
if request.method == 'POST':
form = RegisterForm(request.POST)
try:
if form.is_valid():
name = form.cleaned_data['name']
login = form.cleaned_data['login']
email = form.cleaned_data['email']
password = form.cleaned_data['password']
result, user_id, partner_id = form.save(name, login, email, password)
if result == 'True':
# create the ativate token
activate_token = form.create_activatetoken(login)
if result == 'True':
# prepare and sent the activate email
url = 'http://localhost:8000/account/user_activate/'
user_activate_link = url + activate_token
subject = _("User Activation Link for dingX")
message = _("Please use that link to activate your user account") + ": " + user_activate_link
sender = settings.DEFAULT_FROM_EMAIL
recipient = email
send_mail(subject, message, sender, [recipient])
return redirect('register_done')
else:
error_message = ValidationError
messages.error(request, error_message)
else:
error_message = ValidationError
messages.error(request, error_message)
except ValidationError as error_message:
messages.error(request, error_message.message)
else:
form = RegisterForm()
return render(request, 'register.html', {'form': form})
[docs]
def register_done(request):
"""
Send a validation page that the register of the account is finished.
"""
if request.method == "GET":
return render(request, 'register_done.html')
elif request.method == "POST":
return redirect('login')
return render(request, 'register_done.html')
[docs]
def user_activate(request, activate_token):
"""
If a valid user activate link is submitted in the URL,
the token will be updated and the user account is active.
"""
form = User_ActivateForm(request.POST, activate_token=activate_token)
try:
if form.is_valid():
result = form.save()
if result[0] == 'True':
return redirect('user_activate_done')
else:
return redirect('user_activate_fail')
except Exception as e:
print("An error occurred:", e)
traceback.print_exc()
return redirect('user_activate_fail')
[docs]
def user_activate_done(request):
"""
Send a validation page that the activation of the account is finished.
"""
if request.method == "GET":
return render(request, 'user_activate_done.html')
elif request.method == "POST":
return redirect('login')
return render(request, 'user_activate_done.html')
[docs]
def user_activate_fail(request):
"""
Send a message page that the activation of the account is failed.
"""
if request.method == "GET":
return render(request, 'user_activate_fail.html')
elif request.method == "POST":
return redirect('home')
return render(request, 'user_activate_fail.html')
[docs]
def password_recovery(request):
"""
If a valid email has been submitted,
an email with the reset link will be send.
"""
if request.method == "POST":
form = Password_RecoveryForm(request.POST)
try:
if form.is_valid():
email = form.cleaned_data['email']
reset_token = form.create_resettoken()
if not reset_token == 'False':
url = 'http://localhost:8000/account/reset/'
password_reset_link = url + reset_token
subject = _("Password Reset Link for dingX")
message = _("Please use that link to reset your password") + ": " + password_reset_link
sender = settings.DEFAULT_FROM_EMAIL
recipient = email
send_mail(subject, message, sender, [recipient])
# Although the provided email is not valid and the reset_token is not created
# the validation page will be called.
# This is to avoid giving any hint that the given email is not valid.
return redirect('password_recovery_done')
except:
error_message = ValidationError
messages.error(request, error_message)
else:
form = Password_RecoveryForm()
return render(request, 'password_recovery.html', {'form': form})
[docs]
def password_recovery_done(request):
"""
Send a validation page that the recovery of the password is finished.
"""
if request.method == "GET":
return render(request, 'password_recovery_done.html')
elif request.method == "POST":
return redirect('login')
return render(request, 'password_recovery_done.html')
[docs]
def reset(request, reset_token):
"""
If a valid password reset link is submitted in the URL,
the token will be updated and a new password can be choosen
in the "password_renew" template.
"""
form = ResetForm(request.POST, reset_token=reset_token)
try:
if form.is_valid():
result = form.save()
# Update the reset token and get the credentials of the user.
# Credential "Login" will be used to login to
# Odoo system when renew the passwords.
# The password_renew template is called afterwards.
if result[0] == 'True':
try:
request.session.delete()
except:
pass
request.session.create()
request.session['user_id'] = result[1]
request.session['name'] = result[2]
request.session['login'] = result[3]
request.session['email'] = result[4]
request.session['partner_id'] = result[5]
return redirect('password_renew')
else:
return redirect('password_renew_fail')
except Exception as e:
print("An error occurred:", e)
traceback.print_exc()
return redirect('password_renew_fail')
[docs]
def password_renew(request):
"""
Renew the password of the user.
"""
try:
if request.session:
login = request.session['login']
except:
return redirect(reverse('home'))
if request.method == 'POST':
form = Password_RenewForm(request.POST, login=login)
try:
if form.is_valid():
form.save()
return redirect('password_renew_done')
except ValidationError as error_message:
messages.error(request, error_message.message)
else:
form = Password_RenewForm(login=login)
return render(request, "password_renew.html", {"form": form, "login": login})
[docs]
def password_renew_done(request):
"""
Send a validation page that the password renew is finished.
"""
if request.method == "GET":
return render(request, 'password_renew_done.html')
elif request.method == "POST":
return redirect('login')
return render(request, 'password_renew_done.html')
[docs]
def password_renew_fail(request):
"""
Send a message page that the password renew has failed.
"""
if request.method == "GET":
return render(request, 'password_renew_fail.html')
elif request.method == "POST":
return redirect('login')
return render(request, 'password_renew_fail.html')
[docs]
def password_change(request):
"""
Change the password of the user.
"""
try:
if request.session:
login = request.session['login']
except:
return redirect(reverse('home'))
if request.method == 'POST':
form = Password_ChangeForm(request.POST, login=login)
try:
if form.is_valid():
form.save()
return redirect('password_change_done')
except ValidationError as error_message:
messages.error(request, error_message.message)
else:
form = Password_ChangeForm(login=login)
return render(request, "password_change.html", {"form": form, "login": login})
[docs]
def password_change_done(request):
"""
Send a validation page that the password change is finished.
"""
if request.method == "GET":
return render(request, 'password_change_done.html')
elif request.method == "POST":
# go to dashoard and show all objects in the dashboard (= 'overview')
return redirect('overview', place = "all")
return render(request, 'password_change_done.html')
[docs]
def logout(request):
"""
Logout of the user including deleting the session key,
so that the user is no longer logged into Odoo system.
"""
try:
partner_id = request.session.get('partner_id')
# delete_pattern() only works if django-redis is the cache backend
# deletes all cache keys from that partner_id
cache.delete_pattern(f"partner:{partner_id}:*")
except:
pass
try:
# delete the current session data from the database or session store
request.session.delete()
except:
pass
return redirect(reverse("home"))