From 57fa221d47cd666d4e53ac1fcd4a36b6388c54b7 Mon Sep 17 00:00:00 2001 From: Kobe Date: Thu, 5 Jun 2025 21:05:15 +0200 Subject: [PATCH] Better contact form --- __pycache__/forms.cpython-313.pyc | Bin 6981 -> 7052 bytes __pycache__/models.cpython-313.pyc | Bin 34167 -> 34167 bytes forms.py | 9 +- routes/__pycache__/contacts.cpython-313.pyc | Bin 25169 -> 24394 bytes routes/contacts.py | 153 ++++++++------------ templates/contacts/form.html | 64 ++++---- 6 files changed, 106 insertions(+), 120 deletions(-) diff --git a/__pycache__/forms.cpython-313.pyc b/__pycache__/forms.cpython-313.pyc index beb9363e5bb74bdcbb392f1401ddf47650a5b097..ee0f021cfcec1d163d234367102a02b4b1561481 100644 GIT binary patch delta 1822 zcma)6OKclu5dPO*zp`FCem{~lOPZ>2MH^b0gg#Q*CIoO&_p!22sY$f4H|-YNo6b6g z_>zhX94eLaS0HiYSP5~7#F0ydREY!SfRI*H0U-_nBnX5%Gyg&x5J>Fd+nMj5nVr|a zpN`+0P=8a^9){1o&re9$&s9^BqLx>k-rL}NPB{j)t~1(b3WqGc7-atvbSIK;)7 zJlk7NKtjlY*y%MCG%kv%Wz zxiOz-ixGlHe2}jd&?5iHqo@~(4EppUV!u96WrxwHugI>3uSzhW_rtC#o0MR1n3Z<% z_x!VJ6vJ)4i*Q$hKK`@6ddZb9*iEzKFZo<~#WA6)>K&uqFrcATQAIk`GTO#v6Wq`l zPe3kUG+I{MRZr2&aynpiY++l!Vq107fdTx+`c=8&d%hPXH*jsmgjF}PWHmb`EG(|V zsGyfDK7X>xroa?2+j;$JHxL1B5C_eA>#j*`aOdbhtg;`qTCmtk+ zwjy$L_{Q{B4@*D6d*iPJDNo4oM54fN#@E)U#%|<-ZMU?F(K0vbPC%ZZK+uoiD$}M@ z2Wz=w*==hZbAUmlyUG-pSiVNiFo#T~6oWJ5RUtS_aE?GG@cMz0E4T4O5Fj#kgb1ba zI6j1kzew;h0mTK8v#5HWRDub9ulEJ7n0YMbBkeenu1qz+K{EP#4PdNqBtRORq9c<` z!wUO|V1j>;%4Eb4D02;;8{QyV{Ik?9 z=`{Z#^~M@yTT1?qCcq+EdOfI<6hUzP7OwO)QXe;=MXKn;C6a0gJGX#J5%o~{4*xNI z*6WE$175$F`9_+g#p*_ zTXw^2I${G{Wr_aUlz`QS1Rcjb1|uH8Uy=Fe?W7Rm@`+s(1E4gj|5#Nwd>o^%G|dxm z2aoCII#q;U{E{R+WP=;5`xWbc!MYo)`X#GAWVwfIc!M3>miJ5Pw^p|qzP9Iip>IT{ GQvU$bF_NwT delta 1762 zcma)6O-vg{6yC9o|9`Nt@m~OkK*9nEpz;$`ei9K;FqEWh+#=yVuy<(sdS%zN_&Xri08l6Cq4dEoehP$N1?#~i;mrKl8}bU>^a zWv8~Ccx}Zv^WtL!ld#hh+)upn+lxuI-d;>~3qyjGe2Yk_F^3&HL?k!Mmo8>lMEcc^ ztdz+Ja8AlH?2tMO!YGl>WrVwJ-n#@kr4HyS2-5^|J%ZRpzjQ8z0xaF}Ck&nv$WWK7 zaBeC1kkEJWBwtL43h0y~zks@CcX z$QwJ*$LXpHW*tOsmyL#AhcL56m*p~*TGeW)s!FY5xf-SdbkVa-k_X>cFu@j|vKw6y&(lKa^+A*%1aXI-HzeyN zO|g6|ah;oh6XYF3h<+F9A8@13yY{Q5tl2XIFS4@;*ASuzG3pL?2gcBxM3|wkg$KKC zFw;auT(hf<;}QazzS3pvkdBXT{U#@AT1%>M!>YdE5y2B%?4J}D0N2{?Aq6O z?OOD44hLhoux#kZ$TMZ5yME9e19EE`Z5czovaITAU6uj034;iialu)t)hvfvvAifa zUhgvU9!=3U6fiAMdCO4CiYd0hF#WA9k>D4BJ>}T%L^_+)AMYfyRE$4pVk2Vge~br; zEYhCN9khH5ET_sI#65IR=d+0}A5sCWw;7yO0yfCI#G#n9KXKEZmVD4m{6(hc7`q#s zGqmlJzQb?Yo)+FLWEohVca#lNtt-brB89@WT5fF_x&mGF>*VE?b!5DYdZoc0?#^kh zWnMf-rG2!rVbmx|amG zRM1}$GqjMIp!=CblS_}kJbap5bo_H+xizDr)J%T+R-lR>WjspNp_K(%VVilsUcJIT zSIWsd#1wG^E=E3PUd2D9YI&zv@-DmBAlQLdc=ebtGuN3fhrS_(doj f3duvE=SUcN>FOu(k9J-P?C<3oeK*_Z!jAq1UI&It diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index cc98e344f99a83c9c0b088d47efc716e05231b4a..ec974e1d5524a06b5f749cfe3846cd9dc3f66fc4 100644 GIT binary patch delta 25 fcmey~#q_<4iSIKnFBbz4%$V$$kr1+xFS8W@a*+sO delta 25 fcmey~#q_<4iSIKnFBbz4*!4PQGzM+t%WMSzZ<7ch diff --git a/forms.py b/forms.py index 77bcf0b..de158f7 100644 --- a/forms.py +++ b/forms.py @@ -1,5 +1,5 @@ from flask_wtf import FlaskForm -from wtforms import StringField, TextAreaField, BooleanField, SubmitField, PasswordField, SelectMultipleField +from wtforms import StringField, TextAreaField, BooleanField, SubmitField, PasswordField, SelectMultipleField, SelectField from wtforms.validators import DataRequired, Email, Length, Optional, ValidationError from models import User from flask_login import current_user @@ -13,8 +13,11 @@ class UserForm(FlaskForm): company = StringField('Company (Optional)', validators=[Optional(), Length(max=100)]) position = StringField('Position (Optional)', validators=[Optional(), Length(max=100)]) notes = TextAreaField('Notes (Optional)', validators=[Optional()]) - is_admin = BooleanField('Admin Role', default=False) - is_manager = BooleanField('Manager Role', default=False) + role = SelectField('Role', choices=[ + ('user', 'Standard User'), + ('manager', 'Manager'), + ('admin', 'Administrator') + ], validators=[DataRequired()]) new_password = PasswordField('New Password (Optional)') confirm_password = PasswordField('Confirm Password (Optional)') profile_picture = FileField('Profile Picture (Optional)', validators=[FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Images only!')]) diff --git a/routes/__pycache__/contacts.cpython-313.pyc b/routes/__pycache__/contacts.cpython-313.pyc index 7bea8e67051de3fbb5d499ab66080ed5c08f4a46..21bd10c532562544f46372f75274b411cae0be9a 100644 GIT binary patch delta 4492 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 diff --git a/routes/contacts.py b/routes/contacts.py index c08824f..6403e64 100644 --- a/routes/contacts.py +++ b/routes/contacts.py @@ -98,95 +98,61 @@ def new_contact(): form = UserForm() total_admins = User.query.filter_by(is_admin=True).count() if request.method == 'GET': - form.is_admin.data = False # Ensure admin role is unchecked by default - form.is_manager.data = False # Ensure manager role is unchecked by default + form.role.data = 'user' # Default to standard user elif request.method == 'POST': - if 'is_admin' not in request.form: - form.is_admin.data = False - if 'is_manager' not in request.form: - form.is_manager.data = False + if form.validate_on_submit(): + # Check if a user with this email already exists + existing_user = User.query.filter_by(email=form.email.data).first() + if existing_user: + flash('A user with this email already exists.', 'error') + return render_template('contacts/form.html', form=form, title='New User', total_admins=total_admins) + + # Handle profile picture upload + profile_picture = None + file = request.files.get('profile_picture') + if file and file.filename: + filename = secure_filename(file.filename) + file_path = os.path.join(UPLOAD_FOLDER, filename) + file.save(file_path) + profile_picture = filename - if form.validate_on_submit(): - # Check if a user with this email already exists - existing_user = User.query.filter_by(email=form.email.data).first() - if existing_user: - flash('A user with this email already exists.', 'error') - return render_template('contacts/form.html', form=form, title='New User', total_admins=total_admins) - - # Handle profile picture upload - profile_picture = None - file = request.files.get('profile_picture') - if file and file.filename: - filename = secure_filename(file.filename) - file_path = os.path.join(UPLOAD_FOLDER, filename) - file.save(file_path) - profile_picture = filename + # Generate a random password + alphabet = string.ascii_letters + string.digits + string.punctuation + random_password = ''.join(secrets.choice(alphabet) for _ in range(32)) - # Generate a random password - alphabet = string.ascii_letters + string.digits + string.punctuation - random_password = ''.join(secrets.choice(alphabet) for _ in range(32)) + # Create new user + user = User( + username=form.first_name.data, + last_name=form.last_name.data, + email=form.email.data, + phone=form.phone.data, + company=form.company.data, + position=form.position.data, + notes=form.notes.data, + is_admin=(form.role.data == 'admin'), + is_manager=(form.role.data == 'manager'), + profile_picture=profile_picture + ) + user.set_password(random_password) + db.session.add(user) - # Create new user account - user = User( - username=form.first_name.data, - last_name=form.last_name.data, - email=form.email.data, - phone=form.phone.data, - company=form.company.data, - position=form.position.data, - notes=form.notes.data, - is_active=True, # Set default value - is_admin=form.is_admin.data, - is_manager=form.is_manager.data, - profile_picture=profile_picture - ) - user.set_password(random_password) - db.session.add(user) - db.session.commit() + # Log user creation event + log_event( + event_type='user_create', + details={ + 'created_by': current_user.id, + 'created_by_name': f"{current_user.username} {current_user.last_name}", + 'user_id': user.id, + 'user_name': f"{user.username} {user.last_name}", + 'email': user.email, + 'role': form.role.data, + 'method': 'admin_creation' + } + ) + db.session.commit() - # Create password setup token - token = secrets.token_urlsafe(32) - setup_token = PasswordSetupToken( - user_id=user.id, - token=token, - expires_at=datetime.utcnow() + timedelta(hours=24) - ) - db.session.add(setup_token) - db.session.commit() - - # Create notification for the new user - create_notification( - notif_type='account_created', - user_id=user.id, - sender_id=current_user.id, # Admin who created the account - details={ - 'message': 'Your DocuPulse account has been created by an administrator.', - 'username': user.username, - 'email': user.email, - 'created_by': f"{current_user.username} {current_user.last_name}", - 'timestamp': datetime.utcnow().isoformat(), - 'setup_link': url_for('auth.setup_password', token=token, _external=True) - } - ) - - # Log user creation event - log_event( - event_type='user_create', - details={ - 'created_by': current_user.id, - 'created_by_name': f"{current_user.username} {current_user.last_name}", - 'user_id': user.id, - 'user_name': f"{user.username} {user.last_name}", - 'email': user.email, - 'is_admin': user.is_admin, - 'is_manager': user.is_manager, - 'method': 'admin_creation' - } - ) - db.session.commit() - - flash('User created successfully! They will receive an email with a link to set up their password.', 'success') - return redirect(url_for('contacts.contacts_list')) + flash('User created successfully! They will receive an email with a link to set up their password.', 'success') + return redirect(url_for('contacts.contacts_list')) return render_template('contacts/form.html', form=form, title='New User', total_admins=total_admins) @contacts_bp.route('/profile/edit', methods=['GET', 'POST']) @@ -287,7 +253,13 @@ def edit_contact(id): form.company.data = user.company form.position.data = user.position form.notes.data = user.notes - form.is_admin.data = user.is_admin + # Set role based on current permissions + if user.is_admin: + form.role.data = 'admin' + elif user.is_manager: + form.role.data = 'manager' + else: + form.role.data = 'user' if form.validate_on_submit(): # Handle profile picture removal if 'remove_picture' in request.form: @@ -315,9 +287,10 @@ def edit_contact(id): user.profile_picture = filename # Prevent removing admin from the last admin - if not form.is_admin.data and user.is_admin and total_admins <= 1: + if form.role.data != 'admin' and user.is_admin and total_admins <= 1: flash('There must be at least one admin user in the system.', 'error') return render_template('contacts/form.html', form=form, title='Edit User', total_admins=total_admins, user=user) + # Check if the new email is already used by another user if form.email.data != user.email: existing_user = User.query.filter_by(email=form.email.data).first() @@ -332,7 +305,7 @@ def edit_contact(id): 'phone': user.phone, 'company': user.company, 'position': user.position, - 'is_admin': user.is_admin + 'role': 'admin' if user.is_admin else 'manager' if user.is_manager else 'user' } user.username = form.first_name.data @@ -342,7 +315,8 @@ def edit_contact(id): user.company = form.company.data user.position = form.position.data user.notes = form.notes.data - user.is_admin = form.is_admin.data + user.is_admin = (form.role.data == 'admin') + user.is_manager = (form.role.data == 'manager') # Set password if provided password_changed = False @@ -366,6 +340,7 @@ def edit_contact(id): 'phone': user.phone, 'company': user.company, 'position': user.position, + 'role': form.role.data, 'password_changed': password_changed }, 'timestamp': datetime.utcnow().isoformat() @@ -387,7 +362,7 @@ def edit_contact(id): 'phone': user.phone, 'company': user.company, 'position': user.position, - 'is_admin': user.is_admin + 'role': form.role.data }, 'password_changed': password_changed, 'method': 'admin_update' diff --git a/templates/contacts/form.html b/templates/contacts/form.html index 7ad5f73..da8a337 100644 --- a/templates/contacts/form.html +++ b/templates/contacts/form.html @@ -113,37 +113,45 @@ {% endif %} -
-
-
- {% set is_last_admin = current_user.is_admin and total_admins <= 1 %} - {{ form.is_admin( - class="h-4 w-4 focus:ring-blue-500 border-gray-300 rounded", - style="accent-color: #16767b;", - disabled=is_last_admin and form.is_admin.data - ) }} - {{ form.is_admin.label(class="ml-2 block text-sm text-gray-900") }} - {% if is_last_admin and form.is_admin.data %} - - {% endif %} - -
-
- {{ form.is_manager( - class="h-4 w-4 focus:ring-blue-500 border-gray-300 rounded", - style="accent-color: #16767b;" - ) }} - {{ form.is_manager.label(class="ml-2 block text-sm text-gray-900") }} - + +
+

Role Selection

+
+ {% for value, label in form.role.choices %} +
+ +
+ {% endfor %} + {% if form.role.errors %} + {% for error in form.role.errors %} +

{{ error }}

+ {% endfor %} + {% endif %}
- {% if form.is_admin.errors %} +
+ +
+ {% if form.role.errors %}
- {% for error in form.is_admin.errors %} + {% for error in form.role.errors %}

{{ error }}

{% endfor %}