How to edit the template and show the editable view and final preview using django

Feb. 4, 2024

14 min read

Django, a high-level web framework for Python, provides a robust and efficient way to build web applications. In this article, we'll explore how to create editable views and final previews in Django, allowing users to edit content and view a final preview before saving changes.
We will implement the below points.

  1. How to create the bootstrap templates in django super admin.
  2. How to edit the inbuilt template.
  3. show the editable and final preview of the template after edit and saving the form data.

Firstly we will create the relevant tables to store the templates and form data.

class RLOLetterTemplate(models.Model):
    name = models.CharField(max_length=100, null=True)
    site_address_info = models.TextField(null=True)
    company_info = models.TextField(null=True)
    main_content_block = models.TextField(null=True)
    complete_template = models.TextField(null=True)
    class Meta:
        verbose_name = _('RLO Letter Template')
        verbose_name_plural = _('RLO Letter Templates')

    def __str__(self):

In the above model:-

  1. name is used to store the name of the template.
  2. site_address_info: used to store the address of the site.
  3. company_info : used to store the information about the company mentioned in the template.
  4. main_content_block: used to store the main content of the template.
  5. complete_template: store the complete bootstrap template.

We divide the whole template into blocks per my template's requirement. you can design the template according to your needs and requirements of that particular project.

    ('approved', 'Approved'),
    ('pending', 'Pending'),
    ('rejected', 'Rejected')

class RLO(models.Model):
    user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='rlo_user')
    name = models.CharField(max_length=100, null=True)
    status = models.CharField(max_length=30, choices=RLO_STATUS_CHOICES, default='pending')
    job = models.ForeignKey(Job, on_delete=models.CASCADE)
    base_template = models.ForeignKey(RLOLetterTemplate, on_delete=models.CASCADE, null=True)
    edited_content =  models.TextField(blank=True, null=True)  # New field to store edited template content 
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    class Meta:
        verbose_name = _('RLO')
        verbose_name_plural = _('RLO')
        ordering =['id']

    def __str__(self):

Here’s a breakdown of its fields:

  • user_id: A foreign key linking to a User model, representing the user who created the RLO.
  • name: A character field to store the name or title of the RLO (allowing up to 100 characters).
  • status: A character field with choices from RLO_STATUS_CHOICES, indicating the current status of the RLO (defaulting to ‘pending’).
  • job: A foreign key linking to a Job model, representing the job associated with the RLO.
  • base_template: A foreign key linking to an RLOLetterTemplate model, representing a base template used for the RLO (optional).
  • edited_content: A text field to store any edited content for the template (optional).
  • created_at: A DateTimeField that automatically records the date and time when the RLO was created.
  • updated_at: A DateTimeField that automatically updates with the date and time whenever the RLO is modified.

2. Create the serializer for the RLO model. 

from rest_framework import serializers
from .models import *

class JobField(serializers.PrimaryKeyRelatedField):
    Primary key related field for Job model.

    This field is used for relating Job model instances to other models.

        get_queryset: Get the queryset for Job instances.
    def get_queryset(self):
        Get the queryset for Job instances.

            QuerySet: A queryset of Job instances filtered by status.
        return Job.objects.exclude(quotation__status='approved')

class RLOAddSerializer(serializers.ModelSerializer):
    Serializer for adding a new RLO.

    - name - name of RLO
    - job - job associated with RLO.
    name = serializers.CharField(
        label=('RLO Name'),
            'base_template': 'custom_input.html'
            "required": "This field is required.",
            "blank": "RLO Name is required.",
            "null": "RLO Name is required."
    job = JobField(
        label=('RLO Job'),
        allow_null=True,  # Set allow_null to True
        style={'base_template': 'custom_select.html',
               "autofocus": False,
               "autocomplete": "off",
            "required": "This field is required.",
            "blank": "RLO Job is required.",
    class Meta:
        model = RLO
        fields = ('name','job')

class RLOLetterTemplateSerializer(serializers.ModelSerializer):
    class Meta:
        model = RLOLetterTemplate
        fields = '__all__'

In the above serializer JobField, the query set returns the quotations from the job model where the status is approved and uses these approved quotations.

quotation and Job model are here for reference:-

class Quotation(models.Model):
    user_id = models.ForeignKey(User, on_delete=models.CASCADE, 
                                verbose_name="Created By", related_name="created_quotations", null=True)
    customer_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name="customer_quotations")
    requirement_id = models.ForeignKey(Requirement, on_delete=models.CASCADE, related_name="quotations")
    report_id = models.ForeignKey(Report, on_delete=models.CASCADE, related_name="quotations")
    defect_id = models.ManyToManyField(RequirementDefect, blank=True)
    quotation_json = models.JSONField()  # This field stores JSON data
    total_amount = models.DecimalField(max_digits=10, decimal_places=2,null=True, blank=True)
    status = models.CharField(max_length=30, choices=QUOTATION_STATUS_CHOICES, default='draft')
    submitted_at = models.DateTimeField(auto_now_add=True, null=True, blank=True)
    pdf_path = models.CharField(max_length=500,null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering =['id']

class Job(models.Model):
    quotation = models.ForeignKey(Quotation, on_delete=models.CASCADE)  
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        max_length = 30  # Adjust this to your desired character count
        action_text = self.quotation.requirement_id.action[:max_length]

        if len(self.quotation.requirement_id.action) > max_length:
            action_text += "..."
        return action_text
    class Meta:
        ordering =['id']

Now create the management folder in your app and create the template using management commands.

# management/commands/
from import BaseCommand
from work_planning_management.models import RLOLetterTemplate
import os
from django.apps import apps

class Command(BaseCommand):
    help = 'Create RLO Letter Templates from HTML data'

    def handle(self, *args, **options):
        # Define the output folder for generated HTML files
        site_address_info = """
         <div class="col-12">
                                <img src="" alt="Company Logo"  style="max-height: 4rem;">
        <p class="text-wrap">The Resident</p>
        <p class="text-wrap">Flat 1</p>
        <p class="text-wrap">Antler House</p>
        <p class="text-wrap">Kielder Close</p>
        <p class="text-wrap">Ilford</p>
        <p class="text-wrap">IG6 3ER</p>

        company_info = """
        <div class="col-12">
                                <img src="" alt="Company Logo"  style="max-height: 4rem;">
        <p class="text-wrap">Infinity Fire Prevention Ltd</p>
        <p class="text-wrap">Infinity House 38 Riverside</p>
        <p class="text-wrap">Sir Thomas Longley Road</p>
        <p class="text-wrap">Rochester</p>
        <p class="text-wrap">Kent</p>
        <p class="text-wrap">ME2 4DP</p>

        main_content_block = """
        <p><b>Date:</b> 9th March 2023,</p>
                            <p>Dear Resident,</p>
                            <p><b>Re: Communal Fire Safety Works</b></p>
                            <p>As part of the current fire regulations &amp; your safety, we have been instructed by Thanet District
                                Council, your landlord/housing provider, to carry out Fire Safety Works in your Communal Loft
                            <p>We need to carry out these essential Fire Safety Works in your Communal Loft Spaces and work will
                                commence from 29 th August 2023. We do not need to access the Loft spaces via your Flat Entrance
                                but there may be some noise caused by the works.</p>
                            <p>If you have any questions, please contact us on:</p>
                            <p>Either the door choice form can be handed to the surveyor, or it can be forwarded to our Resident Liaison
                                Officers email address:</p>
                            <p>Phone: 0800 112 0404</p>
                            <p>Should you require verification of this letter please contact Customer Services at Thanet District
                                Council on 01843 577000</p>
                            <p>We would like to apologise in advance for any inconvenience caused but it is necessary for these
                                essential works to be carried out and your co-operation would be greatly appreciated.</p>
                            <p>Yours faithfully,</p>
                            <p><b>The RLO Team</b></p>
                            <p><b>Sureserve Fire &amp; Electrical</b></p>

        # Merge the complete template by concatenating the sections
        complete_template = f"""
        <div class="container">
            <div class="row">
                <div class="col-12">
                    <div class="card">
                        <div class="card-header">
                            <div class="d-flex justify-content-between align-items-center">
                                <h5 class="card-title">Communal without appointment thanet</h5>
                        <div class="card-body">
                            <div class="row">
                                <div class="col-6">
                                <div class="col-6" style="text-align: right;">
                            <div class="row mt-4">
                                <div class="col-12">

        # Create an instance of RLOLetterTemplate with the complete template
        template = RLOLetterTemplate.objects.create(
            name='Communal without appointment thanet Letter',
            complete_template = complete_template
        self.stdout.write('Successfully created RLO Letter Template: {}'))
        # self.stdout.write('Successfully generated HTML page: {output_filename}'))

To create these templates in admin we have to apply the command:-

python create_template1

This command will create a bootstrap template in your django admin. You can create multiple templates in this way and we will further use these templates in RLO form.

Now create the template to show the form.

{% extends 'base.html' %}
{% load static %}
{% block title %} RLO Form {{ block.super }}{% endblock %}
{% block content %}
{% load custom_tags %}
{% load rest_framework %}
{% include 'components/alert.html' %}

{% block extra_css %}
  /* Add your custom styles here */
{% endblock %}

<div class="container-fluid my-3 py-3">
  <div class="row">
    <div class="col-lg-12">
      <!-- Card Basic Info -->
      <div class="card" id="basic-info">
        <div class="card-header">
          <h5>RLO Form</h5>
        <div class="card-body pt-0">
          <form role="form" method="POST" enctype="multipart/form-data" onsubmit="showLoader()">
            <div class='row'>
              {% csrf_token %}
              {% render_form serializer %}

            <div class="error-message">
              <p class="text-secondary text-sm">Hint: You must select the sample template to add the RLO.</p>

            <!-- Add a hidden input field for template_id -->
            <input type="hidden" name="template_id" id="templateId" >
            <input type="hidden" name="edited_content" id="editedContent">

            <button type="button" class="btn bg-gradient-primary btn-sm float-start mt-2 mb-0"
              data-bs-toggle="modal" data-bs-target="#templateModal">
              Sample Template

            <button type="button" class="btn bg-gradient-dark btn-sm float-end mt-2 mb-0 mx-2" onclick="saveChanges()">

            <a class="btn bg-gradient-secondary btn-sm float-end mt-2 mb-0" href="{% url 'rlo_list' %}">Back</a>

          {% include 'components/loader.html' %}

  <div class="modal fade" id="templateModal" tabindex="-1" aria-labelledby="templateModalLabel" aria-hidden="true">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title" id="templateModalLabel">Select a Template</h5>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        <div class="modal-body">
          <div class="template-options">
            {% for template in templates %}
            <div class="form-check">
              <input class="form-check-input" type="radio" name="template_id" id="template{{ forloop.counter }}"
                value="{{ }}">
              <label class="form-check-label" for="template{{ forloop.counter }}">
                {{ }}
            {% endfor %}
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">
          <button class="btn bg-gradient-dark btn-sm" onclick="openCompleteTemplate()">
            Open Complete Template

<div class="container-fluid" style="max-width:none;">
  <div class="row">
    <div class="col-6">
      <div class="section-header" id="editableHeader" style="display: none;background-color:#8fa0be66;
        border-radius:5px;padding:5px 5px 5px 5px;">
        <h5>Editable Format</h5>

      <!-- CKEditor container -->
      <div id="editor" style="width: 100%; height: 100vh; height: 100vh; overflow: hidden; border: none; outline: none;"></div>
    <div class="col-6">
      <div class="section-header" id="previewHeader" style="display: none;background-color:#8fa0be66;
        border-radius:5px;padding:5px 5px 5px 5px;">
        <h5>Final Preview</h5>

      <!-- Final Preview iframe -->
      <iframe id="target" style="width:100%; height: 595px;"></iframe>


{% endblock %}
{% block extra_js %}
<script src=""></script>
<script src=""></script>
<script src="{% static 'assets/js/plugins/perfect-scrollbar.min.js' %}"></script>
<script src="{% static 'assets/js/plugins/smooth-scrollbar.min.js' %}"></script>
<script src="{% static 'assets/js/plugins/datatables.js' %}"></script>
<script src="{% static 'assets/js/plugins/sweetalert.min.js' %}"></script>
<script src="{% static 'assets/js/custom_delete.js' %}"></script>

    var csrf_token = "{{ csrf_token }}";

  function openCompleteTemplate() {
    var selectedTemplateId = document.querySelector('input[name="template_id"]:checked').value;
    var templateIdField = document.getElementById('templateId');
    var editedContentField = document.getElementById('editedContent');

    if (!selectedTemplateId) {
      alert("Please select a template.");

    templateIdField.value = selectedTemplateId;

    var xhr = new XMLHttpRequest();'GET', '/work_planning/RLO/get_template_content/?template_id=' + selectedTemplateId, true);

    xhr.onload = function () {
      if (xhr.status === 200) {
        var response = JSON.parse(xhr.responseText);
        var templateContent = response.template_content;

        var editorContainer = document.getElementById('editor');
        var editableHeader = document.getElementById('editableHeader');
        var previewHeader = document.getElementById('previewHeader'); = "block"; = "block"; = "block";

        // Initialize CKEditor
        CKEDITOR.replace('editor', {
            // Add any CKEditor configurations as needed
            toolbar: 'basic',
            height: 500,
            iconSize: 'small',  // Set the icon size to 'small'
          }).on('change', function () {
        // Set the value of the CKEditor instance

        // Set the value in the hidden input field
        editedContentField.value = templateContent;

        // Trigger the change event to update the iframe

        // Close the modal
      } else {
        alert('Error fetching template content.');


  // Function to strip HTML tags from a string
  function stripHtmlTags(html) {
    var div = document.createElement("div");
    div.innerHTML = html;
    return div.textContent || div.innerText;

  // Handle the onchange event of both the CKEditor and editable format div
  function change() {
    var iFrame = document.getElementById('target');
    var iFrameBody;

    if (iFrame.contentDocument) {
      iFrameBody = iFrame.contentDocument.getElementsByTagName('body')[0];
    } else if (iFrame.contentWindow) {
      iFrameBody = iFrame.contentWindow.document.getElementsByTagName('body')[0];

    // Get the content from CKEditor
    var editorContent = CKEDITOR.instances.editor.getData();

    // Update the content of the iframe
    iFrameBody.innerHTML = editorContent;

  function saveChanges() {
    // Get the edited content from CKEditor
    var editedContent = CKEDITOR.instances.editor.getData();

    // Get the selected template ID
    var selectedTemplateId = document.getElementById('templateId').value;
     // Get the form data
  var formData = new FormData(document.querySelector('form'));
    // You can access the values of name and job fields using formData
    var name = formData.get('name');
    var job = formData.get('job');

    // You can send the edited content and template ID to the server using an AJAX request
    // For example, using jQuery AJAX
      type: "POST",
      url: "/work_planning/RLO/add/",
      data: {
        name: name,
        job: job,
        template_id: selectedTemplateId,
        edited_content: editedContent,
        csrfmiddlewaretoken: '{{ csrf_token }}',
        // Add any additional data if required by your view
      success: function (response) {
        // Handle the success response, if needed
        // Redirect or perform any other actions as necessary
        window.location.href = '{% url "rlo_list" %}';
      error: function (error) {
        // Handle the error, if needed
        // You might want to display an error message to the user
        alert("Error saving changes. Please try again.");
{% endblock %}
  1. Form for RLO Submission: The template includes a form for submitting RLO. It includes input fields, text, and hidden input fields for template ID and edited content. It also includes buttons for selecting a sample template, saving changes, and navigating back.
  2. Modal for Selecting a Template: The template includes a modal that appears when selecting a sample template. It provides radio buttons for choosing a template and buttons for closing the modal or opening the complete template.
  3. Editor and Preview Sections: There are two sections: one for editing content using CKEditor and another for previewing the final result in an iframe.
  4. JavaScript Section: The template includes several JavaScript functions. Notable functions include openCompleteTemplate, which fetches and displays template content, change, which updates the preview based on CKEditor changes, and save changes, which saves the edited content and template ID to the server using AJAX.
  5. Script Tags for JavaScript Libraries: The template includes script tags for jQuery, CKEditor, and other JavaScript libraries required for the functionality.
  6. use the {% render_form serializer %} to render the form created in the file

You can use the template of your choice, as per my project requirements I have used the Bootstrap theme here.

Now create the view:-

class RLOAddView(generics.CreateAPIView):
    View for adding  a RLO ADD.
    Supports both HTML and JSON response formats.
    serializer_class = RLOAddSerializer
    renderer_classes = [TemplateHTMLRenderer, JSONRenderer]
    template_name = 'RLO/rlo_form.html'

    def get(self, request, *args, **kwargs):
        Handle GET request to display a form for adding a RLO.
        If the RLO exists, retrieve the serialized data and render the HTML template.
        If the RLO does not exist, render the HTML template with an empty serializer.
        # Filter the queryset based on the user ID
        serializer = self.serializer_class(context={'request': request})
        templates = RLOLetterTemplate.objects.all()

        # Check if a template has been selected
        selected_template_id = request.GET.get('template_id')
        selected_template = None

        if selected_template_id:
                selected_template = RLOLetterTemplate.objects.get(id=selected_template_id)
            except RLOLetterTemplate.DoesNotExist:
        if request.accepted_renderer.format == 'html':
            context = {'serializer':serializer,'templates':templates,
                                   'selected_template': selected_template,

            return render_html_response(context,self.template_name)
            return create_api_response(status_code=status.HTTP_201_CREATED,
                                    message="GET Method Not Alloweded",)

    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(
        # Get the selected template ID from the request data
        template_id ='template_id')

        if not template_id:
            # If 'template_id' is not provided, return an error response
            if request.accepted_renderer.format == 'html':
                messages.error(request, "Please select a template.")
                return redirect(reverse('rlo_add'))
                return create_api_response(status_code=status.HTTP_400_BAD_REQUEST, message="Please select a template.")
        message = "Your RLO has been added successfully."
        # breakpoint()
        if serializer.is_valid():
            serializer.validated_data['user_id'] = request.user  # Assign the current user instance
            rlo =

            # Check if the template_id is valid
                template = RLOLetterTemplate.objects.get(id=template_id)
            except RLOLetterTemplate.DoesNotExist:
                template = None

            # Update the RLO instance with the selected template (if valid)
            if template:

                rlo.base_template = template
            # Save the edited template content to the RLO instance
            edited_content ='edited_content')
            if edited_content is not None:
                rlo.edited_content = edited_content
                rlo.edited_content = template.complete_template

            if request.accepted_renderer.format == 'html':
                messages.success(request, message)
                return redirect(reverse('rlo_list'))
                # Return JSON response with success message and serialized data.
                return create_api_response(status_code=status.HTTP_201_CREATED, message=message,

            # Return JSON response with error message.
            return create_api_response(status_code=status.HTTP_400_BAD_REQUEST,
                                    message="We apologize for the inconvenience, but please review the below information.",

def get_template_content(request):
    if request.method == 'GET':
        template_id = request.GET.get('template_id')

            template = RLOLetterTemplate.objects.get(pk=template_id)
            template_content = template.complete_template
            # Use BeautifulSoup to remove HTML tags
            # soup = BeautifulSoup(template_content, 'html.parser')
            # plain_text = soup.get_text()

            return JsonResponse({'template_content': template_content})
        except RLOLetterTemplate.DoesNotExist:
            return JsonResponse({'error': 'Template not found'}, status=404)

get method:-

  • Handles the GET request to display a form for adding an RLO.
  • Retrieves RLO templates and checks if a template has been selected.
  • Renders the HTML template with the appropriate context.

Post method:-

  • Handles the POST request for adding an RLO.
  • Validates the serializer, assigns the current user instance to the RLO, and saves the RLO instance.
  • Checks if the selected template ID is valid and updates the RLO instance with the template and edited content.
  • Renders success or error messages based on the response format.

Overall, the class RLOAddView is responsible for rendering the RLO form, handling form submissions, and adding RLO instances to the database. The get_template_content function serves as an endpoint for retrieving template content asynchronously.

Create the url for the above view:-

from .views import *
from django.urls import path

path('RLO/list/', RLOListView.as_view(), name='rlo_list'),

The above-mentioned URL is for the redirecting page where we want to redirect the user after saving the form.
and the second URL is for the add RLO form. But you can redirect the use on the page page as well.

Output of the above code:-

when we click on the select template button it will show the editable and final preview of the template.

It shows the live final preview as you start editing in the editable template it reflects all the changes in the final preview and saves the different copy of the inbuilt template in the RLO model.

django Template edit Appreciate you stopping by my post! 😊

Add a comment

Note: If you use these tags, write your text inside the HTML tag.
Login Required
Author's profile
Profile Image

Abdulla Fajal

Django Developer

With '' under my care, I, Abdulla Fajal, graciously invite your insights and suggestions, as we endeavour to craft an exquisite online experience together.