diff --git a/SumasenLibs/excel_lib/docker/docker-compose.yml b/SumasenLibs/excel_lib/docker/docker-compose.yml index 6355d86..e0b1267 100644 --- a/SumasenLibs/excel_lib/docker/docker-compose.yml +++ b/SumasenLibs/excel_lib/docker/docker-compose.yml @@ -9,6 +9,6 @@ services: - ..:/app environment: - PYTHONPATH=/app - command: /bin/bash + command: python ./testdata/sample.py tty: true diff --git a/SumasenLibs/excel_lib/docker/python/Dockerfile b/SumasenLibs/excel_lib/docker/python/Dockerfile index 01d4495..f7a1e5d 100644 --- a/SumasenLibs/excel_lib/docker/python/Dockerfile +++ b/SumasenLibs/excel_lib/docker/python/Dockerfile @@ -2,10 +2,10 @@ FROM python:3.9-slim WORKDIR /app -# 必要なシステムパッケージのインストール -RUN apt-get update && apt-get install -y \ - git \ - && rm -rf /var/lib/apt/lists/* +# GPGキーの更新とパッケージのインストール +RUN apt-get update --allow-insecure-repositories && \ + apt-get install -y --allow-unauthenticated python3-dev libpq-dev && \ + rm -rf /var/lib/apt/lists/* # Pythonパッケージのインストール COPY requirements.txt . @@ -16,12 +16,11 @@ COPY . . RUN pip install --no-cache-dir -r requirements.txt # 開発用パッケージのインストール -RUN pip install --no-cache-dir \ +RUN pip install --no-cache-dir --upgrade pip \ pytest \ pytest-cov \ flake8 - # パッケージのインストール RUN pip install -e . diff --git a/SumasenLibs/excel_lib/requirements.txt b/SumasenLibs/excel_lib/requirements.txt index 3522b34..8938e66 100644 --- a/SumasenLibs/excel_lib/requirements.txt +++ b/SumasenLibs/excel_lib/requirements.txt @@ -2,4 +2,4 @@ openpyxl>=3.0.0 pandas>=1.0.0 pillow>=8.0.0 configparser>=5.0.0 - +psycopg2-binary==2.9.9 diff --git a/SumasenLibs/excel_lib/sumaexcel/config_handler.py b/SumasenLibs/excel_lib/sumaexcel/config_handler.py new file mode 100644 index 0000000..d6f266a --- /dev/null +++ b/SumasenLibs/excel_lib/sumaexcel/config_handler.py @@ -0,0 +1,166 @@ +# config_handler.py +# +import configparser +import os +from typing import Any, Dict, Optional + +import configparser +import os +import re +from typing import Any, Dict, Optional + +class ConfigHandler: + """変数置換機能付きの設定ファイル管理クラス""" + + def __init__(self, ini_file_path: str, variables: Dict[str, str] = None): + """ + Args: + ini_file_path (str): INIファイルのパス + variables (Dict[str, str], optional): 置換用の変数辞書 + """ + self.ini_file_path = ini_file_path + self.variables = variables or {} + self.config = configparser.ConfigParser() + self.load_config() + + def _substitute_variables(self, text: str) -> str: + """ + テキスト内の変数を置換する + + Args: + text (str): 置換対象のテキスト + + Returns: + str: 置換後のテキスト + """ + # ${var}形式の変数を置換 + pattern1 = r'\${([^}]+)}' + # [var]形式の変数を置換 + pattern2 = r'\[([^\]]+)\]' + + def replace_var(match): + var_name = match.group(1) + return self.variables.get(var_name, match.group(0)) + + # 両方のパターンで置換を実行 + text = re.sub(pattern1, replace_var, text) + text = re.sub(pattern2, replace_var, text) + + return text + + def load_config(self) -> None: + """設定ファイルを読み込み、変数を置換する""" + if not os.path.exists(self.ini_file_path): + raise FileNotFoundError(f"設定ファイルが見つかりません: {self.ini_file_path}") + + # まず生のテキストとして読み込む + with open(self.ini_file_path, 'r', encoding='utf-8') as f: + content = f.read() + + # 変数を置換 + substituted_content = self._substitute_variables(content) + + # 置換済みの内容を StringIO 経由で configparser に読み込ませる + from io import StringIO + self.config.read_file(StringIO(substituted_content)) + + def get_value(self, section: str, key: str, default: Any = None) -> Optional[str]: + """ + 指定されたセクションのキーの値を取得する + + Args: + section (str): セクション名 + key (str): キー名 + default (Any): デフォルト値(オプション) + + Returns: + Optional[str]: 設定値。存在しない場合はデフォルト値 + """ + try: + return self.config[section][key] + except KeyError: + return default + + def get_section(self, section: str) -> Dict[str, str]: + """ + 指定されたセクションの全ての設定を取得する + + Args: + section (str): セクション名 + + Returns: + Dict[str, str]: セクションの設定をディクショナリで返す + """ + try: + return dict(self.config[section]) + except KeyError: + return {} + + def get_all_sections(self) -> Dict[str, Dict[str, str]]: + """ + 全てのセクションの設定を取得する + + Returns: + Dict[str, Dict[str, str]]: 全セクションの設定をネストされたディクショナリで返す + """ + return {section: dict(self.config[section]) for section in self.config.sections()} + + + + + +# 使用例 +if __name__ == "__main__": + # サンプルのINIファイル作成 + sample_ini = """ +[Database] +host = localhost +port = 5432 +database = mydb +user = admin +password = secret + +[Application] +debug = true +log_level = INFO +max_connections = 100 + +[Paths] +data_dir = /var/data +log_file = /var/log/app.log +""" + + # サンプルINIファイルを作成 + with open('config.ini', 'w', encoding='utf-8') as f: + f.write(sample_ini) + + # 設定を読み込んで使用 + config = ConfigHandler('config.ini') + + # 特定の値を取得 + db_host = config.get_value('Database', 'host') + db_port = config.get_value('Database', 'port') + print(f"Database connection: {db_host}:{db_port}") + + # セクション全体を取得 + db_config = config.get_section('Database') + print("Database configuration:", db_config) + + # 全ての設定を取得 + all_config = config.get_all_sections() + print("All configurations:", all_config) + + +# サンプル: +# # 設定ファイルから値を取得 +# config = ConfigHandler('config.ini') +# +# # データベース設定を取得 +# db_host = config.get_value('Database', 'host') +# db_port = config.get_value('Database', 'port') +# db_name = config.get_value('Database', 'database') +# +# # アプリケーション設定を取得 +# debug_mode = config.get_value('Application', 'debug') +# log_level = config.get_value('Application', 'log_level') +# diff --git a/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py b/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py index 24581e5..26841e3 100644 --- a/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py +++ b/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py @@ -10,183 +10,360 @@ import shutil from datetime import datetime from typing import Optional, Dict, Any, Union, Tuple from pathlib import Path +# psycopg2のインポートを追加 +import psycopg2 +import psycopg2.extras + +from .config_handler import ConfigHandler # ini file のロード from .styles import StyleManager from .merge import MergeManager from .image import ImageManager from .conditional import ConditionalFormatManager from .page import PageManager, PaperSizes +import logging class SumasenExcel: """Enhanced Excel handling class with extended functionality""" - def __init__(self, debug: bool = False): - self.debug = debug + def __init__(self, document: str, variables: Dict[str, Any], + docbase: str = "./docbase") -> Dict[str, str]: + + """Initialize Excel document with basic settings + + Args: + document: Document name + variables: Variables for document + docbase: Base directory for documents (default: "./docbase") + + Returns: + Dict with status (true/false) and optional error message + + """ + self.debug = True + self.workbook = None self.template_filepath = None self.output_filepath = None self.current_sheet = None + self.dbname = None + self.user = None + self.password = None + self.host = None + self.port = None + self._style_manager = None self._merge_manager = None self._image_manager = None self._conditional_manager = None self._page_manager = None - def init(self, username: str, project_id: str, document: str, - lang: str = "jp", docbase: str = "./docbase") -> Dict[str, str]: - """Initialize Excel document with basic settings - - Args: - username: User name for file operations - project_id: Project identifier - document: Document name - lang: Language code (default: "jp") - docbase: Base directory for documents (default: "./docbase") - - Returns: - Dict with status ("ACK"/"NCK") and optional error message - """ try: - self.username = username - self.project_id = project_id - self.language = lang - - # Setup directory structure + # document base を設定 self.docpath = docbase - self.docpath2 = f"{docbase}/{project_id}" + if not os.path.exists(docbase): + logging.error(f"Document base directory not found: {docbase}") + + # ini fileをロード + self.ini_file_path = f"{docbase}/{document}.ini" + self.conf = ConfigHandler(self.ini_file_path, variables) + if not os.path.exists(self.ini_file_path): + logging.error(f"INI file not found: {self.ini_file_path}") - # Create directories if they don't exist - for path in [docbase, self.docpath2]: - if not os.path.exists(path): - os.makedirs(path, mode=0o755) + # basic section をロード + basic = self.conf.get_section("basic") + if not basic: + logging.error(f"Basic section not found in INI file: {self.ini_file_path}") - # Load template - inifile = f"{document}.ini" - self.inifilepath = f"{self.docpath2}/{inifile}" + self.basic = basic - if not os.path.exists(self.inifilepath): - return {"status": "NCK", "message": f"INI file not found: {self.inifilepath}"} + # basicセクションから必要なパラメータを取得 + template_file = basic.get("template_file") + if not template_file: + logging.error("template_file not found in basic section") - # Load template workbook - template_file = self._get_ini_param("basic", f"templatefile_{lang}") - self.template_filepath = f"{self.docpath2}/{template_file}" + doc_file = basic.get("doc_file") + if not doc_file: + logging.error("doc_file not found in basic section") + self.maxcol = basic.get("maxcol") + if not self.maxcol: + self.maxcol = 100 # デフォルト値を設定 + logging.warning("maxcol not found in basic section. Defaulting to 100") + else: + self.maxcol = int(self.maxcol) + + sections = basic.get("sections") + if not sections: + logging.error("sections not found in basic section") + + + # セクションをリストに変換 + self.section_list = [s.strip() for s in sections.split(",")] + if not self.section_list: + logging.error("sections not found in basic section") + + # 出力ファイルパスを設定 + self.output_filepath = f"{self.docpath}/{doc_file}" + # 新規のExcelワークブックを作成 + self.workbook = openpyxl.Workbook() + # デフォルトで作成されるシートを削除 + self.workbook.remove(self.workbook.active) + + # テンプレートワークブックをロード + self.template_filepath = f"{self.docpath}/{template_file}" if not os.path.exists(self.template_filepath): - # Copy from default if not exists - default_template = f"{self.docpath}/{template_file}" - shutil.copy2(default_template, self.template_filepath) + logging.error(f"Template file not found: {self.template_filepath}") - self.workbook = openpyxl.load_workbook(self.template_filepath) - - return {"status": "ACK"} + self.template_workbook = openpyxl.load_workbook(self.template_filepath) + self.template_sheet = self.template_workbook.active except Exception as e: - return {"status": "NCK", "message": str(e)} + logging.error(f"Error initializing Excel document: {str(e)}") + + def make_report(self,variables: Dict[str, Any]): + """レポートを生成する""" + logging.info("make_report step-1") + try: + ret_status = True + # セクションごとに処理を実行(ワークシート単位) + for section in self.section_list: + ret = self.proceed_section(section,variables) + if ret["status"] == False: + message = ret.get("message", "No message provided") + return {"status": False, "message": f"Fail generating section: {section}...{message}"} - def _get_ini_param(self, section: str, param: str) -> Optional[str]: - """Get parameter from INI file + # 生成したワークブックを保存 + self.workbook.save(self.output_filepath) + return {"status": True, "message": f"Report generated successfully : {self.output_filepath}", "filepath":self.output_filepath} - Args: - section: INI file section - param: Parameter name + except Exception as e: + return {"status": False, "message": f"Exception in make_report: Error generating report: {str(e)}"} + + def proceed_section(self, section: str, variables: Dict[str, Any]): + logging.info(f"make_report.proceed_section step-1:section={section}") - Returns: - Parameter value or None if not found - """ try: - # Use configparser to handle INI files - import configparser - config = configparser.ConfigParser() - config.read(self.inifilepath) - return config[section][param] - except: - return None + # セクションの設定を取得 + section_config = self.conf.get_section(section) + # セクションが存在しない場合はスキップ + if not section_config: + return {"status": False, "message": f"Error no section found: {section}"} - def make_report(self, db, data_rec: Dict[str, Any], - out_filename: Optional[str] = None, - screen_index: int = 0) -> None: - """Generate Excel report from template + # テンプレートシートをコピー + template_sheet_name = section_config.get("template_sheet") + if not template_sheet_name or template_sheet_name not in self.template_workbook.sheetnames: + return {"status": False, "message": f"Error no template sheet found: {template_sheet_name}"} + # シートの名前を設定 + new_sheet = self.workbook.create_sheet(title=section_config.get("sheet_name", section)) + self.worksheet = new_sheet + + self.dbname=variables.get('db'), + self.user=variables.get('username'), + self.password=variables.get('password'), + self.host=variables.get('host'), + self.port=variables.get('port') + if not self.dbname or not self.user or not self.password or not self.host or not self.port: + return {"status": False, "message": f"Error no database connection information"} + + # PostgreSQLに接続 + self.conn = psycopg2.connect( + self.dbname, + self.user, + self.password, + self.host, + self.port + ) + + self._style_manager = StyleManager() + self._merge_manager = MergeManager(self.current_sheet) + self._image_manager = ImageManager(self.current_sheet) + self._conditional_manager = ConditionalFormatManager(self.current_sheet) + self._page_manager = PageManager(self.current_sheet) + + # シートの幅を設定 + fit_to_width = section_config.get("fit_to_width") + if fit_to_width: + new_sheet.sheet_view.zoomScaleNormal = float(fit_to_width) + + # シートの向きを設定 + orientation = section_config.get("orientation") + new_sheet.sheet_view.orientation = orientation if orientation else "portrait" + self.current_worksheet = new_sheet + + # グループ定義を取得 + groups = section_config.get("groups") + if not groups: + return {"status": False, "message": f"Error no group definitions found: {section}"} + + # グループをリストに変換 + group_list = [g.strip() for g in groups.split(",")] + if not group_list: + return {"status": False, "message": f"Error invalid group definitions found: {section}"} + + # 各グループの設定を取得 + for group in group_list: + ret = self.proceed_group(group,variables) + if ret["status"] == False: + return ret + + return {"status": True, "message": f"Success generating section: {section}"} + + except Exception as e: + return {"status": False, "message": f"Exception in proceed_section : Error generating report: {str(e)}"} + + def proceed_group(self,group:str,variables: Dict[str, Any]): + logging.info(f"make_report.proceed_group step-1:section={group}") + + try: + group_config = self.conf.get_section(group) + if not group_config: + return {"status": False, "message": f"Error no group section found: {group}"} + + # グループの処理パラメータを取得 + group_range = group_config.get("group_range") + table = group_config.get("table") + where = group_config.get("where") + if not where or not table or not group_range: + return {"status": False, "message": f"Error invalid group parameters: {group_config}"} + + sort = group_config.get("sort") + if not sort: + ret = self.proceed_one_record(table,where,group_range,variables) + if ret.status == True: + return {"status": True, "message": f"Success generating group: {group}"} + else: + ret = self.proceed_all_records(table,where,sort,group_range,variables) + if ret.status == True: + return {"status": True, "message": f"Success generating group: {group}"} + + except Exception as e: + logging.error(f"Error in proceed_group: {str(e)}") + return {"status": False, "message": f"Exception in proceed_group : Error generating report: {str(e)}"} + + def proceed_one_record(self,table:str,where:str,group_range:str,variables: Dict[str, Any]): + """1レコードのデータを取得してシートの値を置き換える + Args: - db: Database connection - data_rec: Data records to populate report - out_filename: Optional output filename - screen_index: Screen index for multi-screen reports - """ - # Get output filename - if out_filename: - outfile = f"{out_filename}_{self._get_ini_param('basic', 'doc_file')}" - else: - outfile = self._get_ini_param('basic', 'doc_file') + table: テーブル名 + where: WHERE句 + group_range: 処理対象範囲 + variables: DB接続情報を含む変数辞書 + """ + logging.info(f"make_report.proceed_one_record step-1:table={table},where={where},group_range={group_range}") - self.output_filepath = f"{self.docpath2}/{outfile}" + try: + # まずself.template_sheetの指定範囲のセルをself.current_sheetにコピーする。 + self.copy_template_to_current(self.range,self.range) - # Process sections - sections = self._get_ini_param('basic', 'sections') - if not sections: - return - - for section in sections.split(','): - self._process_section(section, db, data_rec, screen_index) - - # Save workbook - self.workbook.save(self.output_filepath) - - def _process_section(self, section: str, db, data_rec: Dict[str, Any], - screen_index: int) -> None: - """Process individual section of report + cursor = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor) + + # SQLクエリを実行 + query = f"SELECT * FROM {table} WHERE {where} LIMIT 1" + cursor.execute(query) + record = cursor.fetchone() + + if record: + # group_rangeの範囲内のセルを走査 + for row in self.current_worksheet: + for cell in row: + if cell.value and isinstance(cell.value, str): + # [field_name]形式の文字列を検索 + import re + matches = re.findall(r'\[(.*?)\]', cell.value) + + # マッチした場合、フィールド値で置換 + if matches: + new_value = cell.value + for field_name in matches: + if field_name in record: + new_value = new_value.replace( + f'[{field_name}]', + str(record[field_name]) + ) + cell.value = new_value + + cursor.close() + self.conn.close() + return {"status": True, "message": f"Success generating group: "} + + except Exception as e: + logging.error(f"Error in proceed_one_record: {str(e)}") + return {"status": False, "message": f"Exception in proceed_one_record:Error generating report: {str(e)}"} + def proceed_all_record(self, table: str, where: str, sort: str, group_range: str, variables: Dict[str, Any]): + """複数レコードを取得してシートの値を置き換える + Args: - section: Section name - db: Database connection - data_rec: Data records - screen_index: Screen index - """ - # Get template sheet - sheet_orig = self._get_ini_param(section, 'sheet') - sheet_name = self._get_ini_param(section, f"sheetname_{self.language}") + table: テーブル名 + where: WHERE句 + sort: ORDER BY句 + group_range: 処理対象範囲 + variables: DB接続情報を含む変数辞書 + """ + logging.info(f"make_report.proceed_all_record step-1:table={table},where={where},group_range={group_range}") - if not sheet_orig or not sheet_name: - return + try: + # グループ範囲の行数を取得 + start_row, end_row = map(int, group_range.split(':')[0].split(',')) + template_rows = end_row - start_row + 1 - # Copy template sheet - template_sheet = self.workbook[sheet_orig] - new_sheet = self.workbook.copy_worksheet(template_sheet) - new_sheet.title = sheet_name + cursor = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor) + + # SQLクエリを実行 + query = f"SELECT * FROM {table} WHERE {where} ORDER BY {sort}" + cursor.execute(query) + records = cursor.fetchall() + + current_row = start_row + for record in records: + # テンプレート範囲をコピー + self.copy_template_to_current( + f"{start_row},{end_row}", + f"{current_row},{current_row + template_rows - 1}" + ) + + # コピーした範囲内のセルを走査して値を置換 + for row in range(current_row, current_row + template_rows): + for cell in self.current_worksheet[row]: + if cell.value and isinstance(cell.value, str): + # [field_name]形式の文字列を検索 + import re + matches = re.findall(r'\[(.*?)\]', cell.value) + + # マッチした場合、フィールド値で置換 + if matches: + new_value = cell.value + for field_name in matches: + if field_name in record: + new_value = new_value.replace( + f'[{field_name}]', + str(record[field_name]) + ) + cell.value = new_value + + current_row += template_rows + + cursor.close() + self.conn.close() + return {"status": True, "message": "Success processing all records"} + + except Exception as e: + logging.error(f"Error in proceed_all_record: {str(e)}") + return {"status": False, "message": f"Exception in proceed_all_record:Error processing records: {str(e)}"} - # Process groups - groups = self._get_ini_param(section, 'groups') - if groups: - for group in groups.split(','): - self._process_group(new_sheet, section, group, db, data_rec, screen_index) - - def _process_group(self, sheet, section: str, group: str, - db, data_rec: Dict[str, Any], screen_index: int) -> None: - """Process group within section - - Args: - sheet: Worksheet to process - section: Section name - group: Group name - db: Database connection - data_rec: Data records - screen_index: Screen index - """ - pass # Implementation details will follow + def copy_template_to_current(self,range,target_range): + # テンプレートシートから現在のシートにデータをコピー + source_range = self.template_sheet.range(range) # コピーする範囲を指定 + target_range = self.current_sheet.range(target_range) # 貼り付け先の範囲を指定 + + # 値、数式、フォーマットをコピー + source_range.copy(target_range) - - def init_sheet(self, sheet_name: str) -> None: - """Initialize worksheet and managers""" - self.current_sheet = self.workbook[sheet_name] - self._style_manager = StyleManager() - self._merge_manager = MergeManager(self.current_sheet) - self._image_manager = ImageManager(self.current_sheet) - self._conditional_manager = ConditionalFormatManager(self.current_sheet) - self._page_manager = PageManager(self.current_sheet) - # Style operations def apply_style( self, diff --git a/SumasenLibs/excel_lib/testdata/certificate_template.xlsx b/SumasenLibs/excel_lib/testdata/certificate_template.xlsx new file mode 100644 index 0000000..19b4c60 Binary files /dev/null and b/SumasenLibs/excel_lib/testdata/certificate_template.xlsx differ diff --git a/SumasenLibs/excel_lib/testdata/sample.py b/SumasenLibs/excel_lib/testdata/sample.py index 3e8c542..2ccbdad 100644 --- a/SumasenLibs/excel_lib/testdata/sample.py +++ b/SumasenLibs/excel_lib/testdata/sample.py @@ -1,55 +1,19 @@ from sumaexcel import SumasenExcel +import logging # 初期化 -excel = SumasenExcel() -excel.init("username", "project_id", "document") +variables = {"zekken_number":"5033","event_code":"FC岐阜"} +excel = SumasenExcel(document="test", variables=variables, docbase="./testdata") + +logging.info("Excelファイル作成 step-1") # シート初期化 -excel.init_sheet("Sheet1") - -# スタイル適用 -excel.apply_style( - "A1:D10", - font={"name": "Arial", "size": 12, "bold": True}, - fill={"start_color": "FFFF00"}, - alignment={"horizontal": "center"} -) - -# セルのマージ -excel.merge_range(1, 1, 1, 4) - -# 画像追加 -excel.add_image( - "logo.png", - position=(1, 1), - size=(100, 100) -) - -# 条件付き書式 -excel.add_conditional_format( - "B2:B10", - format_type="color_scale", - min_color="00FF0000", - max_color="0000FF00" -) - -# ページ設定 -excel.setup_page( - orientation="landscape", - paper_size=PaperSizes.A4, - margins={ - "left": 1.0, - "right": 1.0, - "top": 1.0, - "bottom": 1.0 - }, - header_footer={ - "odd_header": "&L&BPage &P of &N&C&BConfidential", - "odd_footer": "&RDraft" - } -) - -# レポート生成 -excel.make_report(db, data_rec) - +ret = excel.make_report(variables=variables) +logging.info(f"Excelファイル作成 step-2 : ret={ret}") +if ret["status"]==True: + filepath=ret["filepath"] + logging.info(f"Excelファイル作成 : ret.filepath={filepath}") +else: + message = ret.get("message", "No message provided") + logging.error(f"Excelファイル作成失敗 : ret.message={message}") diff --git a/SumasenLibs/excel_lib/testdata/test.ini b/SumasenLibs/excel_lib/testdata/test.ini index a75b2f2..69ced26 100644 --- a/SumasenLibs/excel_lib/testdata/test.ini +++ b/SumasenLibs/excel_lib/testdata/test.ini @@ -1,25 +1,24 @@ [basic] -templatefile_jp="certificate_template.xlsx" -doc_file="certificate_[zekken_number].xlsx" +template_file=certificate_template.xlsx +doc_file=certificate_[zekken_number].xlsx sections=section1,section2 -developer=Sumasen maxcol=8 [section1] -sheet="certificate" -sheetname_jp="岐阜ロゲ通過証明書" -groups="group1,group2" +template_sheet=certificate +groups=group1,group2 fit_to_width=1 orientation=portrait [section1.group1] table_name=rog_entry -where="zekken_number='[zekken_number]' and event_code='[event_code]'" -group_range="0,0,8,11" +where=zekken_number='[zekken_number]' and event_code='[event_code]' +group_range=0,0,8,11 + [section1.group2] table_name=gps_checkins -where=""zekken_number='[zekken_number]' and event_code='[event_code]' +where=zekken_number='[zekken_number]' and event_code='[event_code]' sort=order group_range=0,12,8,12