Mastering Complex DataTables with Django: A Deep Dive into Server-Side Processing

Saurav SharmaSaurav Sharma
6 min read

Hey fellow Django enthusiasts! Today, I'm excited to share a recent challenge I tackled involving Django and DataTables. This one's a bit complex, so grab your favorite caffeinated beverage and let's dive in!

but first, here is a screencast of how it looks like in action when i implemented it my project. i have replace the project code with some generic example code because i can’t share the project code as it’s confidential.

The Challenge: your company have a blog management system that required displaying a table of blog posts with various attributes. they need server-side processing to handle potentially thousands of posts efficiently, with dynamic columns based on user permissions, and complex sorting and filtering capabilities.

The Solution: Let's break this down step-by-step:

  1. Setting Up the Frontend: First, I set up DataTables on the client side:
// This setup enables server-side processing 
$(document).ready(function () {
  let host_url = window.location.origin;
  var dataTable = $('#blog-posts-table').DataTable({
    serverSide: true,
    sAjaxSource: `${host_url}/api/blog-posts-data`,
    paging: true,
    processing: true,
    // disables ordering on the last two columns (action buttons).
    columnDefs: [
      { orderable: false, targets: -1 },
      { orderable: false, targets: -2 }
    ]
  });
});
  1. Django View Magic: The real complexity lay in the Django view. Here's the complete view.
@login_required
@user_passes_test(lambda u: u.has_perm("view_blog_posts"))
def blog_posts_datatables_api_view(request):
    request_data = request.GET.copy()
    # we need to return the value of draw in int format
    # it is strongly recommended for security reasons that 
    # you cast this parameter to an integer, rather than simply 
    # echoing back to the client what it sent in the draw parameter, 
    # in order to prevent Cross Site Scripting (XSS) attacks
    request_data["draw"] = int(request_data.get("draw", 1))
    user = request.user
    blog = Blog.objects.for_user(user)
    approved_categories = get_approved_categories(request, blog)

    post_columns = _get_post_columns(blog)
    order_by_list = _get_col_list_for_ordering(request.GET, post_columns)

    search_query = request.GET.get("sSearch")
    entries_per_page = int(request.GET.get("iDisplayLength", 10))
    entries_start = int(request.GET.get("iDisplayStart", 0))

    search_db_query = Q()
    if search_query:
        search_db_query = (
            Q(author__first_name__icontains=search_query)
            | Q(author__last_name__icontains=search_query)
            | Q(title__icontains=search_query)
            | Q(content__icontains=search_query)
        )

    posts = (
        blog.blogpost_set.filter(
            deleted_on__isnull=True,
            draft=False,
            author__is_active=True,
            category__in=approved_categories,
        )
        .select_related("author", "category")
        .order_by(*order_by_list)
    )

    request_data["recordsTotal"] = posts.count()

    if search_query:
        posts = posts.filter(search_db_query)

    request_data["recordsFiltered"] = posts.count()

    posts = posts[entries_start : entries_start + entries_per_page]

    data = []
    for post in posts:
        _post_data = []
        _author_link = ""
        if blog.show_author_id:
            if post.author.id:
                _author_link = f'<a href="{post.author.get_absolute_url()}">{post.author.id}</a>'
                _post_data.append(_author_link)
                _post_data.append(post.author.get_full_name())

        if not _author_link:
            _post_data.append(f'<a href="{post.author.get_absolute_url()}">{post.author.get_full_name()}</a>')

        _post_data.append(post.title)
        _post_data.append(post.category.__str__())
        _post_data.append(post.published_date.strftime("%Y-%m-%d %H:%M:%S"))
        _post_data.append(post.view_count)
        _post_data.append(yesno(post.comments_enabled, "Enabled,Disabled"))

        edit_url = reverse(
            "blog:post-update",
            kwargs={"post_id": post.id},
        )
        _post_data.append(f'<a href="{edit_url}"><span class="material-symbols-outlined md-18">edit</span></a>')

        toggle_comments_url = reverse(
            "blog:toggle-post-comments",
            kwargs={"post_id": post.id},
        )
        comments_text = yesno(post.comments_enabled, "comments_disabled,comments")
        title = "Disable Comments" if post.comments_enabled else "Enable Comments"
        _post_data.append(
            f'<a href="{toggle_comments_url}"><span title="{title}" class="material-symbols-outlined">{comments_text}</span></a>'
        )

        data.append(_post_data)

    request_data["data"] = data
    return JsonResponse(request_data)

Key Points:

  • We're copying the request data and modifying it throughout the view.

  • Dynamic column handling based on blog settings.

  • Complex filtering using Q objects.

  • Efficient querying with select_related.

  • Pagination handled server-side.

  • Custom data formatting for each post, including HTML for action buttons.

Now there are few params sent in request by client & here is the info about them.

Here's the formatted table in Markdown:

Parameter nameTypeDescription
drawintegerThe draw counter that this object is a response to - from the draw parameter sent as part of the data request. Note that it is strongly recommended for security reasons that you cast this parameter to an integer, rather than simply echoing back to the client what it sent in the draw parameter, in order to prevent Cross Site Scripting (XSS) attacks.
recordsTotalintegerTotal records, before filtering (i.e. the total number of records in the database)
recordsFilteredintegerTotal records, after filtering (i.e. the total number of records after filtering has been applied - not just the number of records being returned for this page of data).
dataarrayThe data to be displayed in the table. This is an array of data source objects, one for each row, which will be used by DataTables. Note that this parameter's name can be changed using the ajax option's dataSrc property.
errorstringOptional: If an error occurs during the running of the server-side processing script, you can inform the user of this error by passing back the error message to be displayed using this parameter. Do not include if there is no error.

there are other params also which you can read in detail here - https://datatables.net/manual/server-side#Returned-data

  1. Helper Functions: I created helper functions to keep the main view clean:
def _get_post_columns(blog):
    post_columns = []
    if blog.show_author_id:
        post_columns.append("author__id")
    post_columns.extend([
        "author__first_name",
        "title",
        "category",
        "published_date",
        "view_count",
        "comments_enabled"
    ])
    return post_columns

def _get_col_list_for_ordering(request_data, post_columns):
    order_by_list = []
    for key, value in request_data.items():
        if key.startswith("iSortCol_"):
            col_index = value
            col_name_key = f"mDataProp_{col_index}"
            col_name = request_data.get(col_name_key)
            col_name = post_columns[int(col_name)]
            sort_dir_key = f'sSortDir_{key.split("_")[1]}'
            sort_dir = request_data.get(sort_dir_key, "asc")
            if sort_dir == "desc":
                col_name = f"-{col_name}"
            order_by_list.append(col_name)
    return order_by_list or [post_columns[0]]

This function creates a list of columns to use in the queryset's order_by() method:

  • It iterates through the request data (which comes from DataTables).

  • It looks for keys starting with "iSortCol_", which indicate sorting columns.

  • For each sorting column:

    • It gets the column index and finds the corresponding column name.

    • It determines the sort direction (ascending or descending).

    • If descending, it prepends a minus sign to the column name.

    • It adds this to the order_by_list.

  • If no sorting is specified, it defaults to the first column in post_columns.

This function effectively translates DataTables' sorting parameters into Django ORM ordering syntax, allowing for dynamic, multi-column sorting as specified by the user in the frontend.

  1. Template Changes: In the HTML template, I set up the table structure:
<table id="blog-posts-table" class="table table-striped table-hover table-sm table-responsive">
  <thead>
    <tr>
      {% if blog.show_author_id %}
        <th>ID</th>
      {% endif %}
      <th>AUTHOR</th>
      <th>TITLE</th>
      <th>CATEGORY</th>
      <th>PUBLISHED DATE</th>
      <th>VIEWS</th>
      <th>COMMENTS</th>
      <th><span class="material-symbols-outlined md-18">edit</span></th>
      <th><span title="Toggle Comments" class="material-symbols-outlined">comments</span></th>
    </tr>
  </thead>
  <tbody></tbody>
</table>
  1. URL Configuration: I added a new URL pattern for the API view:
path(
    "api/blog-posts-data",
    views.blog_posts_datatables_api_view,
    name="blog-posts-datatables-data",
),

The Result: The end product was a highly efficient and flexible blog post management table. It loads quickly, even with thousands of posts, and provides smooth sorting, searching, and pagination - all handled server-side.

Key Takeaways:

  1. Copying and modifying the request data allows for flexible response handling.

  2. Server-side processing is crucial for large datasets.

  3. Dynamic column handling requires careful planning but offers great flexibility.

  4. Efficient database querying (using select_related and filter) is key to maintaining performance.

  5. Breaking down complex logic into helper functions improves readability and maintainability.

  6. Handling HTML generation server-side for action buttons allows for dynamic permissions and URLs.

This solution has been a game-changer for my blog management project. It's scalable, efficient, and provides a great user experience while handling complex data structures and permissions.

Have you tackled similar challenges with large, dynamic datasets in Django? I'd love to hear your approaches and any tips you might have!

p.s - i am still learning and chances are there may be better approach than this to implement server side datatables. i just shared my way of doing this but if i found a new better way then i will mention that here.

0
Subscribe to my newsletter

Read articles from Saurav Sharma directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Saurav Sharma
Saurav Sharma

I am a Self Taught Backend developer With 3 Years of Experience. Currently, I am working at a tech Startup based in The Bahamas. Here are my skills so far - 💪Expert at - 🔹Python 🔹Django 🔹Django REST framework 🔹Celery ( for distributed tasks ) 🔹ORM ( Know how to write fast queries & design models ) 🔹Django 3rd party packages along with postgresQL and mysql as Databases. 🔹Cache using Redis & Memcache 🔹Numpy + OpenCV for Image Processing 🔹ElasticSearch + HayStack 🔹Linux ( Debian ) 😎 Working Knowledge - Html, CSS, JavaScript, Ajax, Jquery, Git ( GitHub & BitBucket ), Basic React & React Native, Linux ( Arch ), MongoDB, VPS 🤠 Currently Learning - 🔹More Deep Dive into Django 🔹Docker 🔹Making APIs more Robust 🔹NeoVim 🔹System Design ☺️ Will Be Learn in upcoming months - 🔹GraphQL 🔹 Rust language Other than above, there is not a single technology ever exists that i can't master if needed.