Source code for django_webapps_fullstack.account.forms

"""
This module provides comprehensive user authentication and account management forms for the dingx
web application. It handles the complete user lifecycle from registration to password recovery,
integrating with the Odoo ERP system via XMLRPC for centralized user data management.

**Business Context:**

The account app serves as the gateway for user interaction with the dingx platform, which provides
a logistics management system for tracking and managing physical objects across different locations
(storage, home, disposal, transfer). Users must authenticate through this system to access their
personal dashboard and manage their belongings.

**Key Business Processes:**

1. **Authentication:**

- :class:`LoginForm`: Secure login validation against Odoo user database
    - Session management for authenticated users
    - Credential retrieval for authorized access

2. **User Registration & Activation:**

- :class:`RegisterForm`: New users register with name, email, login, and password
    - System validates uniqueness against Odoo database
    - Activation token sent via email to verify user identity

- :class:`User_ActivateForm`: Users must activate account before first login via email token

3. **Password Management:**

- :class:`Password_RecoveryForm`: Password recovery via email-based reset tokens

- :class:`ResetForm`: Validate password reset token

- :class:`Password_RenewForm`: Set new password after reset

- :class:`Password_ChangeForm`: Change password functionality for logged-in users

- All password operations synchronized with Odoo

**Integration:**

All forms communicate with the Odoo ERP backend via Twisted XMLRPC server, ensuring
centralized user data management across the entire dingx ecosystem.
"""

# 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()

# import the standard Django Forms from built-in library
from django       import forms
from django.forms import ValidationError

# for the use of the gettext function to mark text for translation
from django.utils.translation import gettext as _

# for the use of 'configparser' functionality
import configparser

# for the use of 'XMLRPC' functionality
import xmlrpc.client
import ssl
import platform


# ------- get some parameter from the settings file "parameter_global.ini"
# Get the path to the parent directory
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
# Define the directory 'settings' under the parent directory that contains the settings file
sub_dir = os.path.join(parent_dir, 'settings')
# Append this directory to sys.path
sys.path.append(sub_dir)
# Name and full path of the "ini" file
filename  = "parameter_global.ini"
path_name = sub_dir
fullpath  = os.path.join(path_name,filename)
# read the settings file using configparser with UTF-8
config = configparser.ConfigParser()
config.read(fullpath, encoding='utf-8')

# Get the settings to log onto Twisted Server
server_url = config.get('settings_twisted_server', 'server_url')
# Get the image size of the picture stored in the Odoo system, for example '128'
image_size = config.get('settings_camera', 'image_size')

# Create an SSL context with proper certificate validation
# Uses system's trusted CA certificates (includes Let's Encrypt root CA)
context = ssl.create_default_context()

# On macOS development, use unverified context if certificate validation fails
# On Linux production, always use verified certificates
is_macos = platform.system() == 'Darwin'
if is_macos:
    # Development mode: Use unverified context for macOS
    context = ssl._create_unverified_context()

# Create a secure XMLRPC client with SSL/TLS encryption
proxy_client = xmlrpc.client.ServerProxy(server_url, context=context)



[docs] class LoginForm(forms.Form): """ Create a form for the "login" template and create the functions of the form. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # define the input fields for the form self.fields['login'] = forms.CharField(required=True, widget=forms.TextInput(attrs={'id': 'id_login', 'class': 'form-control', 'placeholder': _("Enter login name")}), max_length = 50, label=_("Login Name")) self.fields['password'] = forms.CharField(required=True, widget=forms.PasswordInput(attrs={'id': 'id_password', 'class': 'form-control', 'placeholder': _("Enter password")}), initial='', max_length = 50, label=_("Password"))
[docs] def clean(self): """ Check the credentials it gets and return a user object that matches those credentials if the credentials are valid. If they’re not valid, it return None. """ # Get the user submitted values from the cleaned_data dictionary cleaned_data = super().clean() # cleaned_data = super().clean() login = cleaned_data.get('login') password = cleaned_data.get('password') # check the credentials against the users in Odoo result = proxy_client.check_user_login(login, password) if result == 'False': raise ValidationError(_("Please enter a valid login name or password.")) return (cleaned_data)
[docs] def get_credentials(self, login): """ get the credentials of the account """ result = proxy_client.get_credentials(login) if result[0] != 'True': raise ValidationError(_("The credentials of the user could not be loaded.")) return (result)
[docs] class RegisterForm(forms.Form): """ Create a form for the "register" template and create the corresponding functions of the form. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # define the input fields for the form self.fields['name'] = forms.CharField(required=True, widget=forms.TextInput(attrs={'id': 'id_name', 'class': 'form-control', 'placeholder': _("Enter name")}), max_length = 100, label=_("Name")) self.fields['login'] = forms.CharField(required=True, widget=forms.TextInput(attrs={'id': 'id_login', 'class': 'form-control', 'placeholder': _("Enter login name")}), max_length = 100, label=_("Login Name")) self.fields['email'] = forms.EmailField(required=True, widget=forms.EmailInput(attrs={'id': 'id_email', 'class': 'form-control', 'placeholder': _("Enter e-mail")}), max_length = 100, label=_("E-Mail")) self.fields['password'] = forms.CharField(required=True, widget=forms.PasswordInput(attrs={'id': 'id_password', 'class': 'form-control', 'placeholder': _("Enter Password")}), initial='', max_length = 50, label=_("Password")) self.fields['password_confirm'] = forms.CharField(required=True, widget=forms.PasswordInput(attrs={'id': 'id_password_confirm', 'class': 'form-control', 'placeholder': _("Enter Password Confirm")}), initial='', max_length = 50, label=_("Password Confirm"))
[docs] def clean(self): """ Validation of the entered values during the registration of a user. """ # Get the user submitted values from the cleaned_data dictionary cleaned_data = super().clean() name = cleaned_data.get("name", '').strip() # Ensure value is a string and not None login = cleaned_data.get("login", '').strip() # Ensure value is a string and not None email = cleaned_data.get("email", '').strip() # Ensure value is a string and not None password = cleaned_data.get("password", '').strip() # Ensure value is a string and not None password_confirm = cleaned_data.get("password_confirm", '').strip() # Ensure value is a string and not None # Check the values against the different checks coming form Odoo system result = proxy_client.name_exist(name) if result == 'True': raise ValidationError(_("Name is already used by someone else")) result = proxy_client.login_exist(login) if result == 'True': raise ValidationError(_("Login name is already used by someone else")) result = proxy_client.email_exist(email) if result == 'True': raise ValidationError(_("E-Mail is already used by someone else")) if password != password_confirm: raise ValidationError(_("Passwords do not match")) return cleaned_data
[docs] def save(self, name, login, email, password): """ Save the user in the Odoo Database. Returns: tuple: (result, user_id, partner_id) from create_user function """ result, user_id, partner_id = proxy_client.create_user(name, login, email, password) if result == 'False': raise ValidationError(_("User could not be created")) return (result, user_id, partner_id)
[docs] def create_activatetoken(self, login): """ Create the activate token for the user, which is used for the activation of the user account. """ result = proxy_client.create_activatetoken(login) if result == 'False': raise ValidationError(_("Activatetoken could not be created")) return (result)
[docs] class User_ActivateForm(forms.Form): """ Create a form and create the corresponding functions of the form """ def __init__(self, *args, **kwargs): self.activate_token = kwargs.pop('activate_token', None) super().__init__(*args, **kwargs)
[docs] def clean(self): """ Test if the provided activate token is correct. """ cleaned_data = super().clean() activate_token = self.activate_token result = proxy_client.test_activatetoken(activate_token) if result == 'False': raise ValidationError(_("The provided activate token is not correct")) return (cleaned_data)
[docs] def save(self): """ Update of the provided activate token so that the user account could be used. """ activate_token = self.activate_token result = proxy_client.update_activatetoken(activate_token) if result[0] == 'False': raise ValidationError(_("Update of the activate token could not be executed")) return(result)
[docs] class Password_RecoveryForm(forms.Form): """ Create a form for the "recovery" template and create the corresponding functions of the form """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # define the input fields for the form self.fields['email'] = forms.EmailField(required=True, widget=forms.EmailInput(attrs={'id': 'id_email', 'class': 'form-control', 'placeholder': _("Enter e-mail")}), max_length = 100, label=_("E-Mail"))
[docs] def clean(self): """ Check if the email is valid. """ # Get the user submitted values from the cleaned_data dictionary cleaned_data = super().clean() email = cleaned_data.get("email", '').strip() # Ensure value is a string and not None # Check if the e-mail exit result = proxy_client.email_exist(email) if result == 'False': pass return (cleaned_data)
[docs] def create_resettoken(self): """ Create the password reset token und save it in the Odoo Database. """ # Get the email from the cleaned_data dictionary cleaned_data = super().clean() email = cleaned_data.get("email") # Create the Resetttoken result = proxy_client.create_resettoken(email) return (result)
[docs] class ResetForm(forms.Form): """ Create a form and create the corresponding functions of the form """ def __init__(self, *args, **kwargs): self._reset_token = kwargs.pop('reset_token', None) super().__init__(*args, **kwargs)
[docs] def clean(self): """ Test if the provided reset token is correct. """ cleaned_data = super().clean() reset_token = self._reset_token result = proxy_client.test_resettoken(reset_token) if result == 'False': raise ValidationError(_("The provided reset token is not correct")) return (cleaned_data)
[docs] def save(self): """ Update of the provided reset token so that the token could be used any longer. """ reset_token = self._reset_token result = proxy_client.update_resettoken(reset_token) if result[0] == 'False': raise ValidationError(_("Update of the reset token could not be executed")) return(result)
[docs] class Password_RenewForm(forms.Form): """ Create a form for the "password_renew" template and create the corresponding functions of the form """ def __init__(self, *args, **kwargs): self._login = kwargs.pop('login', None) super().__init__(*args, **kwargs) # define the input fields for the form self.fields['password'] = forms.CharField(required=True, widget=forms.PasswordInput(attrs={'id': 'id_password', 'class': 'form-control', 'placeholder': _("Enter new password")}), initial='', max_length = 50, label=_("New Password")) self.fields['password_confirm'] = forms.CharField(required=True, widget=forms.PasswordInput(attrs={'id': 'id_password_confirm', 'class': 'form-control', 'placeholder': _("Enter confirm new password")}), initial='', max_length = 50, label=_("Confirm New Password"))
[docs] def clean(self): """ Validation of the entered passwords. """ # Get the user submitted values from the cleaned_data dictionary cleaned_data = super().clean() password = cleaned_data.get("password", '').strip() # Ensure value is a string and not None password_confirm = cleaned_data.get("password_confirm", '').strip() # Ensure value is a string and not None # Check if the passwords are equal if password != password_confirm: raise ValidationError(_("Passwords do not match")) return (cleaned_data)
[docs] def save(self): """ Save the changed password in the Odoo Database. """ login = self._login # Get the user submitted values from the cleaned_data dictionary cleaned_data = super().clean() password = cleaned_data.get("password") result = proxy_client.change_password(login, password) return result
[docs] class Password_ChangeForm(forms.Form): """ Create a form for the "password_change" template and create the corresponding functions of the form """ def __init__(self, *args, **kwargs): self._login = kwargs.pop('login', None) super().__init__(*args, **kwargs) # define the input fields for the form self.fields['password_current'] = forms.CharField(required=True, widget=forms.PasswordInput(attrs={'id': 'id_password_current', 'class': 'form-control', 'placeholder': _("Enter Current Password")}), initial='', max_length = 50, label=_("Current Password")) self.fields['password_new'] = forms.CharField(required=True, widget=forms.PasswordInput(attrs={'id': 'id_password_new', 'class': 'form-control', 'placeholder': _("Enter New Password")}), initial='', max_length = 50, label=_("New Password")) self.fields['password_confirm'] = forms.CharField(required=True, widget=forms.PasswordInput(attrs={'id': 'id_password_confirm', 'class': 'form-control', 'placeholder': _("Enter Confirm New Password")}), initial='', max_length = 50, label=_("Confirm New Password"))
[docs] def clean(self): """ Validation of the entered passwords. """ login = self._login # Get the user submitted values from the cleaned_data dictionary cleaned_data = super().clean() password_current = cleaned_data.get("password_current", '').strip() # Ensure value is a string and not None password_new = cleaned_data.get("password_new", '').strip() # Ensure value is a string and not None password_confirm = cleaned_data.get("password_confirm", '').strip() # Ensure value is a string and not None # Check if name and passwort match against the users in Odoo result = proxy_client.check_user_login(login, password_current) if result == 'False': raise ValidationError(_("Please enter a valid login or password")) # Check if the passwords are equal if password_new != password_confirm: raise ValidationError(_("Passwords do not match")) return (cleaned_data)
[docs] def save(self): """ Save the changed password in the Odoo Database. """ login = self._login # Get the user submitted values from the cleaned_data dictionary cleaned_data = super().clean() password = cleaned_data.get("password_new") result = proxy_client.change_password(login, password) return result