From 11b429adf05bc4793015619c2d1d6aabb14a3889 Mon Sep 17 00:00:00 2001 From: Sven Niclas Hebrok Date: Fri, 11 Oct 2024 20:01:45 +0200 Subject: [PATCH] Add ability to add custom code in documents --- .devcontainer/Dockerfile | 12 +++++++++ .devcontainer/devcontainer.json | 44 +++++++++++++++++++++++++++++++ app/client/ui/DocumentSettings.ts | 12 +++++++++ app/common/DocumentSettings.ts | 1 + sandbox/grist/docactions.py | 7 +++++ sandbox/grist/engine.py | 15 ++++++++++- sandbox/grist/gencode.py | 9 ++++++- 7 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000..a319d61fa7 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,12 @@ + +FROM node:22-bookworm +RUN apt-get update && apt-get install -y python3.11 python3-pip pipx gdb +COPY sandbox/requirements3.txt package.json yarn.lock /grist/ +RUN pip3 install --break-system-packages -r /grist/requirements3.txt +RUN yarn install --frozen-lockfile --verbose --network-timeout 600000 + +# absolutely bad idea normally, but I could not get python to attach to a running process otherwise +# it always failed with "ptrace: Operation not permitted." +RUN chmod u+s /usr/bin/gdb + +ENV GRIST_HOST="0.0.0.0" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..242ee03641 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,44 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile +{ + "name": "Existing Dockerfile", + "build": { + // Sets the run context to one level up instead of the .devcontainer folder. + "context": "..", + // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename. + "dockerfile": "./Dockerfile" + }, + "features": { + "ghcr.io/devcontainers/features/node:1": { + "nodeGypDependencies": true, + "installYarnUsingApt": true, + "version": "lts", + "pnpmVersion": "latest", + "nvmVersion": "latest" + }, + "ghcr.io/devcontainers/features/python:1": { + "installTools": true, + "version": "os-provided" + } + }, + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [ + 8484 + ], + // Uncomment the next line to run commands after the container is created. + // "postCreateCommand": "cat /etc/os-release", + "postCreateCommand": "yarn install && yarn install:python", + // Configure tool-specific properties. + // "customizations": {}, + // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. + "remoteUser": "node", + "capAdd": [ + "SYS_PTRACE" + ], + "securityOpt": [ + "seccomp=unconfined", + "apparmor=unconfined", + ], +} \ No newline at end of file diff --git a/app/client/ui/DocumentSettings.ts b/app/client/ui/DocumentSettings.ts index b2c9ba287b..fc4dba52f6 100644 --- a/app/client/ui/DocumentSettings.ts +++ b/app/client/ui/DocumentSettings.ts @@ -2,6 +2,7 @@ * This module export a component for editing some document settings consisting of the timezone, * (new settings to be added here ...). */ +import * as AceEditor from 'app/client/components/AceEditor'; import {cssPrimarySmallLink, cssSmallButton, cssSmallLinkButton} from 'app/client/components/Forms/styles'; import {GristDoc} from 'app/client/components/GristDoc'; import {ACIndexImpl} from 'app/client/lib/ACIndex'; @@ -41,6 +42,7 @@ export class DocSettingsPage extends Disposable { private _timezone = this._docInfo.timezone; private _locale: KoSaveableObservable = this._docInfo.documentSettingsJson.prop('locale'); private _currency: KoSaveableObservable = this._docInfo.documentSettingsJson.prop('currency'); + private _customCode: KoSaveableObservable = this._docInfo.documentSettingsJson.prop('customCode'); private _engine: Computed = Computed.create(this, ( use => use(this._docInfo.documentSettingsJson.prop('engine')) )) @@ -85,6 +87,16 @@ export class DocSettingsPage extends Disposable { {defaultCurrencyLabel: t("Local currency ({{currency}})", {currency: getCurrency(l)})}) ) }), + dom.create(AdminSectionItem, { + id: 'custom_code', + name: t('CustomCode'), + expandedContent: dom('div', + t('Custom python code to include when generating the model. Useful for defining custom functions.'), + AceEditor.create({ observable: this._customCode }).buildDom((aceObj: any) => { + aceObj.renderer.setShowGutter(true); + }), + ), + }), ]), dom.create(AdminSection, t('Data Engine'), [ diff --git a/app/common/DocumentSettings.ts b/app/common/DocumentSettings.ts index 54239cd678..d2d1ab27e2 100644 --- a/app/common/DocumentSettings.ts +++ b/app/common/DocumentSettings.ts @@ -2,6 +2,7 @@ export interface DocumentSettings { locale: string; currency?: string; engine?: EngineCode; + customCode?: string; } /** diff --git a/sandbox/grist/docactions.py b/sandbox/grist/docactions.py index 0ec1e4887b..114f90d9da 100644 --- a/sandbox/grist/docactions.py +++ b/sandbox/grist/docactions.py @@ -99,6 +99,13 @@ def BulkUpdateRecord(self, table_id, row_ids, columns): ("recalcWhen" in columns or "recalcDeps" in columns)): self._engine.trigger_columns_changed() + # If we're updating documentSettings, rebuild usercode as the code might have changed. + if table_id == "_grist_DocInfo" and "documentSettings" in columns: + self._engine.rebuild_usercode() + for table in self._engine.tables.keys(): + # I guess we invalidate everything, as we do not analyze in which regards the code may have changed :S + self._engine.invalidate_records(table) + def ReplaceTableData(self, table_id, row_ids, column_values): old_data = self._engine.fetch_table(table_id, formulas=False) self._engine.out_actions.undo.append(actions.ReplaceTableData(*old_data)) diff --git a/sandbox/grist/engine.py b/sandbox/grist/engine.py index 23a999f340..bcc815002d 100644 --- a/sandbox/grist/engine.py +++ b/sandbox/grist/engine.py @@ -4,6 +4,7 @@ dependency tracking. """ import itertools +import json import logging import re import rlcompleter @@ -346,6 +347,11 @@ def load_table(self, data): # Add the records. self.add_records(data.table_id, data.row_ids, columns) + if data.table_id == "_grist_DocInfo": + # when loading the DocInfo table, update the usercode module with the new doc settings. + # otherwise the custom code will not be loaded until the model is rebuilt due to other events. + self.rebuild_usercode() + def load_done(self): """ Finalizes the loading of data into this Engine. @@ -1137,7 +1143,14 @@ def rebuild_usercode(self): if not self._should_rebuild_usercode: return - self.gencode.make_module(self.schema) + doc_info = None + try: + doc_info = self.docmodel.doc_info.lookupOne() + doc_settings = json.loads(doc_info.documentSettings) + except (AttributeError, ValueError) as e: + doc_settings = {"customCode": "## ERROR: " + + repr(e).replace("\n", "\n# ")} + self.gencode.make_module(self.schema, doc_settings) # Re-populate self.tables, reusing existing tables whenever possible. old_tables = self.tables diff --git a/sandbox/grist/gencode.py b/sandbox/grist/gencode.py index 5c26490c7c..dad19d426d 100644 --- a/sandbox/grist/gencode.py +++ b/sandbox/grist/gencode.py @@ -164,7 +164,7 @@ def _make_table_model(self, table_info, summary_tables, filter_for_user=False): return textbuilder.Combiner(parts) - def make_module(self, schema): + def make_module(self, schema, doc_settings): """Regenerates the code text and usercode module from updated document schema.""" # Collect summary tables to group them by source table. summary_tables = {} @@ -176,6 +176,13 @@ def make_module(self, schema): fullparts = ["import grist\n" + "from functions import * # global uppercase functions\n" + "import datetime, math, re # modules commonly needed in formulas\n"] + + user_code = doc_settings.get('customCode', '') + if user_code: + fullparts.append("\n### BEGIN CUSTOM USER CODE ###\n") + fullparts.append(user_code) + fullparts.append("\n### END CUSTOM USER CODE ###\n") + userparts = fullparts[:] for table_info in six.itervalues(schema): fullparts.append("\n\n")