app.admin.routes

Admin routes for user and training module management.

This file defines all /admin/... endpoints for creating, editing, viewing and deleting users and training modules. All routes ensure that the current user is authenticated and has the “admin” role.

  1"""Admin routes for user and training module management.
  2
  3This file defines all `/admin/...` endpoints for creating, editing,
  4viewing and deleting users and training modules. All routes ensure that the
  5current user is authenticated and has the “admin” role.
  6"""
  7from datetime import datetime
  8
  9from flask import render_template, flash, redirect, url_for, request
 10from flask_login import current_user, login_required
 11
 12from app import app, db
 13from app.admin.forms import CreateUserForm, EditUserForm, CreateTrainingModuleForm
 14from app.models import (
 15    User, 
 16    Role, 
 17    Department, 
 18    TrainingModule, 
 19    Question, 
 20    Option, 
 21    OnboardingPath, 
 22    OnboardingStep
 23)
 24
 25
 26@app.route('/admin/register', methods = ['GET', 'POST'])
 27def register_user():
 28    """Register a new user.
 29
 30    Displays a form for creating a user, validates input, determines the
 31    appropriate onboarding path based on department, sets the password,
 32    and commits the new User record to the database.
 33
 34    Args:
 35        None (form data submitted via POST).
 36
 37    Returns:
 38        - Redirects to logout if the user is not an admin. 
 39        - Re-renders the registration form on GET or validation failure.
 40        - Redirects to manage_users on successful creation (with a flash
 41        message).
 42    """
 43    if not current_user.is_authenticated or current_user.role.role_name != 'admin':
 44        return redirect(url_for('logout'))
 45
 46    form = CreateUserForm()
 47
 48    form.role.choices = [(0, 'Select Role')] + [
 49        (role.id, role.role_name) for role in Role.query.all()
 50    ]
 51    form.department.choices = [(0, 'Select Department')] + [
 52        (dept.id, dept.department_name) for dept in Department.query.all()
 53    ]
 54
 55    manager_role = Role.query.filter_by(role_name='manager').first()
 56    manager_choices = [(0, 'None')]
 57    if manager_role:
 58        manager_choices += [
 59            (u.id, f"{u.first_name.title()} {u.surname.title()}")
 60            for u in User.query.filter_by(role_id=manager_role.id)
 61        ]
 62    form.manager.choices = manager_choices
 63
 64    if form.validate_on_submit():
 65        department_id = int(form.department.data)
 66
 67        if Department.query.filter_by(
 68            id=department_id, 
 69            department_name = "office",
 70        ).first():
 71            onboarding_path = OnboardingPath.query.filter_by(
 72                path_name = "office",
 73            ).first()
 74        else:
 75            onboarding_path = OnboardingPath.query.filter_by(   
 76                path_name = "operational",
 77            ).first()
 78       
 79        user = User(
 80            first_name = form.firstName.data,
 81            surname = form.surname.data,
 82            username = form.username.data,
 83            role_id = int(form.role.data),
 84            is_onboarding = (form.is_onboarding.data == 'yes'),
 85            manager_id = int(form.manager.data) if form.manager.data else None,
 86            department_id = int(form.department.data),
 87            onboarding_path_id = onboarding_path.id if onboarding_path else None,
 88            dateStarted = datetime.now(),
 89            job_title = form.job_title.data
 90        )
 91
 92        user.set_password(form.password.data)
 93        db.session.add(user)
 94        db.session.commit()
 95        flash(
 96            f'User {user.first_name} {user.surname} '
 97            'has been successfully registered!'
 98        )
 99        return redirect(url_for('manage_users'))
100    
101    return render_template(
102        'admin/createUser.html', 
103        title='Register User', 
104        form=form,
105    )
106
107
108@app.route('/admin/dashboard')
109@login_required
110def admin_dashboard():
111    """Display the admin dashboard.
112
113    Returns:
114        - Rendered admin dashboard template.
115        - Redirects to logout if the user is not an admin.
116    """
117    if current_user.role.role_name!= "admin":
118        return redirect(url_for('logout'))
119    
120    return render_template(
121        'admin/dashboard.html', 
122        title='Admin Dashboard',
123    )
124
125
126@app.route('/admin/manage_users', methods = ['GET'])
127@login_required
128def manage_users():
129    """Display all users in the system.
130
131    Returns:
132        - Rendered template with a list of all users.
133        - Redirects to the admin dashboard with a flash message if an error 
134        occurs.
135        - Redirects to logout if the user is not an admin.
136    """
137    if current_user.role.role_name != "admin":
138        return redirect(url_for('logout'))
139     
140    try:
141        users = User.query.all()
142        return render_template(
143            'admin/manageUsers.html',
144            title='Manage Users',
145            users=users,
146        )
147    except Exception as e:
148        print('Error: ' + str(e))
149        flash("An error occured contact support.")
150        return redirect(url_for('admin_dashboard'))
151
152
153@app.route('/admin/view_user/<int:user_id>', methods=['GET'])
154@login_required
155def view_user(user_id):
156    """View details of a specific user.
157
158    Args:
159        user_id (int): ID of the user to view.
160    
161    Returns:
162        - Rendered template with user details.
163        - Redirects to logout if the user is not an admin.
164    """
165    if current_user.role.role_name != "admin":
166        return redirect(url_for('logout'))
167
168    user = User.query.get_or_404(user_id)
169
170    return render_template(
171        'admin/viewUser.html', 
172        title='View User', 
173        user=user,
174    )
175
176
177@app.route('/admin/edit_user/<int:user_id>', methods = ['GET', 'POST'])
178@login_required
179def edit_user(user_id):
180    """Edit details of a specific user.
181
182    Displays a form pre-populated with the user's current details.
183    Validates the form on submission, updating the user record in the database.
184
185    Details:
186        - If `password` is left blank, the existing hashed password is unchanged.
187        - The `manager` dropdown uses 0 to represent “None” (no manager).
188
189    Args:
190        user_id (int): ID of the user to edit.
191
192    Returns:
193        - Rendered template with the edit user form.
194        - Redirects to logout if the user is not an admin.
195        - Redirects to manage_users on successful update with a flash message.
196    """
197    if current_user.role.role_name != "admin":
198        return redirect(url_for('logout'))
199    
200    user = User.query.get_or_404(user_id)
201    form = EditUserForm(obj=user)
202    form.user_id = user.id
203
204    form.role.choices = [
205        (role.id, role.role_name) for role in Role.query.all()
206]
207    form.department.choices = [
208        (dept.id, dept.department_name) for dept in Department.query.all()
209    ]
210
211    manager_role = Role.query.filter_by(role_name='manager').first()
212    manager_choices = [(0, 'None')]
213    if manager_role:
214        manager_choices += [
215            (u.id, f"{u.first_name.title()} {u.surname.title()}")
216            for u in User.query.filter_by(role_id=manager_role.id)
217        ]
218    form.manager.choices = manager_choices
219  
220    if request.method == 'GET':
221        form.role.data = user.role_id
222        form.department.data = user.department_id
223        form.manager.data = user.manager_id or 0   
224        form.is_onboarding.data = 'yes' if user.is_onboarding else 'no'
225
226    if form.validate_on_submit():
227        user.first_name = form.first_name.data
228        user.surname = form.surname.data
229        user.username = form.username.data
230        user.role_id = int(form.role.data)
231        user.is_onboarding = (form.is_onboarding.data == 'yes')
232        user.manager_id = int(form.manager.data) if form.manager.data else None
233        user.department_id = int(form.department.data)
234        user.job_title = form.job_title.data
235
236        if form.password.data:
237            user.set_password(form.password.data)
238
239        if form.dateStarted.data:
240            user.dateStarted = form.dateStarted.data
241
242        db.session.commit()
243
244        flash(
245            f'User {user.first_name} {user.surname} '
246            'user details have been updated')
247        return redirect(url_for('manage_users'))
248    
249    return render_template(
250        'admin/editUser.html', 
251        title='Edit User', 
252        form=form, 
253        user=user,
254    )
255
256
257@app.route('/admin/delete_user/<int:user_id>', methods=['POST'])
258@login_required
259def delete_user(user_id):
260    """Delete a user from the system.
261
262    Details:
263        - Users are prevented from deleting their own account.
264
265    Args:
266        user_id (int): ID of the user to delete.
267
268    Raises:
269        404 error if the user does not exist.
270
271    Returns:
272        - Redirects to manage_users on successful deletion with a flash 
273        message.
274        - Redirects to manage_users with a flash message if the user tries
275        to delete their own account.
276        - Redirects to logout if the user is not an admin.
277    """
278    if current_user.role.role_name != 'admin':
279        return redirect(url_for('logout'))
280        
281    user = User.query.get_or_404(user_id)
282
283    if user.id == current_user.id:
284        flash("You cannot delete your own account.")
285        return redirect(url_for('manage_users'))
286
287    db.session.delete(user)
288    db.session.commit()
289    flash(f"User {user.first_name} {user.surname} has been deleted.")
290
291    return redirect(url_for('manage_users'))
292
293
294@app.route('/admin/create_training_module', methods = ['GET', 'POST'])
295@login_required
296def create_training_module():
297    """Create a new training module.
298
299    This route allows an admin to create a new training module, including
300    questions and options. It handles form submission, validation, and
301    associates the module with onboarding pathways. 
302
303    Details:
304        - A POST with `add_question` in the form will append an extra
305          question entry and re-render the form without saving.
306
307    Args:
308        None (form data submitted via POST).
309    
310    Returns:
311        - Redirects to manage_training_modules with a flash message on 
312        successful creation.
313        - Re-renders the “create” template if fields are invalid or when 
314        dynamically adding a question.
315        - Redirects to logout if the user is not an admin.
316    """
317    if current_user.role.role_name != "admin":
318        return redirect(url_for('logout'))
319
320    form = CreateTrainingModuleForm()
321    form.pathways.choices = [
322        (path.id, path.path_name) 
323        for path in OnboardingPath.query.all()
324    ]
325
326    if request.method == 'POST' and 'add_question' in request.form:
327        form.questions.append_entry()
328        return render_template(
329            'admin/create_training_module.html', 
330            title = 'Create Training Module', 
331            form = form
332        )
333
334    if form.validate_on_submit():
335        try:
336            training_module = TrainingModule(
337                module_title = form.module_title.data,
338                module_description = form.module_description.data,
339                module_instructions = form.module_instructions.data,
340                video_url = form.video_url.data or None
341            )
342            db.session.add(training_module)
343            db.session.flush()
344
345            for pathway_id in form.pathways.data:
346                pathway = OnboardingPath.query.get(pathway_id)
347                if pathway:
348                    onboarding_step = OnboardingStep(
349                        step_name = training_module.module_title,
350                        path = pathway,
351                        training_module = training_module
352                    )
353                    db.session.add(onboarding_step) 
354            
355            if not form.questions.data:
356                flash("You must add at least one question.")
357                db.session.rollback()
358                return redirect(url_for('create_training_module'))
359            
360            for question_form in form.questions:
361                question = Question(
362                    question_text = question_form.question_text.data,
363                    training_module = training_module
364                )
365                db.session.add(question)
366                db.session.flush()
367                for option_form in (
368                    question_form.option1, 
369                    question_form.option2, 
370                    question_form.option3, 
371                    question_form.option4,
372                ):
373                    option = Option(
374                        option_text = option_form.option_text.data,
375                        is_correct = option_form.is_correct.data,
376                        question = question,
377                    )
378                    db.session.add(option)
379
380            db.session.commit()  
381            flash(
382                f'Training module "{training_module.module_title}" has been '
383                'successfully created!')
384            return redirect(url_for('manage_training_modules'))
385    
386        except Exception as e:
387            db.session.rollback()
388            print(f'Error: {str(e)}')
389            flash("An error occurred, please contact support.")
390            return redirect('admin/create_training_module.html', title='Create Training Module', form=form)
391
392    return render_template('admin/create_training_module.html', title='Create Training Module', form=form)
393
394    
395
396@app.route('/admin/manage_training_modules', methods = ['GET'])
397@login_required
398def manage_training_modules():
399    """Display all active training modules.
400
401    Returns:
402        - Rendered template with a list of all active training modules.
403        - Redirects to logout if the user is not an admin.
404    """
405    if current_user.role.role_name != "admin":
406        return redirect(url_for('logout'))
407    
408    modules = TrainingModule.query.filter_by(active=True).all()
409    
410    return render_template(
411        'admin/manage_training_modules.html',
412        title = 'Manage Training Modules',
413        modules = modules
414    )
415
416
417@app.route('/admin/details_training_module/<int:module_id>', methods = ['GET'])
418@login_required
419def details_training_module(module_id):
420    """Display details of a specific training module.
421
422    Args:
423        module_id (int): ID of the training module to view.
424
425    Raises:
426        404 error if the module does not exist.
427
428    Returns:
429        - Rendered template with training module details and associated 
430        questions.
431        - Redirects to logout if the user is not an admin.
432    """
433    if current_user.role.role_name != "admin":
434        return redirect(url_for('logout'))
435    
436    module = TrainingModule.query.get_or_404(module_id)
437    
438    questions = Question.query.filter_by(training_module_id=module_id).all()
439    
440    return render_template(
441        'admin/details_training_module.html', 
442        title=f'{module.module_title} Details',
443        module=module,
444        questions=questions
445    )
446
447
448@app.route('/admin/edit_training_module/<int:module_id>', methods = ['GET', 'POST'])
449@login_required
450def edit_training_module(module_id):
451    """Edit an existing training module.
452
453    This route allows an admin to edit the details of a training module,
454    including its title, description, instructions, video URL, and associated
455    questions and options. It also allows the admin to assign the module to
456    specific onboarding pathways.
457
458    Details:
459        - OnboardingStep entries are cleared and re-created based on the form
460          submission.
461        - Questions and options are updated based on the form data.
462
463    Args:
464        module_id (int): ID of the training module to edit.
465    
466    Raises:
467        404 error if the module does not exist.
468
469    Returns:
470        - Rendered template with the edit form pre-populated with the
471        module's current details.
472        - Redirects to manage_training_modules on successful update with a
473        flash message.
474        - Re-renders the edit form if validation fails or on GET request.
475        - Redirects to logout if the user is not an admin.
476    """
477    if current_user.role.role_name != "admin":
478        return redirect(url_for('logout'))
479    
480    module = TrainingModule.query.get_or_404(module_id)
481
482    form = CreateTrainingModuleForm(obj=module)
483    form.pathways.choices = [
484        (path.id, path.path_name) for path in OnboardingPath.query.all()
485    ]
486
487    if request.method == 'GET':
488        form.questions.entries.clear()
489        for question in module.questions:
490            question_form = form.questions.append_entry()
491            question_form.question_text.data = question.question_text
492            for i, option in enumerate(question.options):
493                sub = getattr(question_form, f'option{i+1}')
494                sub.option_text.data = option.option_text
495                sub.is_correct.data  = option.is_correct
496
497        form.pathways.data = [
498            step.onboarding_path_id 
499            for step in module.onboarding_steps
500        ]
501
502    if form.validate_on_submit():
503        module.module_title = form.module_title.data
504        module.module_description = form.module_description.data
505        module.module_instructions = form.module_instructions.data
506        module.video_url = form.video_url.data or None
507
508        OnboardingStep.query.filter_by(training_module_id = module.id).delete()
509        for path_id in form.pathways.data:
510            path = OnboardingPath.query.get(path_id)
511            db.session.add(OnboardingStep(
512                step_name=module.module_title,
513                path=path,
514                training_module=module
515            ))
516        for i in range(len(module.questions)):
517            question_obj = module.questions[i]
518            question_form = form.questions[i]
519
520            question_obj.question_text = question_form.question_text.data
521
522            for j, option_obj in enumerate(question_obj.options):
523                option = getattr(question_form, f'option{j+1}')
524                option_obj.option_text = option.option_text.data
525                option_obj.is_correct  = option.is_correct.data
526
527        db.session.commit()
528        flash(f'"{module.module_title}" has been updated.')        
529        return redirect(url_for('manage_training_modules'))
530    elif request.method == 'POST':
531        flash('Please check form for errors.')
532
533    return render_template(
534        'admin/edit_training_module.html',            
535        title=f'Edit Module: {module.module_title}',
536        form=form,
537        module=module
538    )
539
540
541@app.route('/admin/delete_training_module/<int:module_id>', methods=['POST'])
542@login_required
543def delete_training_module(module_id):
544    """Deactivate a training module. 
545
546    This marks the module as inactive so it no longer appears in listings.
547    It does not delete the record from the database.
548
549    Args:
550        module_id (int): ID of the training module to be deleted.
551        
552    Raises:
553        404 error if the module does not exist.
554
555    Returns:
556        - Redirect to the training module management page with a success message.
557        - Redirects to logout if the user is not an admin.
558    """
559    if current_user.role.role_name != 'admin':
560        return redirect(url_for('logout'))
561
562    module = TrainingModule.query.get_or_404(module_id)
563    module.active = False
564
565    try:
566        db.session.commit()
567        flash(f'Module "{module.module_title}" has been deleted.')
568    except Exception as e:
569        db.session.rollback()
570        print(f'Failed to deactivate module {module_id}: {e}')
571        flash('An error occurred while deleting the module. Contact support.')
572
573    return redirect(url_for('manage_training_modules'))
@app.route('/admin/register', methods=['GET', 'POST'])
def register_user():
 27@app.route('/admin/register', methods = ['GET', 'POST'])
 28def register_user():
 29    """Register a new user.
 30
 31    Displays a form for creating a user, validates input, determines the
 32    appropriate onboarding path based on department, sets the password,
 33    and commits the new User record to the database.
 34
 35    Args:
 36        None (form data submitted via POST).
 37
 38    Returns:
 39        - Redirects to logout if the user is not an admin. 
 40        - Re-renders the registration form on GET or validation failure.
 41        - Redirects to manage_users on successful creation (with a flash
 42        message).
 43    """
 44    if not current_user.is_authenticated or current_user.role.role_name != 'admin':
 45        return redirect(url_for('logout'))
 46
 47    form = CreateUserForm()
 48
 49    form.role.choices = [(0, 'Select Role')] + [
 50        (role.id, role.role_name) for role in Role.query.all()
 51    ]
 52    form.department.choices = [(0, 'Select Department')] + [
 53        (dept.id, dept.department_name) for dept in Department.query.all()
 54    ]
 55
 56    manager_role = Role.query.filter_by(role_name='manager').first()
 57    manager_choices = [(0, 'None')]
 58    if manager_role:
 59        manager_choices += [
 60            (u.id, f"{u.first_name.title()} {u.surname.title()}")
 61            for u in User.query.filter_by(role_id=manager_role.id)
 62        ]
 63    form.manager.choices = manager_choices
 64
 65    if form.validate_on_submit():
 66        department_id = int(form.department.data)
 67
 68        if Department.query.filter_by(
 69            id=department_id, 
 70            department_name = "office",
 71        ).first():
 72            onboarding_path = OnboardingPath.query.filter_by(
 73                path_name = "office",
 74            ).first()
 75        else:
 76            onboarding_path = OnboardingPath.query.filter_by(   
 77                path_name = "operational",
 78            ).first()
 79       
 80        user = User(
 81            first_name = form.firstName.data,
 82            surname = form.surname.data,
 83            username = form.username.data,
 84            role_id = int(form.role.data),
 85            is_onboarding = (form.is_onboarding.data == 'yes'),
 86            manager_id = int(form.manager.data) if form.manager.data else None,
 87            department_id = int(form.department.data),
 88            onboarding_path_id = onboarding_path.id if onboarding_path else None,
 89            dateStarted = datetime.now(),
 90            job_title = form.job_title.data
 91        )
 92
 93        user.set_password(form.password.data)
 94        db.session.add(user)
 95        db.session.commit()
 96        flash(
 97            f'User {user.first_name} {user.surname} '
 98            'has been successfully registered!'
 99        )
100        return redirect(url_for('manage_users'))
101    
102    return render_template(
103        'admin/createUser.html', 
104        title='Register User', 
105        form=form,
106    )

Register a new user.

Displays a form for creating a user, validates input, determines the appropriate onboarding path based on department, sets the password, and commits the new User record to the database.

Arguments:
  • None (form data submitted via POST).
Returns:
  • Redirects to logout if the user is not an admin.
  • Re-renders the registration form on GET or validation failure.
  • Redirects to manage_users on successful creation (with a flash message).
@app.route('/admin/dashboard')
def admin_dashboard():
109@app.route('/admin/dashboard')
110@login_required
111def admin_dashboard():
112    """Display the admin dashboard.
113
114    Returns:
115        - Rendered admin dashboard template.
116        - Redirects to logout if the user is not an admin.
117    """
118    if current_user.role.role_name!= "admin":
119        return redirect(url_for('logout'))
120    
121    return render_template(
122        'admin/dashboard.html', 
123        title='Admin Dashboard',
124    )

Display the admin dashboard.

Returns:
  • Rendered admin dashboard template.
  • Redirects to logout if the user is not an admin.
@app.route('/admin/manage_users', methods=['GET'])
def manage_users():
127@app.route('/admin/manage_users', methods = ['GET'])
128@login_required
129def manage_users():
130    """Display all users in the system.
131
132    Returns:
133        - Rendered template with a list of all users.
134        - Redirects to the admin dashboard with a flash message if an error 
135        occurs.
136        - Redirects to logout if the user is not an admin.
137    """
138    if current_user.role.role_name != "admin":
139        return redirect(url_for('logout'))
140     
141    try:
142        users = User.query.all()
143        return render_template(
144            'admin/manageUsers.html',
145            title='Manage Users',
146            users=users,
147        )
148    except Exception as e:
149        print('Error: ' + str(e))
150        flash("An error occured contact support.")
151        return redirect(url_for('admin_dashboard'))

Display all users in the system.

Returns:
  • Rendered template with a list of all users.
  • Redirects to the admin dashboard with a flash message if an error occurs.
  • Redirects to logout if the user is not an admin.
@app.route('/admin/view_user/<int:user_id>', methods=['GET'])
def view_user(user_id):
154@app.route('/admin/view_user/<int:user_id>', methods=['GET'])
155@login_required
156def view_user(user_id):
157    """View details of a specific user.
158
159    Args:
160        user_id (int): ID of the user to view.
161    
162    Returns:
163        - Rendered template with user details.
164        - Redirects to logout if the user is not an admin.
165    """
166    if current_user.role.role_name != "admin":
167        return redirect(url_for('logout'))
168
169    user = User.query.get_or_404(user_id)
170
171    return render_template(
172        'admin/viewUser.html', 
173        title='View User', 
174        user=user,
175    )

View details of a specific user.

Arguments:
  • user_id (int): ID of the user to view.
Returns:
  • Rendered template with user details.
  • Redirects to logout if the user is not an admin.
@app.route('/admin/edit_user/<int:user_id>', methods=['GET', 'POST'])
def edit_user(user_id):
178@app.route('/admin/edit_user/<int:user_id>', methods = ['GET', 'POST'])
179@login_required
180def edit_user(user_id):
181    """Edit details of a specific user.
182
183    Displays a form pre-populated with the user's current details.
184    Validates the form on submission, updating the user record in the database.
185
186    Details:
187        - If `password` is left blank, the existing hashed password is unchanged.
188        - The `manager` dropdown uses 0 to represent “None” (no manager).
189
190    Args:
191        user_id (int): ID of the user to edit.
192
193    Returns:
194        - Rendered template with the edit user form.
195        - Redirects to logout if the user is not an admin.
196        - Redirects to manage_users on successful update with a flash message.
197    """
198    if current_user.role.role_name != "admin":
199        return redirect(url_for('logout'))
200    
201    user = User.query.get_or_404(user_id)
202    form = EditUserForm(obj=user)
203    form.user_id = user.id
204
205    form.role.choices = [
206        (role.id, role.role_name) for role in Role.query.all()
207]
208    form.department.choices = [
209        (dept.id, dept.department_name) for dept in Department.query.all()
210    ]
211
212    manager_role = Role.query.filter_by(role_name='manager').first()
213    manager_choices = [(0, 'None')]
214    if manager_role:
215        manager_choices += [
216            (u.id, f"{u.first_name.title()} {u.surname.title()}")
217            for u in User.query.filter_by(role_id=manager_role.id)
218        ]
219    form.manager.choices = manager_choices
220  
221    if request.method == 'GET':
222        form.role.data = user.role_id
223        form.department.data = user.department_id
224        form.manager.data = user.manager_id or 0   
225        form.is_onboarding.data = 'yes' if user.is_onboarding else 'no'
226
227    if form.validate_on_submit():
228        user.first_name = form.first_name.data
229        user.surname = form.surname.data
230        user.username = form.username.data
231        user.role_id = int(form.role.data)
232        user.is_onboarding = (form.is_onboarding.data == 'yes')
233        user.manager_id = int(form.manager.data) if form.manager.data else None
234        user.department_id = int(form.department.data)
235        user.job_title = form.job_title.data
236
237        if form.password.data:
238            user.set_password(form.password.data)
239
240        if form.dateStarted.data:
241            user.dateStarted = form.dateStarted.data
242
243        db.session.commit()
244
245        flash(
246            f'User {user.first_name} {user.surname} '
247            'user details have been updated')
248        return redirect(url_for('manage_users'))
249    
250    return render_template(
251        'admin/editUser.html', 
252        title='Edit User', 
253        form=form, 
254        user=user,
255    )

Edit details of a specific user.

Displays a form pre-populated with the user's current details. Validates the form on submission, updating the user record in the database.

Details:
  • If password is left blank, the existing hashed password is unchanged.
  • The manager dropdown uses 0 to represent “None” (no manager).
Arguments:
  • user_id (int): ID of the user to edit.
Returns:
  • Rendered template with the edit user form.
  • Redirects to logout if the user is not an admin.
  • Redirects to manage_users on successful update with a flash message.
@app.route('/admin/delete_user/<int:user_id>', methods=['POST'])
def delete_user(user_id):
258@app.route('/admin/delete_user/<int:user_id>', methods=['POST'])
259@login_required
260def delete_user(user_id):
261    """Delete a user from the system.
262
263    Details:
264        - Users are prevented from deleting their own account.
265
266    Args:
267        user_id (int): ID of the user to delete.
268
269    Raises:
270        404 error if the user does not exist.
271
272    Returns:
273        - Redirects to manage_users on successful deletion with a flash 
274        message.
275        - Redirects to manage_users with a flash message if the user tries
276        to delete their own account.
277        - Redirects to logout if the user is not an admin.
278    """
279    if current_user.role.role_name != 'admin':
280        return redirect(url_for('logout'))
281        
282    user = User.query.get_or_404(user_id)
283
284    if user.id == current_user.id:
285        flash("You cannot delete your own account.")
286        return redirect(url_for('manage_users'))
287
288    db.session.delete(user)
289    db.session.commit()
290    flash(f"User {user.first_name} {user.surname} has been deleted.")
291
292    return redirect(url_for('manage_users'))

Delete a user from the system.

Details:
  • Users are prevented from deleting their own account.
Arguments:
  • user_id (int): ID of the user to delete.
Raises:
  • 404 error if the user does not exist.
Returns:
  • Redirects to manage_users on successful deletion with a flash message.
  • Redirects to manage_users with a flash message if the user tries to delete their own account.
  • Redirects to logout if the user is not an admin.
@app.route('/admin/create_training_module', methods=['GET', 'POST'])
def create_training_module():
295@app.route('/admin/create_training_module', methods = ['GET', 'POST'])
296@login_required
297def create_training_module():
298    """Create a new training module.
299
300    This route allows an admin to create a new training module, including
301    questions and options. It handles form submission, validation, and
302    associates the module with onboarding pathways. 
303
304    Details:
305        - A POST with `add_question` in the form will append an extra
306          question entry and re-render the form without saving.
307
308    Args:
309        None (form data submitted via POST).
310    
311    Returns:
312        - Redirects to manage_training_modules with a flash message on 
313        successful creation.
314        - Re-renders the “create” template if fields are invalid or when 
315        dynamically adding a question.
316        - Redirects to logout if the user is not an admin.
317    """
318    if current_user.role.role_name != "admin":
319        return redirect(url_for('logout'))
320
321    form = CreateTrainingModuleForm()
322    form.pathways.choices = [
323        (path.id, path.path_name) 
324        for path in OnboardingPath.query.all()
325    ]
326
327    if request.method == 'POST' and 'add_question' in request.form:
328        form.questions.append_entry()
329        return render_template(
330            'admin/create_training_module.html', 
331            title = 'Create Training Module', 
332            form = form
333        )
334
335    if form.validate_on_submit():
336        try:
337            training_module = TrainingModule(
338                module_title = form.module_title.data,
339                module_description = form.module_description.data,
340                module_instructions = form.module_instructions.data,
341                video_url = form.video_url.data or None
342            )
343            db.session.add(training_module)
344            db.session.flush()
345
346            for pathway_id in form.pathways.data:
347                pathway = OnboardingPath.query.get(pathway_id)
348                if pathway:
349                    onboarding_step = OnboardingStep(
350                        step_name = training_module.module_title,
351                        path = pathway,
352                        training_module = training_module
353                    )
354                    db.session.add(onboarding_step) 
355            
356            if not form.questions.data:
357                flash("You must add at least one question.")
358                db.session.rollback()
359                return redirect(url_for('create_training_module'))
360            
361            for question_form in form.questions:
362                question = Question(
363                    question_text = question_form.question_text.data,
364                    training_module = training_module
365                )
366                db.session.add(question)
367                db.session.flush()
368                for option_form in (
369                    question_form.option1, 
370                    question_form.option2, 
371                    question_form.option3, 
372                    question_form.option4,
373                ):
374                    option = Option(
375                        option_text = option_form.option_text.data,
376                        is_correct = option_form.is_correct.data,
377                        question = question,
378                    )
379                    db.session.add(option)
380
381            db.session.commit()  
382            flash(
383                f'Training module "{training_module.module_title}" has been '
384                'successfully created!')
385            return redirect(url_for('manage_training_modules'))
386    
387        except Exception as e:
388            db.session.rollback()
389            print(f'Error: {str(e)}')
390            flash("An error occurred, please contact support.")
391            return redirect('admin/create_training_module.html', title='Create Training Module', form=form)
392
393    return render_template('admin/create_training_module.html', title='Create Training Module', form=form)

Create a new training module.

This route allows an admin to create a new training module, including questions and options. It handles form submission, validation, and associates the module with onboarding pathways.

Details:
  • A POST with add_question in the form will append an extra question entry and re-render the form without saving.
Arguments:
  • None (form data submitted via POST).
Returns:
  • Redirects to manage_training_modules with a flash message on successful creation.
  • Re-renders the “create” template if fields are invalid or when dynamically adding a question.
  • Redirects to logout if the user is not an admin.
@app.route('/admin/manage_training_modules', methods=['GET'])
def manage_training_modules():
397@app.route('/admin/manage_training_modules', methods = ['GET'])
398@login_required
399def manage_training_modules():
400    """Display all active training modules.
401
402    Returns:
403        - Rendered template with a list of all active training modules.
404        - Redirects to logout if the user is not an admin.
405    """
406    if current_user.role.role_name != "admin":
407        return redirect(url_for('logout'))
408    
409    modules = TrainingModule.query.filter_by(active=True).all()
410    
411    return render_template(
412        'admin/manage_training_modules.html',
413        title = 'Manage Training Modules',
414        modules = modules
415    )

Display all active training modules.

Returns:
  • Rendered template with a list of all active training modules.
  • Redirects to logout if the user is not an admin.
@app.route('/admin/details_training_module/<int:module_id>', methods=['GET'])
def details_training_module(module_id):
418@app.route('/admin/details_training_module/<int:module_id>', methods = ['GET'])
419@login_required
420def details_training_module(module_id):
421    """Display details of a specific training module.
422
423    Args:
424        module_id (int): ID of the training module to view.
425
426    Raises:
427        404 error if the module does not exist.
428
429    Returns:
430        - Rendered template with training module details and associated 
431        questions.
432        - Redirects to logout if the user is not an admin.
433    """
434    if current_user.role.role_name != "admin":
435        return redirect(url_for('logout'))
436    
437    module = TrainingModule.query.get_or_404(module_id)
438    
439    questions = Question.query.filter_by(training_module_id=module_id).all()
440    
441    return render_template(
442        'admin/details_training_module.html', 
443        title=f'{module.module_title} Details',
444        module=module,
445        questions=questions
446    )

Display details of a specific training module.

Arguments:
  • module_id (int): ID of the training module to view.
Raises:
  • 404 error if the module does not exist.
Returns:
  • Rendered template with training module details and associated questions.
  • Redirects to logout if the user is not an admin.
@app.route('/admin/edit_training_module/<int:module_id>', methods=['GET', 'POST'])
def edit_training_module(module_id):
449@app.route('/admin/edit_training_module/<int:module_id>', methods = ['GET', 'POST'])
450@login_required
451def edit_training_module(module_id):
452    """Edit an existing training module.
453
454    This route allows an admin to edit the details of a training module,
455    including its title, description, instructions, video URL, and associated
456    questions and options. It also allows the admin to assign the module to
457    specific onboarding pathways.
458
459    Details:
460        - OnboardingStep entries are cleared and re-created based on the form
461          submission.
462        - Questions and options are updated based on the form data.
463
464    Args:
465        module_id (int): ID of the training module to edit.
466    
467    Raises:
468        404 error if the module does not exist.
469
470    Returns:
471        - Rendered template with the edit form pre-populated with the
472        module's current details.
473        - Redirects to manage_training_modules on successful update with a
474        flash message.
475        - Re-renders the edit form if validation fails or on GET request.
476        - Redirects to logout if the user is not an admin.
477    """
478    if current_user.role.role_name != "admin":
479        return redirect(url_for('logout'))
480    
481    module = TrainingModule.query.get_or_404(module_id)
482
483    form = CreateTrainingModuleForm(obj=module)
484    form.pathways.choices = [
485        (path.id, path.path_name) for path in OnboardingPath.query.all()
486    ]
487
488    if request.method == 'GET':
489        form.questions.entries.clear()
490        for question in module.questions:
491            question_form = form.questions.append_entry()
492            question_form.question_text.data = question.question_text
493            for i, option in enumerate(question.options):
494                sub = getattr(question_form, f'option{i+1}')
495                sub.option_text.data = option.option_text
496                sub.is_correct.data  = option.is_correct
497
498        form.pathways.data = [
499            step.onboarding_path_id 
500            for step in module.onboarding_steps
501        ]
502
503    if form.validate_on_submit():
504        module.module_title = form.module_title.data
505        module.module_description = form.module_description.data
506        module.module_instructions = form.module_instructions.data
507        module.video_url = form.video_url.data or None
508
509        OnboardingStep.query.filter_by(training_module_id = module.id).delete()
510        for path_id in form.pathways.data:
511            path = OnboardingPath.query.get(path_id)
512            db.session.add(OnboardingStep(
513                step_name=module.module_title,
514                path=path,
515                training_module=module
516            ))
517        for i in range(len(module.questions)):
518            question_obj = module.questions[i]
519            question_form = form.questions[i]
520
521            question_obj.question_text = question_form.question_text.data
522
523            for j, option_obj in enumerate(question_obj.options):
524                option = getattr(question_form, f'option{j+1}')
525                option_obj.option_text = option.option_text.data
526                option_obj.is_correct  = option.is_correct.data
527
528        db.session.commit()
529        flash(f'"{module.module_title}" has been updated.')        
530        return redirect(url_for('manage_training_modules'))
531    elif request.method == 'POST':
532        flash('Please check form for errors.')
533
534    return render_template(
535        'admin/edit_training_module.html',            
536        title=f'Edit Module: {module.module_title}',
537        form=form,
538        module=module
539    )

Edit an existing training module.

This route allows an admin to edit the details of a training module, including its title, description, instructions, video URL, and associated questions and options. It also allows the admin to assign the module to specific onboarding pathways.

Details:
  • OnboardingStep entries are cleared and re-created based on the form submission.
  • Questions and options are updated based on the form data.
Arguments:
  • module_id (int): ID of the training module to edit.
Raises:
  • 404 error if the module does not exist.
Returns:
  • Rendered template with the edit form pre-populated with the module's current details.
  • Redirects to manage_training_modules on successful update with a flash message.
  • Re-renders the edit form if validation fails or on GET request.
  • Redirects to logout if the user is not an admin.
@app.route('/admin/delete_training_module/<int:module_id>', methods=['POST'])
def delete_training_module(module_id):
542@app.route('/admin/delete_training_module/<int:module_id>', methods=['POST'])
543@login_required
544def delete_training_module(module_id):
545    """Deactivate a training module. 
546
547    This marks the module as inactive so it no longer appears in listings.
548    It does not delete the record from the database.
549
550    Args:
551        module_id (int): ID of the training module to be deleted.
552        
553    Raises:
554        404 error if the module does not exist.
555
556    Returns:
557        - Redirect to the training module management page with a success message.
558        - Redirects to logout if the user is not an admin.
559    """
560    if current_user.role.role_name != 'admin':
561        return redirect(url_for('logout'))
562
563    module = TrainingModule.query.get_or_404(module_id)
564    module.active = False
565
566    try:
567        db.session.commit()
568        flash(f'Module "{module.module_title}" has been deleted.')
569    except Exception as e:
570        db.session.rollback()
571        print(f'Failed to deactivate module {module_id}: {e}')
572        flash('An error occurred while deleting the module. Contact support.')
573
574    return redirect(url_for('manage_training_modules'))

Deactivate a training module.

This marks the module as inactive so it no longer appears in listings. It does not delete the record from the database.

Arguments:
  • module_id (int): ID of the training module to be deleted.
Raises:
  • 404 error if the module does not exist.
Returns:
  • Redirect to the training module management page with a success message.
  • Redirects to logout if the user is not an admin.