mirror of
https://github.com/dwinkler1/np.git
synced 2026-02-19 22:40:57 -05:00
Working template
This commit is contained in:
parent
d9ad604fe6
commit
144eebbcfa
2 changed files with 277 additions and 132 deletions
29
templates/n/flake.lock
generated
29
templates/n/flake.lock
generated
|
|
@ -6,7 +6,6 @@
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
],
|
],
|
||||||
"nixpkgs-unstable": "nixpkgs-unstable",
|
|
||||||
"plugins-cmp-pandoc-references": "plugins-cmp-pandoc-references",
|
"plugins-cmp-pandoc-references": "plugins-cmp-pandoc-references",
|
||||||
"plugins-cmp-r": "plugins-cmp-r",
|
"plugins-cmp-r": "plugins-cmp-r",
|
||||||
"plugins-r": "plugins-r",
|
"plugins-r": "plugins-r",
|
||||||
|
|
@ -15,11 +14,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1756305715,
|
"lastModified": 1756379633,
|
||||||
"narHash": "sha256-GuNro+bHHMde1X2uoaDS0UwJa1aaVTDvG4KmQOmCAWE=",
|
"narHash": "sha256-REv+GIfWkyCIHfcPzotqpaSHha0LPZ300KsJL+9kP40=",
|
||||||
"owner": "dwinkler1",
|
"owner": "dwinkler1",
|
||||||
"repo": "nixCatsConfig",
|
"repo": "nixCatsConfig",
|
||||||
"rev": "e0f5193d7299c36724d17728511260e0d453f0dc",
|
"rev": "e39a1272ef82cd467fb0c29d0b8a0ccdca672f67",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -45,27 +44,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1756159630,
|
"lastModified": 1756288264,
|
||||||
"narHash": "sha256-ohMvsjtSVdT/bruXf5ClBh8ZYXRmD4krmjKrXhEvwMg=",
|
"narHash": "sha256-Om8adB1lfkU7D33VpR+/haZ2gI5r3Q+ZbIPzE5sYnwE=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "84c256e42600cb0fdf25763b48d28df2f25a0c8b",
|
"rev": "ddd1826f294a0ee5fdc198ab72c8306a0ea73aa9",
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nixos",
|
|
||||||
"ref": "nixpkgs-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs-unstable": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1756159630,
|
|
||||||
"narHash": "sha256-ohMvsjtSVdT/bruXf5ClBh8ZYXRmD4krmjKrXhEvwMg=",
|
|
||||||
"owner": "nixos",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "84c256e42600cb0fdf25763b48d28df2f25a0c8b",
|
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@
|
||||||
inputs = {
|
inputs = {
|
||||||
rixpkgs.url = "https://github.com/rstats-on-nix/nixpkgs/archive/2025-08-11.tar.gz";
|
rixpkgs.url = "https://github.com/rstats-on-nix/nixpkgs/archive/2025-08-11.tar.gz";
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
||||||
nixCats.url = "github:dwinkler1/nixCatsConfig";
|
nixCats = {
|
||||||
nixCats.inputs.nixpkgs.follows = "nixpkgs";
|
url = "github:dwinkler1/nixCatsConfig";
|
||||||
nixCats.inputs.rixpkgs.follows = "rixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
## All git packages managed per project
|
inputs.rixpkgs.follows = "rixpkgs";
|
||||||
|
};
|
||||||
|
## Git Plugins
|
||||||
"plugins-r" = {
|
"plugins-r" = {
|
||||||
url = "github:R-nvim/R.nvim";
|
url = "github:R-nvim/R.nvim";
|
||||||
flake = false;
|
flake = false;
|
||||||
|
|
@ -26,73 +28,221 @@
|
||||||
nixCats,
|
nixCats,
|
||||||
...
|
...
|
||||||
} @ inputs: let
|
} @ inputs: let
|
||||||
|
#######################
|
||||||
|
### PROJECT CONFIG ####
|
||||||
|
#######################
|
||||||
|
## Set options below:
|
||||||
|
config = rec {
|
||||||
|
## Set project name
|
||||||
|
defaultPackageName = "p";
|
||||||
|
## Enable languages
|
||||||
|
enabledLanguages = {
|
||||||
|
julia = false;
|
||||||
|
python = false;
|
||||||
|
r = false;
|
||||||
|
};
|
||||||
|
## Enable packages
|
||||||
|
enabledPackages = {
|
||||||
|
## Plugins loaded via flake input
|
||||||
|
### Always enable when R is enabled
|
||||||
|
### You can use your own R installation and just enable the plugin
|
||||||
|
gitPlugins = enabledLanguages.r;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
# R packages
|
||||||
|
rixOverlay = final: prev: {rpkgs = inputs.rixpkgs.legacyPackages.${prev.system};};
|
||||||
|
rOverlay = final: prev: let
|
||||||
|
reqPkgs = with final.rpkgs.rPackages; [
|
||||||
|
broom
|
||||||
|
data_table
|
||||||
|
janitor
|
||||||
|
languageserver
|
||||||
|
reprex
|
||||||
|
styler
|
||||||
|
tidyverse
|
||||||
|
(buildRPackage {
|
||||||
|
name = "nvimcom";
|
||||||
|
src = inputs.plugins-r;
|
||||||
|
sourceRoot = "source/nvimcom";
|
||||||
|
buildInputs = with prev.rpkgs; [
|
||||||
|
R
|
||||||
|
stdenv.cc.cc
|
||||||
|
gnumake
|
||||||
|
];
|
||||||
|
propagatedBuildInputs = [];
|
||||||
|
})
|
||||||
|
];
|
||||||
|
in {
|
||||||
|
quarto = final.rpkgs.quarto.override {extraRPackages = reqPkgs;};
|
||||||
|
rWrapper = final.rpkgs.rWrapper.override {packages = reqPkgs;};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Python packages
|
||||||
|
pythonOverlay = final: prev: {
|
||||||
|
python = prev.python3.withPackages (pyPackages:
|
||||||
|
with pyPackages; [
|
||||||
|
requests
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
###################################
|
||||||
|
## ⬆️ BASIC CONFIG ABOVE HERE ⬆️ ##
|
||||||
|
###################################
|
||||||
|
|
||||||
|
projectScriptsOverlay = final: prev: let
|
||||||
|
initPython = ''
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ ! -f "pyproject.toml" ]]; then
|
||||||
|
echo "🐍 Initializing UV project..."
|
||||||
|
uv init
|
||||||
|
echo "📦 Adding ipython and marimo..."
|
||||||
|
uv add ipython
|
||||||
|
uv add marimo
|
||||||
|
echo "--------------------------------------------------------------------------"
|
||||||
|
echo "✅ Python project initialized!"
|
||||||
|
echo "--------------------------------------------------------------------------"
|
||||||
|
else
|
||||||
|
echo "--------------------------------------------------------------------------"
|
||||||
|
echo "🔄 Existing Python project detected."
|
||||||
|
echo "Run '${config.defaultPackageName}-updateDeps' to update dependencies."
|
||||||
|
echo "--------------------------------------------------------------------------"
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
mkDirsScript = ''
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROJECT_NAME="''${1:-${config.defaultPackageName}}"
|
||||||
|
|
||||||
|
echo "🚀 Setting up project: $PROJECT_NAME"
|
||||||
|
|
||||||
|
# Create directory structure
|
||||||
|
directories=(
|
||||||
|
"data/raw"
|
||||||
|
"data/processed"
|
||||||
|
"data/interim"
|
||||||
|
"docs"
|
||||||
|
"figures"
|
||||||
|
"tables"
|
||||||
|
"src/analysis"
|
||||||
|
"src/data_prep"
|
||||||
|
"src/explore"
|
||||||
|
"src/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
for dir in "''${directories[@]}"; do
|
||||||
|
mkdir -p "$dir"
|
||||||
|
echo "✓ Created $dir/"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Create essential files
|
||||||
|
if [[ ! -f "README.md" ]]; then
|
||||||
|
cat > README.md << 'EOF'
|
||||||
|
# $PROJECT_NAME
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
- `data/`: Data files (gitignored)
|
||||||
|
- `docs/`: Documentation
|
||||||
|
- `figures/`: Output figures
|
||||||
|
- `tables/`: Output tables
|
||||||
|
- `src/`: Source code
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
- Julia environment: `$PROJECT_NAME-jl`
|
||||||
|
- Python environment: `$PROJECT_NAME-m` (Marimo)
|
||||||
|
- R environment: `$PROJECT_NAME-r`
|
||||||
|
- Neovide: `$PROJECT_NAME-g`
|
||||||
|
- Neovim: `$PROJECT_NAME`
|
||||||
|
- Update: `$PROJECT_NAME-updateDeps`
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create .gitignore
|
||||||
|
if [[ ! -f ".gitignore" ]]; then
|
||||||
|
cat > .gitignore << 'EOF'
|
||||||
|
# Data files
|
||||||
|
data/
|
||||||
|
*.csv
|
||||||
|
*.docx
|
||||||
|
*.xlsx
|
||||||
|
*.parquet
|
||||||
|
|
||||||
|
# R specific
|
||||||
|
.Rproj.user/
|
||||||
|
.Rhistory
|
||||||
|
.RData
|
||||||
|
.Ruserdata
|
||||||
|
*.Rproj
|
||||||
|
.Rlibs/
|
||||||
|
|
||||||
|
# Python specific
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.pytest_cache/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# Jupyter
|
||||||
|
.ipynb_checkpoints/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Project setup completed successfully!"
|
||||||
|
'';
|
||||||
|
|
||||||
|
updateDepsScript = ''
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "🔄 Updating project dependencies..."
|
||||||
|
|
||||||
|
if [[ -f "flake.lock" ]]; then
|
||||||
|
nix flake update
|
||||||
|
echo "✅ Flake inputs updated"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "pyproject.toml" ]]; then
|
||||||
|
uv sync --upgrade
|
||||||
|
echo "✅ Python dependencies updated"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "Project.toml" ]]; then
|
||||||
|
${config.defaultPackageName}-jl -e "using Pkg; Pkg.update()"
|
||||||
|
echo "✅ Julia dependencies updated"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🎉 All dependencies updated!"
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
initPython = prev.writeShellScriptBin "initPython" initPython;
|
||||||
|
mkDirs = prev.writeShellScriptBin "mkDirs" mkDirsScript;
|
||||||
|
updateDeps = prev.writeShellScriptBin "updateDeps" updateDepsScript;
|
||||||
|
};
|
||||||
forSystems = nixpkgs.lib.genAttrs nixpkgs.lib.platforms.all;
|
forSystems = nixpkgs.lib.genAttrs nixpkgs.lib.platforms.all;
|
||||||
defaultPackageName = "p";
|
|
||||||
projectConfig = forSystems (
|
projectConfig = forSystems (
|
||||||
system: let
|
system: let
|
||||||
inherit (nixCats) utils;
|
inherit (nixCats) utils;
|
||||||
inherit defaultPackageName;
|
inherit (config) defaultPackageName;
|
||||||
prevPackage = nixCats.packages.${system}.default;
|
prevPackage = nixCats.packages.${system}.default;
|
||||||
finalPackage = prevPackage.override (prev: {
|
finalPackage = prevPackage.override (prev: {
|
||||||
name = defaultPackageName;
|
name = config.defaultPackageName;
|
||||||
dependencyOverlays =
|
dependencyOverlays =
|
||||||
prev.dependencyOverlays
|
prev.dependencyOverlays
|
||||||
++ [
|
++ [
|
||||||
(utils.standardPluginOverlay inputs)
|
(utils.standardPluginOverlay inputs)
|
||||||
## Pull in local rix copy
|
rixOverlay
|
||||||
(final: prev: {
|
rOverlay
|
||||||
rpkgs = inputs.rixpkgs.legacyPackages.${prev.system};
|
pythonOverlay
|
||||||
})
|
projectScriptsOverlay
|
||||||
## Define project level R packages
|
|
||||||
(
|
|
||||||
final: prev: let
|
|
||||||
reqPkgs = with prev.rpkgs.rPackages; [
|
|
||||||
Hmisc
|
|
||||||
Rcpp
|
|
||||||
arm
|
|
||||||
broom
|
|
||||||
car
|
|
||||||
data_table
|
|
||||||
devtools
|
|
||||||
janitor
|
|
||||||
konfound
|
|
||||||
languageserver
|
|
||||||
quarto
|
|
||||||
reprex
|
|
||||||
styler
|
|
||||||
tidyverse
|
|
||||||
(buildRPackage {
|
|
||||||
name = "nvimcom";
|
|
||||||
src = inputs.plugins-r;
|
|
||||||
sourceRoot = "source/nvimcom";
|
|
||||||
buildInputs = with prev.rpkgs; [
|
|
||||||
R
|
|
||||||
stdenv.cc.cc
|
|
||||||
gnumake
|
|
||||||
];
|
|
||||||
propagatedBuildInputs = [];
|
|
||||||
})
|
|
||||||
];
|
|
||||||
in {
|
|
||||||
quarto = prev.rpkgs.quarto.override {extraRPackages = reqPkgs;};
|
|
||||||
rWrapper = prev.rpkgs.rWrapper.override {packages = reqPkgs;};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
## Define project level Python Packages
|
|
||||||
## Only use if uv should not be used
|
|
||||||
(
|
|
||||||
final: prev: let
|
|
||||||
reqPkgs = pyPackages:
|
|
||||||
with pyPackages; [
|
|
||||||
numpy
|
|
||||||
polars
|
|
||||||
requests
|
|
||||||
];
|
|
||||||
in {
|
|
||||||
python = prev.python3.withPackages reqPkgs;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
];
|
];
|
||||||
categoryDefinitions = utils.mergeCatDefs prev.categoryDefinitions (
|
categoryDefinitions = utils.mergeCatDefs prev.categoryDefinitions (
|
||||||
{
|
{
|
||||||
|
|
@ -198,7 +348,7 @@
|
||||||
packageDefinitions =
|
packageDefinitions =
|
||||||
prev.packageDefinitions
|
prev.packageDefinitions
|
||||||
// {
|
// {
|
||||||
"${defaultPackageName}" = utils.mergeCatDefs prev.packageDefinitions.n (
|
p = utils.mergeCatDefs prev.packageDefinitions.n (
|
||||||
{
|
{
|
||||||
pkgs,
|
pkgs,
|
||||||
name,
|
name,
|
||||||
|
|
@ -221,74 +371,64 @@
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
m = let
|
m = let
|
||||||
preHookInit = ''
|
marimoInit = ''
|
||||||
# Check if pyproject.toml exists
|
set -euo pipefail
|
||||||
if [ ! -f "pyproject.toml" ]; then
|
echo "🔄 Syncing existing project..."
|
||||||
echo "pyproject.toml not found. Initializing new UV project..."
|
uv sync
|
||||||
|
|
||||||
# Initialize UV project
|
|
||||||
uv init
|
|
||||||
|
|
||||||
# Check if uv init was successful
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo "UV project initialized successfully."
|
|
||||||
|
|
||||||
# Add marimo dependency
|
|
||||||
echo "Adding marimo dependency..."
|
|
||||||
uv add marimo
|
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo "Marimo added successfully!"
|
|
||||||
else
|
|
||||||
echo "Error: Failed to add marimo dependency."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Error: Failed to initialize UV project."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "pyproject.toml already exists. Syncing...."
|
|
||||||
uv sync
|
|
||||||
fi
|
|
||||||
'';
|
'';
|
||||||
in {
|
in {
|
||||||
enable = true;
|
enable = config.enabledLanguages.python;
|
||||||
path = {
|
path = {
|
||||||
value = "${pkgs.uv}/bin/uv";
|
value = "${pkgs.uv}/bin/uv";
|
||||||
args = [
|
args = [
|
||||||
"--run"
|
"--run"
|
||||||
"${preHookInit}"
|
"${marimoInit}"
|
||||||
"--add-flags"
|
"--add-flags"
|
||||||
"run marimo edit"
|
"run marimo edit \"$@\""
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
jl = {
|
jl = {
|
||||||
enable = false;
|
enable = config.enabledLanguages.julia;
|
||||||
path = {
|
path = {
|
||||||
value = "${pkgs.julia-bin}/bin/julia";
|
value = "${pkgs.julia-bin}/bin/julia";
|
||||||
args = ["--add-flags" "--project=@."];
|
args = ["--add-flags" "--project=."];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
r = {
|
r = {
|
||||||
enable = true;
|
enable = config.enabledLanguages.r;
|
||||||
path = {
|
path = {
|
||||||
value = "${pkgs.rWrapper}/bin/R";
|
value = "${pkgs.rWrapper}/bin/R";
|
||||||
args = ["--add-flags" "--no-save --no-restore"];
|
args = ["--add-flags" "--no-save --no-restore"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
initPython = {
|
||||||
|
enable = config.enabledLanguages.python;
|
||||||
|
path.value = "${pkgs.initPython}/bin/initPython";
|
||||||
|
};
|
||||||
|
mkDirs = {
|
||||||
|
enable = true;
|
||||||
|
path = {
|
||||||
|
value = "${pkgs.mkDirs}/bin/mkDirs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
updateDeps = {
|
||||||
|
enable = true;
|
||||||
|
path = {
|
||||||
|
value = "${pkgs.updateDeps}/bin/updateDeps";
|
||||||
|
};
|
||||||
|
};
|
||||||
node.enable = true;
|
node.enable = true;
|
||||||
perl.enable = true;
|
perl.enable = true;
|
||||||
ruby.enable = true;
|
ruby.enable = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
categories = {
|
categories = {
|
||||||
julia = false;
|
julia = config.enabledLanguages.julia;
|
||||||
python = false;
|
python = config.enabledLanguages.python;
|
||||||
r = true;
|
r = config.enabledLanguages.r;
|
||||||
project = true;
|
project = true;
|
||||||
gitPlugins = true;
|
gitPlugins = config.enabledPackages.gitPlugins;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -302,13 +442,35 @@
|
||||||
devShells = forSystems (system: let
|
devShells = forSystems (system: let
|
||||||
pkgs = import nixpkgs {inherit system;};
|
pkgs = import nixpkgs {inherit system;};
|
||||||
in {
|
in {
|
||||||
default = pkgs.mkShell {
|
default = let
|
||||||
name = defaultPackageName;
|
shellCmds = pkgs.lib.concatLines (pkgs.lib.filter (cmd: cmd != "") [
|
||||||
packages = [projectConfig.${system}.default];
|
(pkgs.lib.optionalString config.enabledLanguages.r " - ${config.defaultPackageName}-r: Launch R console")
|
||||||
inputsFrom = [];
|
(pkgs.lib.optionalString config.enabledLanguages.julia " - ${config.defaultPackageName}-jl: Launch Julia REPL")
|
||||||
shellHook = ''
|
(pkgs.lib.optionalString config.enabledLanguages.python " - ${config.defaultPackageName}-m: Launch Marimo notebook")
|
||||||
'';
|
"See options in flake.nix"
|
||||||
};
|
]);
|
||||||
|
in
|
||||||
|
pkgs.mkShell {
|
||||||
|
name = config.defaultPackageName;
|
||||||
|
packages = [projectConfig.${system}.default];
|
||||||
|
inputsFrom = [];
|
||||||
|
shellHook = ''
|
||||||
|
echo ""
|
||||||
|
${pkgs.lib.optionalString config.enabledLanguages.python "${config.defaultPackageName}-initPython"}
|
||||||
|
echo "=========================================================================="
|
||||||
|
echo "🎯 ${config.defaultPackageName} Development Environment"
|
||||||
|
echo "---"
|
||||||
|
echo "📝 Run '${config.defaultPackageName}-mkDirs' to set up project structure"
|
||||||
|
echo "🔄 Run '${config.defaultPackageName}-updateDeps' to update all dependencies"
|
||||||
|
echo "---"
|
||||||
|
echo "🚀 Available commands:"
|
||||||
|
echo " - ${config.defaultPackageName}: Launch Neovim"
|
||||||
|
echo " - ${config.defaultPackageName}-g: Launch Neovide"
|
||||||
|
echo "${shellCmds}"
|
||||||
|
echo "=========================================================================="
|
||||||
|
echo ""
|
||||||
|
'';
|
||||||
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue