Классы Python для реализации таблицы данных AJAX в Django

Привет. В этом тексте я не буду расписывать все пошагово. Вместо того, чтобы писать шаг за шагом, я объясню в нескольких строках, что я сделал для реализации динамических таблиц данных, созданных с помощью DataTables.js.

Я создал репо, чтобы поделиться примером кода. Вы можете получить доступ к примеру, используя ссылку ниже.



Фабрика таблиц данных

Спасибо за этот класс, мы можем легко получить соответствующий класс DataTableFactory. По умолчанию я создал фабричный класс для подключения к AWS. Вы можете напрямую представить свои данные SQL с помощью этого класса.

import pandas as pd
from .factories.aws_dtfactory import AWSDataTableFactory


class DataTableFactory(object):
    """Data Table Factory class to generate a related sub data table factory class

    """

    @staticmethod
    def get_factory(query_engine):
        """Get a query_engine related data table factory object

        Args:
            query_engine (str): [aws] To get related factory class
        Returns:
            an object that is created by using query_engine related data table factory class
        """
        if query_engine == "aws":
            return AWSDataTableFactory(query_engine)
        else:
            return pd.DataFrame()

Фабрика таблиц данных AWS

Используя этот класс, мы можем легко фильтровать запрошенные данные с помощью параметров фильтра AJAX. Если вы хотите подробно изучить метод фильтрации, вы можете посмотреть метод filter_by_request_args.

from sqlalchemy import create_engine
import pandas as pd


def get_redshift_con():
    user = ""
    password = ""
    host = ""
    port = ""
    database = ""
    return create_engine(
        f'postgresql+psycopg2://{user}:{password}@{host}:{port}/{database}'
    )


class AWSDataTableFactory(object):
    def __init__(self, query_engine):
        """Create an AWSDataTableFactory object

        AWS specific data table data generator
        Args:
            query_engine (str): "aws"
        """
        self.query_engine = query_engine
        self.query = None
        self.columns = []
        self.displayed_columns = []

    def set_query(self, query):
        """Set factory's query

        Args:
            query (str): The query will be executed

        Returns:
            It doesn't return anything. It sets the factory's query.
        """
        self.query = query
        self.set_columns()

    def set_columns(self):
        """Set factory's columns

        Returns:
            It doesn't return anything. It sets the factory's columns.
        """
        limited_query = self.query.split('limit')[0]
        limited_query += " limit 1"
        res = self.get_data(limited_query)
        self.columns = [col for col in res.columns]
        self.displayed_columns = self.columns[:4]

    def set_displayed_columns(self, column_list):
        """Set factory's displayed columns

        Args:
            column_list (list): List of columns
        """
        self.displayed_columns = column_list

    def get_displayed_columns_for_data_table(self):
        """Get data table columns for AJAX request

        Returns:
            list: List of dictionaries
        """
        return [{'data': col} for col in self.displayed_columns]

    def get_data(self, query=None, *args, **kwargs):
        """Get (extract) data from the source by executing the query with the engine

        Args:
            query (str): The query will be executed
            *args:
            **kwargs:

        Returns:
            table_data (pd.DataFrame): Returns the data that extracted from AWS
        """
        if self.query is None:
            raise NotImplementedError(
                'Firstly you have to set a query for the engine')
        con = get_redshift_con()
        if query is None:
            table_data = pd.read_sql(con=con, sql=self.query, *args, **kwargs)
        else:
            table_data = pd.read_sql(con=con, sql=query, *args, **kwargs)
        return table_data

    def filter_by_request_args(self, **kwargs):
        """Filter and sort the data by using request.GET parameters

        Args:
            **kwargs: request.GET

        Returns:
            dict: to use in the frontend with ajax data table requests
        """
        draw = int(kwargs.get('draw', None)[0])
        length = int(kwargs.get('length', None)[0])
        start = int(kwargs.get('start', None)[0])
        search_value = kwargs.get('search[value]', None)[0]
        order_column = kwargs.get('order[0][column]', None)[0]
        order = kwargs.get('order[0][dir]', None)[0]  # asc or desc
        factory_filters = [f for f in kwargs if f.startswith('factory_filter')]
        order_column = self.columns[int(order_column)]
        queryset = self.get_data()

        ############ factory_filters applying ############
        if len(factory_filters) > 0:
            for ff in factory_filters:
                query_col = ff.split('factory_filter-')[1]
                query_val = kwargs.get(ff)[0]
                if query_val != 'All':
                    queryset = queryset[queryset[query_col] == query_val]

        total = len(queryset)
        if search_value:
            queryset = queryset[queryset.apply(lambda row: row.astype(
                str).str.contains(search_value).any(), axis=1)]

        count = len(queryset)

        queryset.sort_values(
            by=order_column, ascending=False if order == 'desc' else True, inplace=True)

        queryset = queryset[self.displayed_columns]

        return {
            'data': queryset,
            'count': count,
            'total': total,
            'draw': draw,
            'start': start,
            'length': length
        }

Представление таблицы данных

Это наше представление, основанное на классе. В этом классе мы используем параметры, поступающие из urls.py.

from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.views import View
from .utilities.dtfactory import DataTableFactory
from django.utils.decorators import method_decorator
from django.core.exceptions import PermissionDenied
from django.http import JsonResponse


@method_decorator(login_required, name="dispatch")
class DataTableView(View):
    """Data table class based view
    This view can get the parameters below in the as_view function  to use in the view
    Args:
        page_name (str): To display in page
        query_engine (str): [aws] To create a DataTableFactory for querying the data source
        query (str): To execute the query by using the connection that is created with query_engine
        query_parameters (dict): Additional parameters to pass into to exec_sql method of the factory
        permission (str): The needed permission to access the url
    """
    page_name = ""
    query_engine = ""
    query = ""
    query_parameters = {}
    permission = ""
    filters = []

    def get(self, request, *args, **kwargs):
        # todo: kolon seçimi eklenecek
        data_table_factory = DataTableFactory.get_factory(self.query_engine)
        data_table_factory.set_query(self.query)

        if request.GET.get('ajax_factory_loader', None) is not None:
            filter_args = data_table_factory.filter_by_request_args(
                **request.GET)
            data = filter_args['data'].to_dict(orient='records')
            data = data[filter_args['start']                        :filter_args['start'] + filter_args['length']]
            result = {'data': data, 'draw': filter_args['draw'], 'recordsTotal': filter_args['total'],
                      'recordsFiltered': filter_args['count']}
            return JsonResponse(result)
        return render(
            request,
            "datagateway/data_table.html",
            context={"page_name": self.page_name,
                     'filters': self.filters, 'factory': data_table_factory},
        )

    def dispatch(self, request, *args, **kwargs):
        if not request.user.has_perm(self.permission):
            raise PermissionDenied(
                "You do not have permission to view this page")
        return super().dispatch(request, *args, **kwargs)

urls.py

В этом файле мы передаем параметры для использования в представлении на основе классов. Мы можем определить необходимые разрешения, механизм запросов, запросы и статические фильтры и т. д.

from django.urls import path
from . import views

app_name = "dtfactory"

urlpatterns = [
    path(
        "",
        views.DataTableView.as_view(
            page_name="Data Table Demo",
            query_engine="aws",
            query=f"select * from QUERY",
            permission="custom_app.can_view_data_table_page",
            filters=[
                {'filter_column': 'custom_region', 'display_name': 'Region', 'type': 'static',
                 'filter_values': ['All', 'TR', 'EN']}
            ]
        ),
        name="dt_demo",
    ),
]

Файл шаблона

В этом файле мы используем переданные заводские параметры, поступающие из бэкенда.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Data Table Demo</title>
    <link
      rel="stylesheet"
      href="cdn.datatables.net/1.13.1/css/jquery.dataTables.min.css"
    />
  </head>

  <body>
    <table id="demoDT">
      <thead>
        <tr>
          {% for col in factory.displayed_columns %}
          <th>{{ col }}</th>
          {% endfor %}
        </tr>
      </thead>

      <tbody></tbody>
    </table>

    <script src="cdn.datatables.net/1.13.1/js/jquery.dataTables.min.js"></script>
    <script>
      $(document).ready(function () {
          function customExportAction(e, dt, button, config) {
              var self = this;
              var oldStart = dt.settings()[0]._iDisplayStart;
              dt.one('preXhr', function (e, s, data) {
                  // Just this once, load all data from the server...
                  data.start = 0;
                  data.length = 2147483647;
                  dt.one('preDraw', function (e, settings) {
                      // Call the original action function
                      if (button[0].className.indexOf('buttons-copy') >= 0) {
                          $.fn.dataTable.ext.buttons.copyHtml5.action.call(self, e, dt, button, config);
                      } else if (button[0].className.indexOf('buttons-excel') >= 0) {
                          $.fn.dataTable.ext.buttons.excelHtml5.available(dt, config) ?
                              $.fn.dataTable.ext.buttons.excelHtml5.action.call(self, e, dt, button, config) :
                              $.fn.dataTable.ext.buttons.excelFlash.action.call(self, e, dt, button, config);
                      } else if (button[0].className.indexOf('buttons-csv') >= 0) {
                          $.fn.dataTable.ext.buttons.csvHtml5.available(dt, config) ?
                              $.fn.dataTable.ext.buttons.csvHtml5.action.call(self, e, dt, button, config) :
                              $.fn.dataTable.ext.buttons.csvFlash.action.call(self, e, dt, button, config);
                      } else if (button[0].className.indexOf('buttons-pdf') >= 0) {
                          $.fn.dataTable.ext.buttons.pdfHtml5.available(dt, config) ?
                              $.fn.dataTable.ext.buttons.pdfHtml5.action.call(self, e, dt, button, config) :
                              $.fn.dataTable.ext.buttons.pdfFlash.action.call(self, e, dt, button, config);
                      } else if (button[0].className.indexOf('buttons-print') >= 0) {
                          $.fn.dataTable.ext.buttons.print.action(e, dt, button, config);
                      }
                      dt.one('preXhr', function (e, s, data) {
                          // DataTables thinks the first item displayed is index 0, but we're not drawing that.
                          // Set the property to what it was before exporting.
                          settings._iDisplayStart = oldStart;
                          data.start = oldStart;
                      });
                      // Reload the grid with the original page. Otherwise, API functions like table.cell(this) don't work properly.
                      setTimeout(dt.ajax.reload, 0);
                      // Prevent rendering of the full data to the DOM
                      return false;
                  });
              });
              // Requery the server with the new one-time export settings
              dt.ajax.reload();
          }

          $("#demoDT").DataTable({
                  processing: true,
                  serverSide: true,
                  lengthChange: true,
                  ajax: {
                      url: "{{ request.get_full_path }}", // send ajax request to itself for filtering and paginating the data
                       data: {ajax_factory_loader:true},
                  },
                  columns: {{ factory.get_displayed_columns_for_data_table | safe }},
                  "dom": 'Blfrtip',
                  "buttons": [
                      {
                          extend: 'excel',
                          text: 'Excel',
                          filename: "{{page_name}}",
                          header: true,
                          title: "{{page_name}}",
                          "action": customExportAction
                      },
                      {
                          extend: 'csv',
                          text: 'CSV',
                          filename: "{{page_name}}",
                          header: true,
                          title: "{{page_name}}",
                          "action": customExportAction
                      },
                      {
                          extend: 'pdf',
                          text: 'PDF',
                          filename: "{{page_name}}",
                          header: true,
                          title: "{{page_name}}",
                          "action": customExportAction
                      },
                  ],
              })
              .buttons()
              .container()
              .appendTo("#download-action-area")
      });
    </script>
  </body>
</html>

Окончательно

Надеюсь, это поможет вам использовать DataTables.js в ваших проектах Django. Я знаю, что это слишком коротко. Однако я считаю, что вы можете понять код, просто взглянув на него. Если вы хотите расширить фабрику, вам следует реализовать свои собственные фабрики в классе DataTableFactory.

С уважением