Compare commits
462 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
762b83934a | ||
|
|
ae820877d5 | ||
|
|
0d1b37997a | ||
|
|
8990f4bbcb | ||
|
|
c265b9a729 | ||
|
|
6625943701 | ||
|
|
788c2088f8 | ||
|
|
629e9dc881 | ||
|
|
ec2ee2bfad | ||
|
|
b24f1da8b5 | ||
|
|
88a1099f06 | ||
|
|
bf574c16f4 | ||
|
|
6cac9a71ed | ||
|
|
6858a60fb1 | ||
|
|
01f1c8c5c8 | ||
|
|
e45d25beb4 | ||
|
|
bec1580991 | ||
|
|
f453995aa3 | ||
|
|
cfe163d5a6 | ||
|
|
51c3ce4c56 | ||
|
|
77fd354a22 | ||
|
|
c2a42c572c | ||
|
|
0bd53daed7 | ||
|
|
6358ac8b18 | ||
|
|
02eca7a50c | ||
|
|
f45e847ad2 | ||
|
|
5555c48bcb | ||
|
|
295e0a12e7 | ||
|
|
b2402acccf | ||
|
|
167cf1b693 | ||
|
|
606ba1fa45 | ||
|
|
c4ec9c9b0f | ||
|
|
c2ec3b94a9 | ||
|
|
163deaff28 | ||
|
|
cf14bf2f68 | ||
|
|
12facb53e4 | ||
|
|
89b8598cd7 | ||
|
|
33db1e49a1 | ||
|
|
96c89f7def | ||
|
|
a536f58daa | ||
|
|
cb0372343f | ||
|
|
748377b11c | ||
|
|
714d4f799b | ||
|
|
218d591f35 | ||
|
|
1bb92428a7 | ||
|
|
ccf29db1d3 | ||
|
|
de19818429 | ||
|
|
01e736bccc | ||
|
|
5f717d4e70 | ||
|
|
f79fb4dbc1 | ||
|
|
bd1926c57b | ||
|
|
6bebb04015 | ||
|
|
a72bc2ddd6 | ||
|
|
44951bf648 | ||
|
|
b1fc780f44 | ||
|
|
cfb20edb9f | ||
|
|
0753be9d12 | ||
|
|
db05479cbe | ||
|
|
c850737d24 | ||
|
|
333ba1b3f0 | ||
|
|
cf31d3aea4 | ||
|
|
27ad2b5a85 | ||
|
|
c76dfeefe4 | ||
|
|
9caf1d4029 | ||
|
|
e432f6a625 | ||
|
|
ade2c8c63a | ||
|
|
9c13a20cda | ||
|
|
f9f0aed2d8 | ||
|
|
1b4b2f401d | ||
|
|
a7bf8e452a | ||
|
|
c00fe4ac68 | ||
|
|
14c439b8e7 | ||
|
|
2c03451597 | ||
|
|
e85ebc0cab | ||
|
|
ea10333b23 | ||
|
|
e773cffa76 | ||
|
|
b795d72e39 | ||
|
|
be6c548ac1 | ||
|
|
23396821db | ||
|
|
78c04e4b2d | ||
|
|
61a1d72d7e | ||
|
|
ede6c52d63 | ||
|
|
372c904a96 | ||
|
|
b500c97440 | ||
|
|
daaf55c50f | ||
|
|
79358f0e58 | ||
|
|
e3fa6c656e | ||
|
|
1521810537 | ||
|
|
67e8a2a835 | ||
|
|
f770048c99 | ||
|
|
4d2db238cb | ||
|
|
4e21d46120 | ||
|
|
1c5c69ba5c | ||
|
|
ebc7e3c6ac | ||
|
|
f260f3ffcb | ||
|
|
ef12579ee8 | ||
|
|
82e70c39d3 | ||
|
|
7174a611ac | ||
|
|
75607c7835 | ||
|
|
dc58f00106 | ||
|
|
74774b917a | ||
|
|
e9a0b5216d | ||
|
|
79997c5dd0 | ||
|
|
d866e1e7e1 | ||
|
|
46fdee0acb | ||
|
|
4829b9fe62 | ||
|
|
7b437a0383 | ||
|
|
d7def5a37a | ||
|
|
13bb916c85 | ||
|
|
169522f401 | ||
|
|
8774e7c2f0 | ||
|
|
7440a433ba | ||
|
|
72b9728540 | ||
|
|
d6224ac2e4 | ||
|
|
f8ddf6af72 | ||
|
|
665e208932 | ||
|
|
48616ebbcf | ||
|
|
4b0734c988 | ||
|
|
28e7103dae | ||
|
|
990e86f717 | ||
|
|
85e93169b5 | ||
|
|
0e3a5611c6 | ||
|
|
31b9fe1605 | ||
|
|
7963342574 | ||
|
|
80264c2822 | ||
|
|
9be9c5874f | ||
|
|
8115ed383b | ||
|
|
81152f626e | ||
|
|
4f7c480a8a | ||
|
|
9d30664c14 | ||
|
|
cca86956cd | ||
|
|
bd0e06bdd6 | ||
|
|
e2581a3276 | ||
|
|
eef8d8d1be | ||
|
|
5fce383bb3 | ||
|
|
317c67d01c | ||
|
|
03e4904506 | ||
|
|
0461155b5d | ||
|
|
cd3d3d3389 | ||
|
|
fde96522e8 | ||
|
|
9ccfa6732a | ||
|
|
244f63c07c | ||
|
|
f3df30a727 | ||
|
|
de377e30b0 | ||
|
|
ded9fd4223 | ||
|
|
b8e77668aa | ||
|
|
1f0eea5789 | ||
|
|
49e50c2834 | ||
|
|
176172c262 | ||
|
|
ec1a32f126 | ||
|
|
8c5c19fa81 | ||
|
|
d6ccec99b0 | ||
|
|
102a0976e5 | ||
|
|
1082f65e75 | ||
|
|
a187331b6d | ||
|
|
9a5fb5cb80 | ||
|
|
42e4fe24b9 | ||
|
|
fa33819414 | ||
|
|
06351b0c16 | ||
|
|
5285e6424f | ||
|
|
3202a290dc | ||
|
|
b70617eada | ||
|
|
9ce924ce1f | ||
|
|
60c85832d6 | ||
|
|
0d96c6a380 | ||
|
|
7225cbdd61 | ||
|
|
098d29deeb | ||
|
|
4ce8e0c6a1 | ||
|
|
8d770217a2 | ||
|
|
462f2aa10a | ||
|
|
1d5af4e48a | ||
|
|
e62d392eee | ||
|
|
e0473b748d | ||
|
|
1da214a53f | ||
|
|
86f46297c6 | ||
|
|
136bd9d848 | ||
|
|
4b7ddc263c | ||
|
|
f1f4e5f1a6 | ||
|
|
2a8c57ba10 | ||
|
|
280f8776c5 | ||
|
|
82d3f45090 | ||
|
|
edeb9b062a | ||
|
|
3b717664b8 | ||
|
|
bf3d5a8835 | ||
|
|
5cf061af30 | ||
|
|
f3c72dc102 | ||
|
|
0b771f49fd | ||
|
|
b072aa4f9a | ||
|
|
3a782d8114 | ||
|
|
81eccea140 | ||
|
|
0f33ee5052 | ||
|
|
825efd4eb9 | ||
|
|
3d2758c6ad | ||
|
|
2f52413a7c | ||
|
|
b1d3159bd8 | ||
|
|
fcdf564d2b | ||
|
|
823efe5050 | ||
|
|
b88e012759 | ||
|
|
82a00bd388 | ||
|
|
a187fa2c8c | ||
|
|
d8b786a25a | ||
|
|
55485e105e | ||
|
|
293d8d6680 | ||
|
|
2eacacb021 | ||
|
|
459586ff77 | ||
|
|
3ed8d8ae98 | ||
|
|
d77d62e806 | ||
|
|
eab8db2734 | ||
|
|
623ce16801 | ||
|
|
dfac02cc53 | ||
|
|
b99e461e10 | ||
|
|
9f66ca34d9 | ||
|
|
53e075953d | ||
|
|
d060629477 | ||
|
|
b7b34c775c | ||
|
|
2f2b07cb4c | ||
|
|
900e6abc6d | ||
|
|
63458261a8 | ||
|
|
65776080a6 | ||
|
|
1016a9b9dc | ||
|
|
c518bcde8f | ||
|
|
17f49c3827 | ||
|
|
48e0cb6588 | ||
|
|
bb09928d73 | ||
|
|
2b8f10a2a7 | ||
|
|
db0ddf2a60 | ||
|
|
51849937bb | ||
|
|
e640ccb603 | ||
|
|
1e812a8ef5 | ||
|
|
2c186ab72e | ||
|
|
a6aa162527 | ||
|
|
fd2be921d0 | ||
|
|
5710dcc85f | ||
|
|
64bfd89225 | ||
|
|
41fd7334dc | ||
|
|
1afae2b77c | ||
|
|
9b1ff9fd6e | ||
|
|
64b4f8c1d1 | ||
|
|
d03e991cdc | ||
|
|
dfe73d2f95 | ||
|
|
dfb0aa9609 | ||
|
|
79805cea66 | ||
|
|
89130998a4 | ||
|
|
ec11a81d1e | ||
|
|
92d7993137 | ||
|
|
2ea8792568 | ||
|
|
4148fe6d28 | ||
|
|
7aea9e2927 | ||
|
|
37df1ddccd | ||
|
|
df64277c7f | ||
|
|
0505da4f83 | ||
|
|
b268ccedd8 | ||
|
|
80a60a5b13 | ||
|
|
46ace24f53 | ||
|
|
d0542d02da | ||
|
|
b050ca7714 | ||
|
|
b0684d679c | ||
|
|
76c642239f | ||
|
|
2542a0c6f8 | ||
|
|
dbacb315da | ||
|
|
66ec659354 | ||
|
|
25f9188fc1 | ||
|
|
aaf297e63f | ||
|
|
03a74ec5ec | ||
|
|
83f3c4f657 | ||
|
|
3725f4ea0e | ||
|
|
debc8aef69 | ||
|
|
b3e0235a9e | ||
|
|
c0972dd390 | ||
|
|
d90cadcb9c | ||
|
|
e40d61c7ef | ||
|
|
6aaac55017 | ||
|
|
d28a4ef3a0 | ||
|
|
09c8271289 | ||
|
|
57fea5698e | ||
|
|
30930734d8 | ||
|
|
50db2e31cf | ||
|
|
ec5b8e93a9 | ||
|
|
8f0ff70813 | ||
|
|
99c5b22745 | ||
|
|
6678b3ccfe | ||
|
|
41d95bc0f5 | ||
|
|
d58a2a2823 | ||
|
|
47569858fb | ||
|
|
33c161c836 | ||
|
|
8f568c0425 | ||
|
|
2e36378f2d | ||
|
|
143bfa97b6 | ||
|
|
70285d8b14 | ||
|
|
7fb6379d71 | ||
|
|
a4aff4551f | ||
|
|
000395fbd2 | ||
|
|
339032de14 | ||
|
|
30f5071e37 | ||
|
|
701a74b68c | ||
|
|
6a6ef9b13e | ||
|
|
8c333063be | ||
|
|
1ea59a76c2 | ||
|
|
dcfbb71d89 | ||
|
|
74a0650f11 | ||
|
|
a870cafd2c | ||
|
|
7b856e4556 | ||
|
|
d28918df94 | ||
|
|
5d3a93e4c4 | ||
|
|
9703d9db35 | ||
|
|
c6ff2e3c58 | ||
|
|
813aa85554 | ||
|
|
61b18c9442 | ||
|
|
d4d968f399 | ||
|
|
d22669272a | ||
|
|
871074ef45 | ||
|
|
2954334e1f | ||
|
|
1879cb20bc | ||
|
|
42b36e4bee | ||
|
|
4370232031 | ||
|
|
4bbf408a68 | ||
|
|
cb06de7f89 | ||
|
|
763f53f579 | ||
|
|
69fb501b37 | ||
|
|
ee027863ed | ||
|
|
7e9abcc3ee | ||
|
|
f5fe770991 | ||
|
|
91921d2179 | ||
|
|
cba0ad2862 | ||
|
|
d613e8c5d2 | ||
|
|
92b06c47d5 | ||
|
|
d547607feb | ||
|
|
400f025169 | ||
|
|
7372f7513c | ||
|
|
de509aca9f | ||
|
|
46abee647f | ||
|
|
378e503ea4 | ||
|
|
a7c54c4b96 | ||
|
|
71b64f61d0 | ||
|
|
6ae3245a79 | ||
|
|
e2c058db1c | ||
|
|
7aa476f822 | ||
|
|
77e60643f9 | ||
|
|
85e5613254 | ||
|
|
04f5fe02d5 | ||
|
|
b0b773fe44 | ||
|
|
75f884161e | ||
|
|
658f9e0c7b | ||
|
|
2e15a91c08 | ||
|
|
29b463fa4a | ||
|
|
eea53c71ab | ||
|
|
a6ca6075ad | ||
|
|
811c5e51ba | ||
|
|
e4e2da8790 | ||
|
|
a3c3740805 | ||
|
|
abf203db7e | ||
|
|
b824b1112c | ||
|
|
d75e568ad6 | ||
|
|
555e3aa095 | ||
|
|
cc2dc072a0 | ||
|
|
f47142acbe | ||
|
|
b3deccc8ea | ||
|
|
4609d25c32 | ||
|
|
90f018479a | ||
|
|
c5f78aebdc | ||
|
|
9572211cac | ||
|
|
09e82be391 | ||
|
|
8258c6ee53 | ||
|
|
9b7529956e | ||
|
|
876f477005 | ||
|
|
1f16cd9bf4 | ||
|
|
9c90bb67c2 | ||
|
|
e6ba81cb9c | ||
|
|
aa5d3e4a5c | ||
|
|
9f3c24cb6a | ||
|
|
a11c1b9ffe | ||
|
|
6d157e9129 | ||
|
|
245925f2e0 | ||
|
|
11067cb9ff | ||
|
|
ad38afe1f0 | ||
|
|
ef8d2cc743 | ||
|
|
4a398181ec | ||
|
|
809364090d | ||
|
|
f5531894e5 | ||
|
|
019e507f6e | ||
|
|
5cdb49b870 | ||
|
|
61e3fa5b4f | ||
|
|
bdfeef3763 | ||
|
|
b665811a5e | ||
|
|
7218ab7870 | ||
|
|
dad03c8e3d | ||
|
|
02ba32cade | ||
|
|
0fe1ff0e1b | ||
|
|
5784abbd9d | ||
|
|
a25dac0ad5 | ||
|
|
5766d2a6cb | ||
|
|
299f48eb8e | ||
|
|
bd7011c0ef | ||
|
|
b8e7ff7ea5 | ||
|
|
4646b441ae | ||
|
|
0db0905a89 | ||
|
|
24f1ed585a | ||
|
|
2148fd95d2 | ||
|
|
6e0d478e8e | ||
|
|
e6e86af878 | ||
|
|
5a57672c86 | ||
|
|
bfea4f3483 | ||
|
|
7af970de0a | ||
|
|
37c36eb673 | ||
|
|
2639c3ba6b | ||
|
|
d6f2610fa1 | ||
|
|
7fc46aae45 | ||
|
|
8aacd9f2b3 | ||
|
|
7deca69075 | ||
|
|
e07dc34bf2 | ||
|
|
1412c5912b | ||
|
|
427e20a0b2 | ||
|
|
4febdeb2bf | ||
|
|
036fe20cfd | ||
|
|
cd106d28d0 | ||
|
|
abbba50c1e | ||
|
|
6c0f2cf1e8 | ||
|
|
b505821b29 | ||
|
|
00734f12e6 | ||
|
|
a8819e7f42 | ||
|
|
fad9989a18 | ||
|
|
be5e2e2d66 | ||
|
|
36a5802c91 | ||
|
|
cee95719d4 | ||
|
|
4dab2be02d | ||
|
|
f168e09cf4 | ||
|
|
c3873754fc | ||
|
|
9916e58b37 | ||
|
|
2196c1dfa5 | ||
|
|
3da2c6c279 | ||
|
|
f54a58ebbb | ||
|
|
e9f46f7088 | ||
|
|
f06cf23111 | ||
|
|
a4b582b4cc | ||
|
|
56b4b01038 | ||
|
|
9aba66634e | ||
|
|
63ddd66306 | ||
|
|
232e832c1d | ||
|
|
5e91709421 | ||
|
|
d37ea1e473 | ||
|
|
096ff5d0d8 | ||
|
|
e20f59a66a | ||
|
|
2cb44c8933 | ||
|
|
64703c8497 | ||
|
|
d6e74a2ef5 | ||
|
|
b1d491165e | ||
|
|
e94c3a2355 | ||
|
|
71b05e16f8 | ||
|
|
4beed65983 | ||
|
|
ca03fcff33 | ||
|
|
d3333322c4 | ||
|
|
2b8172f25c | ||
|
|
9496292816 | ||
|
|
cf32549a22 | ||
|
|
13abf6da82 | ||
|
|
c72f697d57 | ||
|
|
03cfe18acd | ||
|
|
9939ef76c1 | ||
|
|
ae387507f7 | ||
|
|
969443acd1 | ||
|
|
3606612135 | ||
|
|
1ee48edfb5 |
12
.github/FUNDING.yml
vendored
@@ -1,12 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [vabene1111]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
94
.github/dependabot.yml
vendored
@@ -1,26 +1,92 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
open-pull-requests-limit: 20
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
interval: "daily"
|
||||
cooldown:
|
||||
semver-major-days: 30
|
||||
semver-minor-days: 7
|
||||
semver-patch-days: 3
|
||||
exclude:
|
||||
- "recipe-scrapers"
|
||||
directory: "/"
|
||||
groups:
|
||||
pip-patches:
|
||||
exclude-patterns: ["pytest*"]
|
||||
update-types: ["patch"]
|
||||
pip-minors:
|
||||
exclude-patterns: ["pytest*"]
|
||||
update-types: ["minor"]
|
||||
pip-pytest:
|
||||
patterns: ["pytest*"]
|
||||
update-types: ["minor", "patch"]
|
||||
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
labels:
|
||||
- "python"
|
||||
- "dependencies"
|
||||
- "automerge"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/vue/"
|
||||
open-pull-requests-limit: 20
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
cooldown:
|
||||
semver-major-days: 30
|
||||
semver-minor-days: 3
|
||||
semver-patch-days: 3
|
||||
directory: "vue"
|
||||
ignore:
|
||||
- dependency-name: "vue"
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "vue*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "pinia"
|
||||
update-types: ["version-update:semver-major"]
|
||||
groups:
|
||||
npm-patches:
|
||||
patterns: ["*"]
|
||||
update-types: ["patch"]
|
||||
npm-minors:
|
||||
patterns: ["*"]
|
||||
update-types: ["minor"]
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
labels:
|
||||
- "javascript"
|
||||
- "dependencies"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
open-pull-requests-limit: 20
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
interval: "weekly"
|
||||
directory: "/.github/"
|
||||
groups:
|
||||
actions-updates:
|
||||
patterns: ["*"]
|
||||
update-types: ["patch", "minor"]
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
labels:
|
||||
- "github_actions"
|
||||
- "dependencies"
|
||||
- "automerge"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
open-pull-requests-limit: 5
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
directory: "/"
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
labels:
|
||||
- "docker"
|
||||
- "dependencies"
|
||||
|
||||
|
||||
4
.github/workflows/build-docker-open-data.yml
vendored
@@ -1,6 +1,8 @@
|
||||
name: Build Docker Container with open data plugin installed
|
||||
|
||||
on: push
|
||||
on:
|
||||
push:
|
||||
branches: [disabled]
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
|
||||
57
.github/workflows/build-docker.yml
vendored
@@ -1,6 +1,8 @@
|
||||
name: Build Docker Container
|
||||
|
||||
on: push
|
||||
on:
|
||||
push:
|
||||
branches: [tandoor-1]
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
@@ -17,27 +19,16 @@ jobs:
|
||||
# Standard build config
|
||||
- name: Standard
|
||||
dockerfile: Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64
|
||||
suffix: ""
|
||||
continue-on-error: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get version number
|
||||
id: get_version
|
||||
run: |
|
||||
if [[ "$GITHUB_REF" = refs/tags/* ]]; then
|
||||
echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
|
||||
elif [[ "$GITHUB_REF" = refs/heads/beta ]]; then
|
||||
echo VERSION=beta >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo VERSION=develop >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Build Vue frontend
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '20'
|
||||
cache: yarn
|
||||
cache-dependency-path: vue/yarn.lock
|
||||
- name: Install dependencies
|
||||
@@ -75,11 +66,8 @@ jobs:
|
||||
latest=false
|
||||
suffix=${{ matrix.suffix }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=ref,event=branch
|
||||
type=raw,value=tandoor-v1-{{date 'YYYYMMDD'}}
|
||||
type=raw,value=tandoor-v1
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
@@ -93,34 +81,3 @@ jobs:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
notify-stable:
|
||||
name: Notify Stable
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-container
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- name: Set tag name
|
||||
run: |
|
||||
# Strip "refs/tags/" prefix
|
||||
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
# Send stable discord notification
|
||||
- name: Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: '🚀 Version {{ VERSION }} of tandoor has been released 🥳 Check it out https://github.com/vabene1111/recipes/releases/tag/{{ VERSION }}'
|
||||
|
||||
notify-beta:
|
||||
name: Notify Beta
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-container
|
||||
if: github.ref == 'refs/heads/beta'
|
||||
steps:
|
||||
# Send beta discord notification
|
||||
- name: Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_BETA_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: '🚀 The BETA Image has been updated! 🥳'
|
||||
|
||||
7
.github/workflows/ci.yml
vendored
@@ -1,6 +1,11 @@
|
||||
name: Continuous Integration
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches: [disabled]
|
||||
pull_request:
|
||||
branches: [disabled]
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
5
.github/workflows/codeql-analysis.yml
vendored
@@ -2,9 +2,10 @@ name: "Code scanning - action"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [disabled]
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 13 * * 2'
|
||||
branches: [disabled]
|
||||
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
|
||||
235
.github/workflows/create-upstream-pr.yml
vendored
Normal file
@@ -0,0 +1,235 @@
|
||||
name: Create Upstream PR
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Push Workflow"]
|
||||
types:
|
||||
- completed
|
||||
branches: [working]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
create-upstream-pr:
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: upstream-pr
|
||||
cancel-in-progress: true
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: Generate GitHub App token (for branch push)
|
||||
id: generate_token_push
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.BOT_APP_ID }}
|
||||
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout fork
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.generate_token_push.outputs.token }}
|
||||
|
||||
- name: Setup git user
|
||||
run: |
|
||||
git config user.name "GitHub Action"
|
||||
git config user.email "action@github.com"
|
||||
|
||||
- name: Add upstream remote
|
||||
run: |
|
||||
git remote get-url upstream || git remote add upstream https://github.com/TandoorRecipes/recipes.git
|
||||
git fetch upstream
|
||||
|
||||
- name: Ensure jq is available
|
||||
run: |
|
||||
if ! command -v jq &> /dev/null; then
|
||||
sudo apt-get update && sudo apt-get install -y jq
|
||||
fi
|
||||
|
||||
- name: Create upstream PR branch
|
||||
id: create_branch
|
||||
run: |
|
||||
BRANCH_NAME="upstream-pr-$(date +%Y%m%d-%H%M%S)"
|
||||
git checkout -b "$BRANCH_NAME" || { echo "❌ Failed to create branch $BRANCH_NAME"; exit 1; }
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
echo "✅ Created branch: $BRANCH_NAME"
|
||||
|
||||
- name: Restore upstream infrastructure files
|
||||
id: restore_infra
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.create_branch.outputs.branch_name }}"
|
||||
git checkout "$BRANCH_NAME"
|
||||
git rm .gitattributes || echo "ℹ️ .gitattributes not present, skipping removal."
|
||||
git checkout upstream/tandoor-1 -- .github/workflows/ || echo "ℹ️ No workflows to restore."
|
||||
git checkout upstream/tandoor-1 -- cookbook/version_info.py || echo "ℹ️ No version_info.py to restore."
|
||||
git add .
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m $'Restore upstream infrastructure files for PR\n\n- Removed fork-specific .gitattributes\n- Restored upstream .github/workflows/\n- Restored upstream cookbook/version_info.py'
|
||||
echo "✅ Infrastructure files restored and committed."
|
||||
else
|
||||
echo "ℹ️ No infrastructure changes to commit."
|
||||
fi
|
||||
|
||||
- name: Push branch to fork (after infra commit)
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token_push.outputs.token }}
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.create_branch.outputs.branch_name }}"
|
||||
echo "Pushing branch $BRANCH_NAME after infra file restore."
|
||||
git push --set-upstream https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git "$BRANCH_NAME"
|
||||
echo "✅ Branch pushed: $BRANCH_NAME (infra files)"
|
||||
|
||||
|
||||
- name: Merge upstream branch
|
||||
id: merge_upstream
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.create_branch.outputs.branch_name }}"
|
||||
git checkout "$BRANCH_NAME"
|
||||
if git merge --no-edit upstream/tandoor-1; then
|
||||
echo "✅ Merged upstream/tandoor-1 into $BRANCH_NAME"
|
||||
if ! git diff --cached --quiet || [ -n "$(git log origin/$BRANCH_NAME..$BRANCH_NAME --oneline)" ]; then
|
||||
echo "merge_commit=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "merge_commit=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "❌ Merge conflict detected during merge with upstream/tandoor-1. Please resolve conflicts manually." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Push branch to fork (after merge)
|
||||
if: steps.merge_upstream.outputs.merge_commit == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token_push.outputs.token }}
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.create_branch.outputs.branch_name }}"
|
||||
echo "Pushing branch $BRANCH_NAME after merge."
|
||||
git push https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git "$BRANCH_NAME"
|
||||
echo "✅ Branch pushed: $BRANCH_NAME (after merge)"
|
||||
|
||||
|
||||
- name: Get commit list
|
||||
id: get_commits
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.create_branch.outputs.branch_name }}"
|
||||
COMMITS_RAW=$(git log upstream/tandoor-1..$BRANCH_NAME --oneline)
|
||||
if [ -z "$COMMITS_RAW" ]; then
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "✅ No commits to contribute - exiting gracefully"
|
||||
exit 0
|
||||
fi
|
||||
echo "commits_raw<<EOF" >> $GITHUB_OUTPUT
|
||||
printf "%s\n" "$COMMITS_RAW" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
- name: Get changed files
|
||||
id: get_files
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.create_branch.outputs.branch_name }}"
|
||||
CHANGED_FILES=$(git diff upstream/tandoor-1..$BRANCH_NAME --name-only)
|
||||
echo "changed_files<<EOF" >> $GITHUB_OUTPUT
|
||||
printf "%s\n" "$CHANGED_FILES" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
CODE_COUNT=$(echo "$CHANGED_FILES" | grep -c '^' || true)
|
||||
echo "code_count=$CODE_COUNT" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Summarize changes
|
||||
id: summarize_changes
|
||||
run: |
|
||||
CODE_COUNT=${{ steps.get_files.outputs.code_count }}
|
||||
CHANGES_SUMMARY="Modified $CODE_COUNT code files"
|
||||
echo "changes_summary=$CHANGES_SUMMARY" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Prepare commit subjects and JSON
|
||||
id: prepare_commits
|
||||
run: |
|
||||
COMMITS_RAW="${{ steps.get_commits.outputs.commits_raw }}"
|
||||
CODE_FILES=( $(echo "${{ steps.get_files.outputs.changed_files }}") )
|
||||
FILTERED_COMMITS_JSON="[]"
|
||||
COMMIT_SUBJECTS_ARRAY=()
|
||||
INFRA_PATTERNS='^\.github/|^cookbook/version_info\.py$|^\.gitattributes$'
|
||||
while IFS= read -r commit_line; do
|
||||
if [ -z "$commit_line" ]; then continue; fi
|
||||
COMMIT_SHA=$(echo "$commit_line" | cut -d' ' -f1)
|
||||
COMMIT_SUBJECT=$(echo "$commit_line" | cut -d' ' -f2-)
|
||||
mapfile -t COMMIT_FILES < <(git diff-tree --no-commit-id --name-only -r "$COMMIT_SHA" | grep -Ev "$INFRA_PATTERNS")
|
||||
# Only include commit if it touches at least one non-infra file that is still different
|
||||
INCLUDE_COMMIT=false
|
||||
for file in "${COMMIT_FILES[@]}"; do
|
||||
for code_file in "${CODE_FILES[@]}"; do
|
||||
if [ "$file" = "$code_file" ]; then
|
||||
INCLUDE_COMMIT=true
|
||||
break 2
|
||||
fi
|
||||
done
|
||||
done
|
||||
if [ "$INCLUDE_COMMIT" = true ]; then
|
||||
COMMIT_SUBJECTS_ARRAY+=("- $COMMIT_SUBJECT")
|
||||
FILES_JSON=$(printf '%s\n' "${COMMIT_FILES[@]}" | jq -R . | jq -s .)
|
||||
COMMIT_JSON=$(jq -n --arg sha "$COMMIT_SHA" --arg subject "$COMMIT_SUBJECT" --argjson files "$FILES_JSON" '{sha: $sha, subject: $subject, files: $files}')
|
||||
FILTERED_COMMITS_JSON=$(echo "$FILTERED_COMMITS_JSON" | jq --argjson item "$COMMIT_JSON" '. + [$item]')
|
||||
fi
|
||||
done <<< "$COMMITS_RAW"
|
||||
echo 'commits_json<<EOF' >> $GITHUB_OUTPUT
|
||||
printf "%s\n" "$FILTERED_COMMITS_JSON" >> $GITHUB_OUTPUT
|
||||
echo 'EOF' >> $GITHUB_OUTPUT
|
||||
echo "commit_subjects<<EOF" >> $GITHUB_OUTPUT
|
||||
printf '%s\n' "${COMMIT_SUBJECTS_ARRAY[@]}" >> $GITHUB_OUTPUT
|
||||
echo 'EOF' >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
- name: Build PR content
|
||||
if: steps.get_commits.outputs.has_changes == 'true'
|
||||
id: build_pr_content
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
COMMITS_JSON: ${{ steps.prepare_commits.outputs.commits_json }}
|
||||
CHANGES_SUMMARY: ${{ steps.summarize_changes.outputs.changes_summary }}
|
||||
BRANCH_NAME: ${{ steps.create_branch.outputs.branch_name }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
with:
|
||||
github-token: ${{ steps.generate_token_push.outputs.token }}
|
||||
script: |
|
||||
const commits = JSON.parse(process.env.COMMITS_JSON || '[]');
|
||||
const changesSummary = process.env.CHANGES_SUMMARY || 'Changes from fork';
|
||||
const branchName = process.env.BRANCH_NAME || '';
|
||||
const repo = process.env.GITHUB_REPOSITORY || '';
|
||||
const [owner, reponame] = repo.split('/');
|
||||
const nCommits = commits.length;
|
||||
let prTitle = 'Sync ' + nCommits + ' commit' + (nCommits !== 1 ? 's' : '') + ' from fork:';
|
||||
if (nCommits > 0) {
|
||||
prTitle += ' ' + commits[0].subject;
|
||||
}
|
||||
let prBody = `This PR syncs ${nCommits} commit${nCommits !== 1 ? 's' : ''} from branch ${branchName}.\n\n`;
|
||||
prBody += `**Changes Summary:**\n${changesSummary}\n\n`;
|
||||
prBody += `Commits included:\n`;
|
||||
for (const c of commits) {
|
||||
prBody += `- ${c.subject} ([${c.sha}](https://github.com/${owner}/${reponame}/commit/${c.sha}))\n`;
|
||||
}
|
||||
prBody += `\n---\n`;
|
||||
core.setOutput('prTitle', prTitle);
|
||||
core.setOutput('prBody', prBody);
|
||||
|
||||
|
||||
- name: Print PR creation instructions
|
||||
if: steps.get_commits.outputs.has_changes == 'true'
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.create_branch.outputs.branch_name }}
|
||||
PR_TITLE: ${{ steps.build_pr_content.outputs.prTitle }}
|
||||
PR_BODY: ${{ steps.build_pr_content.outputs.prBody }}
|
||||
run: |
|
||||
echo "✅ Branch pushed: $BRANCH_NAME"
|
||||
echo
|
||||
echo "To create a pull request, open:"
|
||||
echo "https://github.com/TandoorRecipes/recipes/compare/tandoor-1...${{ github.repository_owner }}:$BRANCH_NAME?expand=1"
|
||||
echo
|
||||
echo "Suggested PR title:"
|
||||
echo "$PR_TITLE"
|
||||
echo
|
||||
echo "Suggested PR body:"
|
||||
echo "$PR_BODY"
|
||||
68
.github/workflows/dependabot-automerge.yml
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
name: Dependabot Auto-merge
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
if: contains(github.event.pull_request.labels.*.name, 'automerge') && github.event.pull_request.user.login != 'dependabot[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Remove automerge label
|
||||
uses: actions-ecosystem/action-remove-labels@d05162525702062b6bdef750ed8594fc024b3ed7 # v1.3.0
|
||||
with:
|
||||
labels: automerge
|
||||
|
||||
- name: Add invalid label
|
||||
uses: actions-ecosystem/action-add-labels@1a9c3715c0037e96b97bb38cb4c4b56a1f1d4871 # v1.1.0
|
||||
with:
|
||||
labels: invalid
|
||||
|
||||
- name: Comment restriction
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body: |
|
||||
⚠️ **Automerge Restriction Notice**
|
||||
|
||||
The `automerge` label has been removed from this PR because it has restricted use. Only PRs created by `dependabot[bot]` are allowed to use the automerge functionality.
|
||||
|
||||
If you believe this is an error, please contact a repository maintainer.
|
||||
|
||||
auto-merge:
|
||||
if: github.actor == 'dependabot[bot]' && contains(github.event.pull_request.labels.*.name, 'automerge')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Generate token
|
||||
id: generate_token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.BOT_APP_ID }}
|
||||
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
||||
|
||||
- name: Auto-approve PR
|
||||
uses: hmarr/auto-approve-action@b40d6c9ed2fa10c9a2749eca7eb004418a705501 # v4.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate_token.outputs.token }}
|
||||
review-message: |
|
||||
🤖 **Dependabot Auto-merge**
|
||||
|
||||
This PR has been automatically approved and enabled for auto-merge. It will be merged automatically once all required checks pass.
|
||||
|
||||
- name: Enable auto-merge
|
||||
uses: daneden/enable-automerge-action@f8558b65c5b8d8bfb592c4e74e3d491624a38fbd # v1.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate_token.outputs.token }}
|
||||
allowed-author: "dependabot[bot]"
|
||||
merge-method: REBASE # Allowed values: MERGE | SQUASH | REBASE
|
||||
75
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: publish docker image
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
concurrency:
|
||||
group: docker-build-${{ github.ref }}
|
||||
name: Build Tandoor Container
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: ${{ matrix.continue-on-error }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# Standard build config
|
||||
- name: Standard
|
||||
dockerfile: Dockerfile
|
||||
platforms: linux/amd64
|
||||
suffix: ""
|
||||
continue-on-error: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get version number
|
||||
id: get_version
|
||||
run: |
|
||||
echo VERSION=latest >> $GITHUB_OUTPUT
|
||||
|
||||
# Build Vue frontend
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: yarn
|
||||
cache-dependency-path: vue/yarn.lock
|
||||
- name: Install dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn build
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@05340d1c670183e7caabdb33ae9f1c80fae3b0c2 # v3.1.0
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.7.1
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@3d100841f68d4548bf57e52eb27bd33ec5069f55 # v3.3.0
|
||||
with:
|
||||
username: ${{ secrets.USERNAME }}
|
||||
password: ${{ secrets.PASSWORD }}
|
||||
- name: Docker Metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@be19121bfd18b9c1ac415d9571d4f67b9b357886 # v5.6.0
|
||||
with:
|
||||
images: |
|
||||
smilerz/recipes
|
||||
tags: |
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.10.0
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
pull: true
|
||||
push: true
|
||||
platforms: ${{ matrix.platforms }}
|
||||
tags: '${{ steps.meta.outputs.tags }}'
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
57
.github/workflows/push-orchestrator.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: Push Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [working]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
detect-pr-merge:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
pr_merged: ${{ steps.detect_pr.outputs.result }}
|
||||
steps:
|
||||
- name: Check if commit is part of a PR
|
||||
id: detect_pr
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const prs = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: context.sha
|
||||
});
|
||||
|
||||
const pr = prs.data.find(pr => pr.merged_at);
|
||||
const merged = pr ? 'true' : 'false';
|
||||
|
||||
// Set the output explicitly
|
||||
core.setOutput('pr_merged', merged);
|
||||
return merged;
|
||||
|
||||
|
||||
run-ci:
|
||||
needs: detect-pr-merge
|
||||
if: needs.detect-pr-merge.outputs.pr_merged != 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
uses: ./.github/workflows/ci.yml
|
||||
secrets: inherit
|
||||
|
||||
docker-publish:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs: [detect-pr-merge, run-ci]
|
||||
if: |
|
||||
needs.detect-pr-merge.outputs.pr_merged == 'true' ||
|
||||
needs.run-ci.result == 'success'
|
||||
uses: ./.github/workflows/docker-publish.yml
|
||||
secrets: inherit
|
||||
268
.github/workflows/stage-branch-for-pr.yml
vendored
Normal file
@@ -0,0 +1,268 @@
|
||||
name: Stage Branch for PR
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Push Workflow"]
|
||||
types:
|
||||
- completed
|
||||
branches: [working]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
create-upstream-pr:
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: upstream-pr
|
||||
cancel-in-progress: true
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: Generate GitHub App token (for branch push)
|
||||
id: generate_token_push
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.BOT_APP_ID }}
|
||||
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout fork
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.generate_token_push.outputs.token }}
|
||||
|
||||
- name: Setup git user
|
||||
run: |
|
||||
git config user.name "GitHub Action"
|
||||
git config user.email "action@github.com"
|
||||
|
||||
- name: Add upstream remote
|
||||
run: |
|
||||
git remote get-url upstream || git remote add upstream https://github.com/TandoorRecipes/recipes.git
|
||||
git fetch upstream
|
||||
|
||||
- name: Ensure jq is available
|
||||
run: |
|
||||
if ! command -v jq &> /dev/null; then
|
||||
sudo apt-get update && sudo apt-get install -y jq
|
||||
fi
|
||||
|
||||
- name: Create upstream PR branch
|
||||
id: create_branch
|
||||
run: |
|
||||
BRANCH_NAME="upstream-pr-$(date +%Y%m%d-%H%M%S)"
|
||||
git checkout -b "$BRANCH_NAME" || { echo "❌ Failed to create branch $BRANCH_NAME"; exit 1; }
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
echo "✅ Created branch: $BRANCH_NAME"
|
||||
|
||||
- name: Restore upstream infrastructure files
|
||||
id: restore_infra
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.create_branch.outputs.branch_name }}"
|
||||
git checkout "$BRANCH_NAME"
|
||||
git rm .gitattributes || echo "ℹ️ .gitattributes not present, skipping removal."
|
||||
git checkout upstream/tandoor-1 -- .github/workflows/ || echo "ℹ️ No workflows to restore."
|
||||
git checkout upstream/tandoor-1 -- cookbook/version_info.py || echo "ℹ️ No version_info.py to restore."
|
||||
git add .
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m $'Restore upstream infrastructure files for PR\n\n- Removed fork-specific .gitattributes\n- Restored upstream .github/workflows/\n- Restored upstream cookbook/version_info.py'
|
||||
echo "✅ Infrastructure files restored and committed."
|
||||
else
|
||||
echo "ℹ️ No infrastructure changes to commit."
|
||||
fi
|
||||
|
||||
- name: Push branch to fork (after infra commit)
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token_push.outputs.token }}
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.create_branch.outputs.branch_name }}"
|
||||
echo "Pushing branch $BRANCH_NAME after infra file restore."
|
||||
git push --set-upstream https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git "$BRANCH_NAME"
|
||||
echo "✅ Branch pushed: $BRANCH_NAME (infra files)"
|
||||
|
||||
|
||||
- name: Merge upstream branch
|
||||
id: merge_upstream
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.create_branch.outputs.branch_name }}"
|
||||
git checkout "$BRANCH_NAME"
|
||||
if git merge --no-edit upstream/tandoor-1; then
|
||||
echo "✅ Merged upstream/tandoor-1 into $BRANCH_NAME"
|
||||
if ! git diff --cached --quiet || [ -n "$(git log origin/$BRANCH_NAME..$BRANCH_NAME --oneline)" ]; then
|
||||
echo "merge_commit=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "merge_commit=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "❌ Merge conflict detected during merge with upstream/tandoor-1. Please resolve conflicts manually." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Push branch to fork (after merge)
|
||||
if: steps.merge_upstream.outputs.merge_commit == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token_push.outputs.token }}
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.create_branch.outputs.branch_name }}"
|
||||
echo "Pushing branch $BRANCH_NAME after merge."
|
||||
git push https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git "$BRANCH_NAME"
|
||||
echo "✅ Branch pushed: $BRANCH_NAME (after merge)"
|
||||
|
||||
|
||||
- name: Get commit list
|
||||
id: get_commits
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.create_branch.outputs.branch_name }}"
|
||||
COMMITS_RAW=$(git log upstream/tandoor-1..$BRANCH_NAME --oneline)
|
||||
if [ -z "$COMMITS_RAW" ]; then
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "✅ No commits to contribute - exiting gracefully"
|
||||
exit 0
|
||||
fi
|
||||
echo "commits_raw<<EOF" >> $GITHUB_OUTPUT
|
||||
printf "%s\n" "$COMMITS_RAW" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
- name: Get changed files
|
||||
id: get_files
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.create_branch.outputs.branch_name }}"
|
||||
INFRA_PATTERNS='^\.github/|^cookbook/version_info\.py$|^\.gitattributes$'
|
||||
CHANGED_FILES=$(git diff upstream/tandoor-1..$BRANCH_NAME --name-only | grep -Ev "$INFRA_PATTERNS" || true)
|
||||
echo "changed_files<<EOF" >> $GITHUB_OUTPUT
|
||||
printf "%s\n" "$CHANGED_FILES" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
CODE_COUNT=$(echo "$CHANGED_FILES" | grep -c '^' || true)
|
||||
echo "code_count=$CODE_COUNT" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Summarize changes
|
||||
id: summarize_changes
|
||||
run: |
|
||||
CODE_COUNT=${{ steps.get_files.outputs.code_count }}
|
||||
CHANGED_FILES="${{ steps.get_files.outputs.changed_files }}"
|
||||
if [ "$CODE_COUNT" -eq 0 ]; then
|
||||
CHANGES_SUMMARY="No code files changed. No PR required."
|
||||
else
|
||||
CHANGES_SUMMARY="Modified $CODE_COUNT code files"
|
||||
CHANGES_SUMMARY+=$'\nFiles changed:'
|
||||
CHANGES_SUMMARY+=$'\n'$(echo "$CHANGED_FILES" | sed '/^$/d; s/^/- /')
|
||||
fi
|
||||
echo "changes_summary<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CHANGES_SUMMARY" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Prepare commit subjects and JSON
|
||||
id: prepare_commits
|
||||
run: |
|
||||
COMMITS_RAW="${{ steps.get_commits.outputs.commits_raw }}"
|
||||
CODE_FILES=( $(echo "${{ steps.get_files.outputs.changed_files }}") )
|
||||
FILTERED_COMMITS_JSON="[]"
|
||||
COMMIT_SUBJECTS_ARRAY=()
|
||||
INFRA_PATTERNS='^\.github/|^cookbook/version_info\.py$|^\.gitattributes$'
|
||||
while IFS= read -r commit_line; do
|
||||
if [ -z "$commit_line" ]; then continue; fi
|
||||
COMMIT_SHA=$(echo "$commit_line" | cut -d' ' -f1)
|
||||
COMMIT_SUBJECT=$(echo "$commit_line" | cut -d' ' -f2-)
|
||||
mapfile -t COMMIT_FILES < <(git diff-tree --no-commit-id --name-only -r "$COMMIT_SHA" | grep -Ev "$INFRA_PATTERNS")
|
||||
# Only include commit if it touches at least one non-infra file that is still different
|
||||
INCLUDE_COMMIT=false
|
||||
for file in "${COMMIT_FILES[@]}"; do
|
||||
for code_file in "${CODE_FILES[@]}"; do
|
||||
if [ "$file" = "$code_file" ]; then
|
||||
INCLUDE_COMMIT=true
|
||||
break 2
|
||||
fi
|
||||
done
|
||||
done
|
||||
if [ "$INCLUDE_COMMIT" = true ]; then
|
||||
COMMIT_SUBJECTS_ARRAY+=("- $COMMIT_SUBJECT")
|
||||
FILES_JSON=$(printf '%s\n' "${COMMIT_FILES[@]}" | jq -R . | jq -s .)
|
||||
COMMIT_JSON=$(jq -n --arg sha "$COMMIT_SHA" --arg subject "$COMMIT_SUBJECT" --argjson files "$FILES_JSON" '{sha: $sha, subject: $subject, files: $files}')
|
||||
FILTERED_COMMITS_JSON=$(echo "$FILTERED_COMMITS_JSON" | jq --argjson item "$COMMIT_JSON" '. + [$item]')
|
||||
fi
|
||||
done <<< "$COMMITS_RAW"
|
||||
echo 'commits_json<<EOF' >> $GITHUB_OUTPUT
|
||||
printf "%s\n" "$FILTERED_COMMITS_JSON" >> $GITHUB_OUTPUT
|
||||
echo 'EOF' >> $GITHUB_OUTPUT
|
||||
echo "commit_subjects<<EOF" >> $GITHUB_OUTPUT
|
||||
printf '%s\n' "${COMMIT_SUBJECTS_ARRAY[@]}" >> $GITHUB_OUTPUT
|
||||
echo 'EOF' >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
- name: Build PR content
|
||||
if: steps.get_commits.outputs.has_changes == 'true'
|
||||
id: build_pr_content
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
COMMITS_JSON: ${{ steps.prepare_commits.outputs.commits_json }}
|
||||
CHANGES_SUMMARY: ${{ steps.summarize_changes.outputs.changes_summary }}
|
||||
BRANCH_NAME: ${{ steps.create_branch.outputs.branch_name }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
with:
|
||||
github-token: ${{ steps.generate_token_push.outputs.token }}
|
||||
script: |
|
||||
const commits = JSON.parse(process.env.COMMITS_JSON || '[]');
|
||||
const changesSummary = process.env.CHANGES_SUMMARY || 'Changes from fork';
|
||||
const branchName = process.env.BRANCH_NAME || '';
|
||||
const repo = process.env.GITHUB_REPOSITORY || '';
|
||||
const [owner, reponame] = repo.split('/');
|
||||
const nCommits = commits.length;
|
||||
let prTitle = 'Sync ' + nCommits + ' commit' + (nCommits !== 1 ? 's' : '') + ' from fork:';
|
||||
if (nCommits > 0) {
|
||||
prTitle += ' ' + commits[0].subject;
|
||||
}
|
||||
let prBody = `This PR syncs ${nCommits} commit${nCommits !== 1 ? 's' : ''} from branch ${branchName}.\n\n`;
|
||||
prBody += `**Changes Summary:**\n${changesSummary}\n\n`;
|
||||
prBody += `Commits included:\n`;
|
||||
for (const c of commits) {
|
||||
prBody += `- ${c.subject} ([${c.sha}](https://github.com/${owner}/${reponame}/commit/${c.sha}))\n`;
|
||||
}
|
||||
core.setOutput('prTitle', prTitle);
|
||||
core.setOutput('prBody', prBody);
|
||||
|
||||
|
||||
- name: Open issue for PR checklist
|
||||
if: steps.get_files.outputs.code_count != '0' && steps.prepare_commits.outputs.commit_subjects != ''
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.create_branch.outputs.branch_name }}
|
||||
PR_TITLE: ${{ steps.build_pr_content.outputs.prTitle }}
|
||||
PR_BODY: ${{ steps.build_pr_content.outputs.prBody }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const branch = process.env.BRANCH_NAME || '';
|
||||
const prTitle = process.env.PR_TITLE || '';
|
||||
const prBody = process.env.PR_BODY || '';
|
||||
const repo = process.env.GITHUB_REPOSITORY || '';
|
||||
const [owner, reponame] = repo.split('/');
|
||||
const prLink = `https://github.com/TandoorRecipes/recipes/compare/tandoor-1...${owner}:${branch}?expand=1&title=${encodeURIComponent(prTitle)}`;
|
||||
const issueTitle = `Manual Upstream PR Checklist: ${branch}`;
|
||||
const issueBody = [
|
||||
`A new branch is ready for upstream PR submission.`,
|
||||
'',
|
||||
`- [ ] Submit PR: [Create PR](${prLink})`,
|
||||
'- [ ] PR merged',
|
||||
'- [ ] Branch deleted',
|
||||
'',
|
||||
'**Suggested PR title:**',
|
||||
'```',
|
||||
prTitle,
|
||||
'```',
|
||||
'**Suggested PR body:**',
|
||||
'```',
|
||||
prBody,
|
||||
'```',
|
||||
].join('\n');
|
||||
await github.rest.issues.create({
|
||||
owner,
|
||||
repo: reponame,
|
||||
title: issueTitle,
|
||||
body: issueBody
|
||||
});
|
||||
28
.vscode/launch.json
vendored
@@ -1,18 +1,18 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python Debugger: Django",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/manage.py",
|
||||
"args": ["runserver"],
|
||||
"django": true,
|
||||
"justMyCode": true
|
||||
},
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python Debugger: Django",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/manage.py",
|
||||
"args": ["runserver"],
|
||||
"django": true,
|
||||
"justMyCode": true
|
||||
},
|
||||
{
|
||||
"name": "Python: Debug Tests",
|
||||
"type": "debugpy",
|
||||
|
||||
25
.vscode/settings.json
vendored
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"python.testing.pytestArgs": [
|
||||
"cookbook/tests"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "eeyore.yapf",
|
||||
},
|
||||
"yapf.args": [],
|
||||
"isort.args": []
|
||||
"python.testing.pytestArgs": ["cookbook/tests"],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "eeyore.yapf"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"yapf.args": [],
|
||||
"isort.args": []
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.12-alpine3.19
|
||||
FROM python:3.12.12-alpine3.21
|
||||
|
||||
#Install all dependencies.
|
||||
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git
|
||||
|
||||
@@ -1,29 +1,62 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from cookbook.models import ShoppingListEntry, Space, ConnectorConfig
|
||||
from cookbook.models import ConnectorConfig, ShoppingListEntry, User
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserDTO:
|
||||
username: str
|
||||
first_name: Optional[str]
|
||||
|
||||
@staticmethod
|
||||
def create_from_user(instance: User) -> 'UserDTO':
|
||||
return UserDTO(username=instance.username, first_name=instance.first_name if instance.first_name else None)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShoppingListEntryDTO:
|
||||
food_name: str
|
||||
amount: Optional[float]
|
||||
base_unit: Optional[str]
|
||||
unit_name: Optional[str]
|
||||
created_by: UserDTO
|
||||
|
||||
@staticmethod
|
||||
def try_create_from_entry(instance: ShoppingListEntry) -> Optional['ShoppingListEntryDTO']:
|
||||
if instance.food is None or instance.created_by is None:
|
||||
return None
|
||||
|
||||
return ShoppingListEntryDTO(
|
||||
food_name=instance.food.name,
|
||||
amount=instance.amount if instance.amount else None,
|
||||
unit_name=instance.unit.name if instance.unit else None,
|
||||
base_unit=instance.unit.base_unit if instance.unit and instance.unit.base_unit else None,
|
||||
created_by=UserDTO.create_from_user(instance.created_by),
|
||||
)
|
||||
|
||||
|
||||
# A Connector is 'destroyed' & recreated each time 'any' ConnectorConfig in a space changes.
|
||||
class Connector(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def __init__(self, config: ConnectorConfig):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def on_shopping_list_entry_created(self, space: Space, instance: ShoppingListEntry) -> None:
|
||||
async def on_shopping_list_entry_created(self, instance: ShoppingListEntryDTO) -> None:
|
||||
pass
|
||||
|
||||
# This method might not trigger on 'direct' entry updates: https://stackoverflow.com/a/35238823
|
||||
@abstractmethod
|
||||
async def on_shopping_list_entry_updated(self, space: Space, instance: ShoppingListEntry) -> None:
|
||||
async def on_shopping_list_entry_updated(self, instance: ShoppingListEntryDTO) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def on_shopping_list_entry_deleted(self, space: Space, instance: ShoppingListEntry) -> None:
|
||||
async def on_shopping_list_entry_deleted(self, instance: ShoppingListEntryDTO) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
||||
# TODO: Add Recipes & possibly Meal Place listeners/hooks (And maybe more?)
|
||||
|
||||
@@ -7,14 +7,14 @@ from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from logging import Logger
|
||||
from types import UnionType
|
||||
from typing import List, Any, Dict, Optional, Type
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
|
||||
from django.conf import settings
|
||||
from django_scopes import scope
|
||||
|
||||
from cookbook.connectors.connector import Connector
|
||||
from cookbook.connectors.connector import Connector, ShoppingListEntryDTO
|
||||
from cookbook.connectors.homeassistant import HomeAssistant
|
||||
from cookbook.models import ShoppingListEntry, Space, ConnectorConfig
|
||||
from cookbook.models import ConnectorConfig, ShoppingListEntry, Space
|
||||
|
||||
REGISTERED_CLASSES: UnionType | Type = ShoppingListEntry
|
||||
|
||||
@@ -56,15 +56,16 @@ class ConnectorManager(metaclass=Singleton):
|
||||
|
||||
def __init__(self):
|
||||
self._logger = logging.getLogger("recipes.connector")
|
||||
self._logger.debug("ConnectorManager initializing")
|
||||
self._queue = queue.Queue(maxsize=settings.EXTERNAL_CONNECTORS_QUEUE_SIZE)
|
||||
self._worker = threading.Thread(target=self.worker, args=(0, self._queue,), daemon=True)
|
||||
self._worker = threading.Thread(target=self.worker, args=(
|
||||
0,
|
||||
self._queue,
|
||||
), daemon=True)
|
||||
self._worker.start()
|
||||
|
||||
# Called by post save & post delete signals
|
||||
def __call__(self, instance: Any, **kwargs) -> None:
|
||||
if not isinstance(instance, self._listening_to_classes) or not hasattr(instance, "space"):
|
||||
return
|
||||
|
||||
action_type: ActionType
|
||||
if "created" in kwargs and kwargs["created"]:
|
||||
action_type = ActionType.CREATED
|
||||
@@ -75,16 +76,37 @@ class ConnectorManager(metaclass=Singleton):
|
||||
else:
|
||||
return
|
||||
|
||||
try:
|
||||
self._queue.put_nowait(Work(instance, action_type))
|
||||
except queue.Full:
|
||||
self._logger.info(f"queue was full, so skipping {action_type} of type {type(instance)}")
|
||||
return
|
||||
self._add_work(action_type, instance)
|
||||
|
||||
def _add_work(self, action_type: ActionType, *instances: REGISTERED_CLASSES):
|
||||
for instance in instances:
|
||||
if not isinstance(instance, self._listening_to_classes) or not hasattr(instance, "space"):
|
||||
continue
|
||||
try:
|
||||
_force_load_instance(instance)
|
||||
self._queue.put_nowait(Work(instance, action_type))
|
||||
except queue.Full:
|
||||
self._logger.info(f"queue was full, so skipping {action_type} of type {type(instance)}")
|
||||
|
||||
def stop(self):
|
||||
self._queue.join()
|
||||
self._worker.join()
|
||||
|
||||
@classmethod
|
||||
def is_initialized(cls):
|
||||
return cls in cls._instances
|
||||
|
||||
@staticmethod
|
||||
def add_work(action_type: ActionType, *instances: REGISTERED_CLASSES):
|
||||
"""
|
||||
Manually inject work that failed to come in through the __call__ (aka Django signal)
|
||||
Before the work is processed, we check if the connectionManager is initialized, because if it's not, we don't want to accidentally initialize it.
|
||||
Be careful calling it, because it might result in a instance being processed twice.
|
||||
"""
|
||||
if not ConnectorManager.is_initialized():
|
||||
return
|
||||
ConnectorManager()._add_work(action_type, *instances)
|
||||
|
||||
@staticmethod
|
||||
def worker(worker_id: int, worker_queue: queue.Queue):
|
||||
logger = logging.getLogger("recipes.connector.worker")
|
||||
@@ -116,7 +138,7 @@ class ConnectorManager(metaclass=Singleton):
|
||||
|
||||
if connectors is None or refresh_connector_cache:
|
||||
if connectors is not None:
|
||||
loop.run_until_complete(close_connectors(connectors))
|
||||
loop.run_until_complete(_close_connectors(connectors))
|
||||
|
||||
with scope(space=space):
|
||||
connectors: List[Connector] = list()
|
||||
@@ -142,7 +164,7 @@ class ConnectorManager(metaclass=Singleton):
|
||||
|
||||
logger.debug(f"running {len(connectors)} connectors for {item.instance=} with {item.actionType=}")
|
||||
|
||||
loop.run_until_complete(run_connectors(connectors, space, item.instance, item.actionType))
|
||||
loop.run_until_complete(run_connectors(connectors, item.instance, item.actionType))
|
||||
worker_queue.task_done()
|
||||
|
||||
logger.info(f"terminating ConnectionManager worker {worker_id}")
|
||||
@@ -159,7 +181,14 @@ class ConnectorManager(metaclass=Singleton):
|
||||
return None
|
||||
|
||||
|
||||
async def close_connectors(connectors: List[Connector]):
|
||||
def _force_load_instance(instance: REGISTERED_CLASSES):
|
||||
if isinstance(instance, ShoppingListEntry):
|
||||
_ = instance.food # Force load food
|
||||
_ = instance.unit # Force load unit
|
||||
_ = instance.created_by # Force load created_by
|
||||
|
||||
|
||||
async def _close_connectors(connectors: List[Connector]):
|
||||
tasks: List[Task] = [asyncio.create_task(connector.close()) for connector in connectors]
|
||||
|
||||
if len(tasks) == 0:
|
||||
@@ -171,22 +200,24 @@ async def close_connectors(connectors: List[Connector]):
|
||||
logging.exception("received an exception while closing one of the connectors")
|
||||
|
||||
|
||||
async def run_connectors(connectors: List[Connector], space: Space, instance: REGISTERED_CLASSES, action_type: ActionType):
|
||||
async def run_connectors(connectors: List[Connector], instance: REGISTERED_CLASSES, action_type: ActionType):
|
||||
tasks: List[Task] = list()
|
||||
|
||||
if isinstance(instance, ShoppingListEntry):
|
||||
shopping_list_entry: ShoppingListEntry = instance
|
||||
shopping_list_entry = ShoppingListEntryDTO.try_create_from_entry(instance)
|
||||
if shopping_list_entry is None:
|
||||
return
|
||||
|
||||
match action_type:
|
||||
case ActionType.CREATED:
|
||||
for connector in connectors:
|
||||
tasks.append(asyncio.create_task(connector.on_shopping_list_entry_created(space, shopping_list_entry)))
|
||||
tasks.append(asyncio.create_task(connector.on_shopping_list_entry_created(shopping_list_entry)))
|
||||
case ActionType.UPDATED:
|
||||
for connector in connectors:
|
||||
tasks.append(asyncio.create_task(connector.on_shopping_list_entry_updated(space, shopping_list_entry)))
|
||||
tasks.append(asyncio.create_task(connector.on_shopping_list_entry_updated(shopping_list_entry)))
|
||||
case ActionType.DELETED:
|
||||
for connector in connectors:
|
||||
tasks.append(asyncio.create_task(connector.on_shopping_list_entry_deleted(space, shopping_list_entry)))
|
||||
tasks.append(asyncio.create_task(connector.on_shopping_list_entry_deleted(shopping_list_entry)))
|
||||
|
||||
if len(tasks) == 0:
|
||||
return
|
||||
|
||||
@@ -37,7 +37,7 @@ def get_filetype(name):
|
||||
|
||||
def is_file_type_allowed(filename, image_only=False):
|
||||
is_file_allowed = False
|
||||
allowed_file_types = ['.pdf','.docx', '.xlsx']
|
||||
allowed_file_types = ['.pdf', '.docx', '.xlsx']
|
||||
allowed_image_types = ['.png', '.jpg', '.jpeg', '.gif', '.webp']
|
||||
check_list = allowed_image_types
|
||||
if not image_only:
|
||||
@@ -49,6 +49,7 @@ def is_file_type_allowed(filename, image_only=False):
|
||||
|
||||
return is_file_allowed
|
||||
|
||||
|
||||
# TODO this whole file needs proper documentation, refactoring, and testing
|
||||
# TODO also add env variable to define which images sizes should be compressed
|
||||
# filetype argument can not be optional, otherwise this function will treat all images as if they were a jpeg
|
||||
|
||||
@@ -58,8 +58,7 @@ class IngredientParser:
|
||||
def parse_fraction(self, x):
|
||||
if len(x) == 1 and 'fraction' in unicodedata.decomposition(x):
|
||||
frac_split = unicodedata.decomposition(x[-1:]).split()
|
||||
return (float((frac_split[1]).replace('003', ''))
|
||||
/ float((frac_split[3]).replace('003', '')))
|
||||
return (float((frac_split[1]).replace('003', '')) / float((frac_split[3]).replace('003', '')))
|
||||
else:
|
||||
frac_split = x.split('/')
|
||||
if not len(frac_split) == 2:
|
||||
@@ -78,12 +77,7 @@ class IngredientParser:
|
||||
|
||||
did_check_frac = False
|
||||
end = 0
|
||||
while (end < len(x) and (x[end] in string.digits
|
||||
or (
|
||||
(x[end] == '.' or x[end] == ',' or x[end] == '/')
|
||||
and end + 1 < len(x)
|
||||
and x[end + 1] in string.digits
|
||||
))):
|
||||
while (end < len(x) and (x[end] in string.digits or ((x[end] == '.' or x[end] == ',' or x[end] == '/') and end + 1 < len(x) and x[end + 1] in string.digits))):
|
||||
end += 1
|
||||
if end > 0:
|
||||
if "/" in x[:end]:
|
||||
@@ -107,8 +101,9 @@ class IngredientParser:
|
||||
if unit is not None and unit.strip() == '':
|
||||
unit = None
|
||||
|
||||
if unit is not None and (unit.startswith('(') or unit.startswith(
|
||||
'-')): # i dont know any unit that starts with ( or - so its likely an alternative like 1L (500ml) Water or 2-3
|
||||
if unit is not None and (
|
||||
unit.startswith('(') or unit.startswith('-')
|
||||
): # i dont know any unit that starts with ( or - so its likely an alternative like 1L (500ml) Water or 2-3
|
||||
unit = None
|
||||
note = x
|
||||
return amount, unit, note
|
||||
|
||||
@@ -9,8 +9,7 @@ from django.utils import timezone, translation
|
||||
|
||||
from cookbook.helper.HelperFunctions import Round, str2bool
|
||||
from cookbook.managers import DICTIONARY
|
||||
from cookbook.models import (CookLog, CustomFilter, Food, Keyword, Recipe, SearchFields,
|
||||
SearchPreference, ViewLog)
|
||||
from cookbook.models import CookLog, CustomFilter, Food, Keyword, Recipe, SearchFields, SearchPreference, ViewLog
|
||||
from recipes import settings
|
||||
|
||||
|
||||
@@ -22,11 +21,8 @@ class RecipeSearch():
|
||||
self._request = request
|
||||
self._queryset = None
|
||||
if f := params.get('filter', None):
|
||||
custom_filter = (
|
||||
CustomFilter.objects.filter(id=f, space=self._request.space)
|
||||
.filter(Q(created_by=self._request.user) | Q(shared=self._request.user) | Q(recipebook__shared=self._request.user))
|
||||
.first()
|
||||
)
|
||||
custom_filter = (CustomFilter.objects.filter(id=f, space=self._request.space).filter(
|
||||
Q(created_by=self._request.user) | Q(shared=self._request.user) | Q(recipebook__shared=self._request.user)).first())
|
||||
if custom_filter:
|
||||
self._params = {**json.loads(custom_filter.search)}
|
||||
self._original_params = {**(params or {})}
|
||||
@@ -38,7 +34,7 @@ class RecipeSearch():
|
||||
else:
|
||||
self._params = {**(params or {})}
|
||||
if self._request.user.is_authenticated:
|
||||
CACHE_KEY = f'search_pref_{request.user.id}'
|
||||
CACHE_KEY = f"search_pref_{request.user.id}"
|
||||
cached_result = cache.get(CACHE_KEY, default=None)
|
||||
if cached_result is not None:
|
||||
self._search_prefs = cached_result
|
||||
@@ -47,44 +43,43 @@ class RecipeSearch():
|
||||
cache.set(CACHE_KEY, self._search_prefs, timeout=10)
|
||||
else:
|
||||
self._search_prefs = SearchPreference()
|
||||
self._string = self._params.get('query').strip(
|
||||
) if self._params.get('query', None) else None
|
||||
self._rating = self._params.get('rating', None)
|
||||
self._string = self._params.get("query").strip() if self._params.get("query", None) else None
|
||||
self._rating = self._params.get("rating", None)
|
||||
self._keywords = {
|
||||
'or': self._params.get('keywords_or', None) or self._params.get('keywords', None),
|
||||
'and': self._params.get('keywords_and', None),
|
||||
'or_not': self._params.get('keywords_or_not', None),
|
||||
'and_not': self._params.get('keywords_and_not', None)
|
||||
"or": self._params.get("keywords_or", None) or self._params.get("keywords", None),
|
||||
"and": self._params.get("keywords_and", None),
|
||||
"or_not": self._params.get("keywords_or_not", None),
|
||||
"and_not": self._params.get("keywords_and_not", None),
|
||||
}
|
||||
self._foods = {
|
||||
'or': self._params.get('foods_or', None) or self._params.get('foods', None),
|
||||
'and': self._params.get('foods_and', None),
|
||||
'or_not': self._params.get('foods_or_not', None),
|
||||
'and_not': self._params.get('foods_and_not', None)
|
||||
"or": self._params.get("foods_or", None) or self._params.get("foods", None),
|
||||
"and": self._params.get("foods_and", None),
|
||||
"or_not": self._params.get("foods_or_not", None),
|
||||
"and_not": self._params.get("foods_and_not", None),
|
||||
}
|
||||
self._books = {
|
||||
'or': self._params.get('books_or', None) or self._params.get('books', None),
|
||||
'and': self._params.get('books_and', None),
|
||||
'or_not': self._params.get('books_or_not', None),
|
||||
'and_not': self._params.get('books_and_not', None)
|
||||
"or": self._params.get("books_or", None) or self._params.get("books", None),
|
||||
"and": self._params.get("books_and", None),
|
||||
"or_not": self._params.get("books_or_not", None),
|
||||
"and_not": self._params.get("books_and_not", None),
|
||||
}
|
||||
self._steps = self._params.get('steps', None)
|
||||
self._units = self._params.get('units', None)
|
||||
self._steps = self._params.get("steps", None)
|
||||
self._units = self._params.get("units", None)
|
||||
# TODO add created by
|
||||
# TODO image exists
|
||||
self._sort_order = self._params.get('sort_order', None)
|
||||
self._internal = str2bool(self._params.get('internal', None))
|
||||
self._random = str2bool(self._params.get('random', False))
|
||||
self._new = str2bool(self._params.get('new', False))
|
||||
self._num_recent = int(self._params.get('num_recent', 0))
|
||||
self._include_children = str2bool(
|
||||
self._params.get('include_children', None))
|
||||
self._timescooked = self._params.get('timescooked', None)
|
||||
self._cookedon = self._params.get('cookedon', None)
|
||||
self._createdon = self._params.get('createdon', None)
|
||||
self._updatedon = self._params.get('updatedon', None)
|
||||
self._viewedon = self._params.get('viewedon', None)
|
||||
self._makenow = self._params.get('makenow', None)
|
||||
self._sort_order = self._params.get("sort_order", None)
|
||||
self._internal = str2bool(self._params.get("internal", None))
|
||||
self._random = str2bool(self._params.get("random", False))
|
||||
self._new = str2bool(self._params.get("new", False))
|
||||
self._num_recent = int(self._params.get("num_recent", 0))
|
||||
self._include_children = str2bool(self._params.get("include_children", None))
|
||||
self._timescooked = self._params.get("timescooked", None)
|
||||
self._cookedon = self._params.get("cookedon", None)
|
||||
self._createdon = self._params.get("createdon", None)
|
||||
self._updatedon = self._params.get("updatedon", None)
|
||||
self._viewedon = self._params.get("viewedon", None)
|
||||
self._makenow = self._params.get("makenow", None)
|
||||
self._never_used_food = str2bool(self._params.get("never_used_food", False))
|
||||
# this supports hidden feature to find recipes missing X ingredients
|
||||
if isinstance(self._makenow, bool) and self._makenow == True:
|
||||
self._makenow = 0
|
||||
@@ -96,7 +91,7 @@ class RecipeSearch():
|
||||
except (ValueError, TypeError):
|
||||
self._makenow = None
|
||||
|
||||
self._search_type = self._search_prefs.search or 'plain'
|
||||
self._search_type = self._search_prefs.search or "plain"
|
||||
if self._string:
|
||||
if self._postgres:
|
||||
self._unaccent_include = self._search_prefs.unaccent.values_list('field', flat=True)
|
||||
@@ -114,11 +109,7 @@ class RecipeSearch():
|
||||
|
||||
if self._search_type not in ['websearch', 'raw'] and self._trigram_include:
|
||||
self._trigram = True
|
||||
self.search_query = SearchQuery(
|
||||
self._string,
|
||||
search_type=self._search_type,
|
||||
config=self._language,
|
||||
)
|
||||
self.search_query = SearchQuery(self._string, search_type=self._search_type, config=self._language, )
|
||||
self.search_rank = None
|
||||
self.orderby = []
|
||||
self._filters = None
|
||||
@@ -127,7 +118,6 @@ class RecipeSearch():
|
||||
def get_queryset(self, queryset):
|
||||
self._queryset = queryset
|
||||
self._queryset = self._queryset.prefetch_related('keywords')
|
||||
|
||||
self._build_sort_order()
|
||||
self._recently_viewed(num_recent=self._num_recent)
|
||||
self._cooked_on_filter(cooked_date=self._cookedon)
|
||||
@@ -144,6 +134,7 @@ class RecipeSearch():
|
||||
self.step_filters(steps=self._steps)
|
||||
self.unit_filters(units=self._units)
|
||||
self._makenow_filter(missing=self._makenow)
|
||||
self._never_used_food_filter()
|
||||
self.string_filters(string=self._string)
|
||||
return self._queryset.filter(space=self._request.space).order_by(*self.orderby)
|
||||
|
||||
@@ -151,22 +142,22 @@ class RecipeSearch():
|
||||
for x in args:
|
||||
if x in self.orderby:
|
||||
return True
|
||||
elif '-' + x in self.orderby:
|
||||
elif "-" + x in self.orderby:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _build_sort_order(self):
|
||||
if self._random:
|
||||
self.orderby = ['?']
|
||||
self.orderby = ["?"]
|
||||
else:
|
||||
order = []
|
||||
# TODO add userpreference for default sort order and replace '-favorite'
|
||||
default_order = ['name']
|
||||
default_order = ["name"]
|
||||
# recent and new_recipe are always first; they float a few recipes to the top
|
||||
if self._num_recent:
|
||||
order += ['-recent']
|
||||
order += ["-recent"]
|
||||
if self._new:
|
||||
order += ['-new_recipe']
|
||||
order += ["-new_recipe"]
|
||||
|
||||
# if a sort order is provided by user - use that order
|
||||
if self._sort_order:
|
||||
@@ -175,20 +166,18 @@ class RecipeSearch():
|
||||
else:
|
||||
order += self._sort_order
|
||||
if not self._postgres or not self._string:
|
||||
if 'score' in order:
|
||||
order.remove('score')
|
||||
if '-score' in order:
|
||||
order.remove('-score')
|
||||
if "score" in order:
|
||||
order.remove("score")
|
||||
if "-score" in order:
|
||||
order.remove("-score")
|
||||
# if no sort order provided prioritize text search, followed by the default search
|
||||
elif self._postgres and self._string and (self._trigram or self._fulltext_include):
|
||||
order += ['-score', *default_order]
|
||||
order += ["-score", *default_order]
|
||||
# otherwise sort by the remaining order_by attributes or favorite by default
|
||||
else:
|
||||
order += default_order
|
||||
order[:] = [Lower('name').asc() if x ==
|
||||
'name' else x for x in order]
|
||||
order[:] = [Lower('name').desc() if x ==
|
||||
'-name' else x for x in order]
|
||||
order[:] = [Lower("name").asc() if x == "name" else x for x in order]
|
||||
order[:] = [Lower("name").desc() if x == "-name" else x for x in order]
|
||||
self.orderby = order
|
||||
|
||||
def string_filters(self, string=None):
|
||||
@@ -223,7 +212,7 @@ class RecipeSearch():
|
||||
self._queryset = self._queryset.annotate(score=F('rank') + F('simularity'))
|
||||
else:
|
||||
query_filter = Q()
|
||||
for f in [x + '__unaccent__iexact' if x in self._unaccent_include else x + '__iexact' for x in SearchFields.objects.all().values_list('field', flat=True)]:
|
||||
for f in [x + "__unaccent__iexact" if x in self._unaccent_include else x + "__iexact" for x in SearchFields.objects.all().values_list("field", flat=True)]:
|
||||
query_filter |= Q(**{"%s" % f: self._string})
|
||||
self._queryset = self._queryset.filter(query_filter).distinct()
|
||||
|
||||
@@ -235,12 +224,11 @@ class RecipeSearch():
|
||||
else:
|
||||
default = timezone.now()
|
||||
self._queryset = self._queryset.annotate(
|
||||
lastcooked=Coalesce(Max(Case(When(cooklog__created_by=self._request.user, cooklog__space=self._request.space, then='cooklog__created_at'))), Value(default))
|
||||
)
|
||||
lastcooked=Coalesce(Max(Case(When(cooklog__created_by=self._request.user, cooklog__space=self._request.space, then='cooklog__created_at'))), Value(default)))
|
||||
if cooked_date is None:
|
||||
return
|
||||
|
||||
cooked_date = date(*[int(x)for x in cooked_date.split('-') if x != ''])
|
||||
cooked_date = date(*[int(x) for x in cooked_date.split('-') if x != ''])
|
||||
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
|
||||
@@ -261,22 +249,21 @@ class RecipeSearch():
|
||||
if updated_date is None:
|
||||
return
|
||||
lessthan = '-' in updated_date[:1]
|
||||
updated_date = date(*[int(x)for x in updated_date.split('-') if x != ''])
|
||||
updated_date = date(*[int(x) for x in updated_date.split('-') if x != ''])
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(updated_at__date__lte=updated_date)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(updated_at__date__gte=updated_date)
|
||||
|
||||
def _viewed_on_filter(self, viewed_date=None):
|
||||
if self._sort_includes('lastviewed') or viewed_date:
|
||||
if self._sort_includes("lastviewed") or viewed_date:
|
||||
longTimeAgo = timezone.now() - timedelta(days=100000)
|
||||
self._queryset = self._queryset.annotate(
|
||||
lastviewed=Coalesce(Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__created_at'))), Value(longTimeAgo))
|
||||
)
|
||||
lastviewed=Coalesce(Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__created_at'))), Value(longTimeAgo)))
|
||||
if viewed_date is None:
|
||||
return
|
||||
lessthan = '-' in viewed_date[:1]
|
||||
viewed_date = date(*[int(x)for x in viewed_date.split('-') if x != ''])
|
||||
viewed_date = date(*[int(x) for x in viewed_date.split('-') if x != ''])
|
||||
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||
@@ -287,24 +274,17 @@ class RecipeSearch():
|
||||
# TODO make new days a user-setting
|
||||
if not self._new:
|
||||
return
|
||||
self._queryset = self._queryset.annotate(
|
||||
new_recipe=Case(
|
||||
When(created_at__gte=(timezone.now() - timedelta(days=new_days)), then=('pk')),
|
||||
default=Value(0),
|
||||
)
|
||||
)
|
||||
self._queryset = self._queryset.annotate(new_recipe=Case(When(created_at__gte=(timezone.now() - timedelta(days=new_days)), then=('pk')), default=Value(0), ))
|
||||
|
||||
def _recently_viewed(self, num_recent=None):
|
||||
if not num_recent:
|
||||
if self._sort_includes('lastviewed'):
|
||||
self._queryset = self._queryset.annotate(lastviewed=Coalesce(
|
||||
Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__pk'))), Value(0)))
|
||||
if self._sort_includes("lastviewed"):
|
||||
self._queryset = self._queryset.annotate(
|
||||
lastviewed=Coalesce(Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then="viewlog__pk"))), Value(0)))
|
||||
return
|
||||
|
||||
num_recent_recipes = (
|
||||
ViewLog.objects.filter(created_by=self._request.user, space=self._request.space)
|
||||
.values('recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent]
|
||||
)
|
||||
num_recent_recipes = (ViewLog.objects.filter(created_by=self._request.user,
|
||||
space=self._request.space).values('recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent])
|
||||
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
|
||||
|
||||
def _favorite_recipes(self, times_cooked=None):
|
||||
@@ -314,17 +294,13 @@ class RecipeSearch():
|
||||
default = 1000
|
||||
else:
|
||||
default = 0
|
||||
favorite_recipes = (
|
||||
CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk'))
|
||||
.values('recipe')
|
||||
.annotate(count=Count('pk', distinct=True))
|
||||
.values('count')
|
||||
)
|
||||
favorite_recipes = (CookLog.objects.filter(created_by=self._request.user, space=self._request.space,
|
||||
recipe=OuterRef('pk')).values('recipe').annotate(count=Count('pk', distinct=True)).values('count'))
|
||||
self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default))
|
||||
if times_cooked is None:
|
||||
return
|
||||
|
||||
if times_cooked == '0':
|
||||
if times_cooked == "0":
|
||||
self._queryset = self._queryset.filter(favorite=0)
|
||||
elif less_than:
|
||||
self._queryset = self._queryset.filter(favorite__lte=int(times_cooked.replace('-', ''))).exclude(favorite=0)
|
||||
@@ -341,23 +317,23 @@ class RecipeSearch():
|
||||
kwargs[kw_filter] = [kwargs[kw_filter]]
|
||||
|
||||
keywords = Keyword.objects.filter(pk__in=kwargs[kw_filter])
|
||||
if 'or' in kw_filter:
|
||||
if "or" in kw_filter:
|
||||
if self._include_children:
|
||||
f_or = Q(keywords__in=Keyword.include_descendants(keywords))
|
||||
else:
|
||||
f_or = Q(keywords__in=keywords)
|
||||
if 'not' in kw_filter:
|
||||
if "not" in kw_filter:
|
||||
self._queryset = self._queryset.exclude(f_or)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(f_or)
|
||||
elif 'and' in kw_filter:
|
||||
elif "and" in kw_filter:
|
||||
recipes = Recipe.objects.all()
|
||||
for kw in keywords:
|
||||
if self._include_children:
|
||||
f_and = Q(keywords__in=kw.get_descendants_and_self())
|
||||
else:
|
||||
f_and = Q(keywords=kw)
|
||||
if 'not' in kw_filter:
|
||||
if "not" in kw_filter:
|
||||
recipes = recipes.filter(f_and)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(f_and)
|
||||
@@ -374,24 +350,24 @@ class RecipeSearch():
|
||||
kwargs[fd_filter] = [kwargs[fd_filter]]
|
||||
|
||||
foods = Food.objects.filter(pk__in=kwargs[fd_filter])
|
||||
if 'or' in fd_filter:
|
||||
if "or" in fd_filter:
|
||||
if self._include_children:
|
||||
f_or = Q(steps__ingredients__food__in=Food.include_descendants(foods))
|
||||
else:
|
||||
f_or = Q(steps__ingredients__food__in=foods)
|
||||
|
||||
if 'not' in fd_filter:
|
||||
if "not" in fd_filter:
|
||||
self._queryset = self._queryset.exclude(f_or)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(f_or)
|
||||
elif 'and' in fd_filter:
|
||||
elif "and" in fd_filter:
|
||||
recipes = Recipe.objects.all()
|
||||
for food in foods:
|
||||
if self._include_children:
|
||||
f_and = Q(steps__ingredients__food__in=food.get_descendants_and_self())
|
||||
else:
|
||||
f_and = Q(steps__ingredients__food=food)
|
||||
if 'not' in fd_filter:
|
||||
if "not" in fd_filter:
|
||||
recipes = recipes.filter(f_and)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(f_and)
|
||||
@@ -416,11 +392,11 @@ class RecipeSearch():
|
||||
else:
|
||||
default = 0
|
||||
# TODO make ratings a settings user-only vs all-users
|
||||
self._queryset = self._queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=self._request.user, then='cooklog__rating'), default=default))))
|
||||
self._queryset = self._queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=self._request.user, then="cooklog__rating"), default=default))))
|
||||
if rating is None:
|
||||
return
|
||||
|
||||
if rating == '0':
|
||||
if rating == "0":
|
||||
self._queryset = self._queryset.filter(rating=0)
|
||||
elif lessthan:
|
||||
self._queryset = self._queryset.filter(rating__lte=int(rating[1:])).exclude(rating=0)
|
||||
@@ -441,13 +417,13 @@ class RecipeSearch():
|
||||
if not isinstance(kwargs[bk_filter], list):
|
||||
kwargs[bk_filter] = [kwargs[bk_filter]]
|
||||
|
||||
if 'or' in bk_filter:
|
||||
if "or" in bk_filter:
|
||||
f = Q(recipebookentry__book__id__in=kwargs[bk_filter])
|
||||
if 'not' in bk_filter:
|
||||
if "not" in bk_filter:
|
||||
self._queryset = self._queryset.exclude(f)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(f)
|
||||
elif 'and' in bk_filter:
|
||||
elif "and" in bk_filter:
|
||||
recipes = Recipe.objects.all()
|
||||
for book in kwargs[bk_filter]:
|
||||
if 'not' in bk_filter:
|
||||
@@ -520,62 +496,66 @@ class RecipeSearch():
|
||||
trigram += TrigramSimilarity(f, self._string)
|
||||
else:
|
||||
trigram = TrigramSimilarity(f, self._string)
|
||||
self._fuzzy_match = (
|
||||
Recipe.objects.annotate(trigram=trigram)
|
||||
.distinct()
|
||||
.annotate(simularity=Max('trigram'))
|
||||
.values('id', 'simularity')
|
||||
.filter(simularity__gt=self._search_prefs.trigram_threshold)
|
||||
)
|
||||
self._fuzzy_match = (Recipe.objects.annotate(trigram=trigram).distinct().annotate(simularity=Max('trigram')).values('id', 'simularity').filter(
|
||||
simularity__gt=self._search_prefs.trigram_threshold))
|
||||
self._filters += [Q(pk__in=self._fuzzy_match.values('pk'))]
|
||||
|
||||
def _makenow_filter(self, missing=None):
|
||||
if missing is None or (isinstance(missing, bool) and missing == False):
|
||||
return
|
||||
shopping_users = [*self._request.user.get_shopping_share(), self._request.user]
|
||||
onhand_filter = Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
|
||||
|
||||
onhand_filter = (
|
||||
Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
|
||||
# ignore substitutions when also using the never_used_food filter
|
||||
if not self._never_used_food:
|
||||
# or substitute food onhand
|
||||
| Q(steps__ingredients__food__substitute__onhand_users__in=shopping_users)
|
||||
| Q(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users))
|
||||
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
|
||||
)
|
||||
makenow_recipes = Recipe.objects.annotate(
|
||||
count_food=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__isnull=False), distinct=True),
|
||||
count_onhand=Count('steps__ingredients__food__pk', filter=onhand_filter, distinct=True),
|
||||
count_ignore_shopping=Count(
|
||||
'steps__ingredients__food__pk', filter=Q(steps__ingredients__food__ignore_shopping=True, steps__ingredients__food__recipe__isnull=True), distinct=True
|
||||
),
|
||||
has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users), then=Value(1)), default=Value(0)),
|
||||
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users), then=Value(1)), default=Value(0))
|
||||
).annotate(missingfood=F('count_food') - F('count_onhand') - F('count_ignore_shopping')).filter(missingfood__lte=missing)
|
||||
onhand_filter |= (Q(steps__ingredients__food__substitute__onhand_users__in=shopping_users)
|
||||
| Q(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users))
|
||||
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users)))
|
||||
makenow_recipes = (Recipe.objects.annotate(count_food=Count("steps__ingredients__food__pk", filter=Q(steps__ingredients__food__isnull=False), distinct=True),
|
||||
count_onhand=Count("steps__ingredients__food__pk", filter=onhand_filter, distinct=True),
|
||||
count_ignore_shopping=Count("steps__ingredients__food__pk",
|
||||
filter=Q(steps__ingredients__food__ignore_shopping=True,
|
||||
steps__ingredients__food__recipe__isnull=True),
|
||||
distinct=True),
|
||||
has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users), then=Value(1)),
|
||||
default=Value(0)),
|
||||
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users), then=Value(1)),
|
||||
default=Value(0)),
|
||||
).annotate(missingfood=F("count_food") - F("count_onhand") - F("count_ignore_shopping")).filter(missingfood=missing))
|
||||
makenow_recipes = Recipe.objects.annotate(count_food=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__isnull=False), distinct=True),
|
||||
count_onhand=Count('steps__ingredients__food__pk', filter=onhand_filter, distinct=True),
|
||||
count_ignore_shopping=Count('steps__ingredients__food__pk',
|
||||
filter=Q(steps__ingredients__food__ignore_shopping=True,
|
||||
steps__ingredients__food__recipe__isnull=True),
|
||||
distinct=True),
|
||||
has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users), then=Value(1)),
|
||||
default=Value(0)),
|
||||
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users), then=Value(1)),
|
||||
default=Value(0))).annotate(missingfood=F('count_food') - F('count_onhand')
|
||||
- F('count_ignore_shopping')).filter(missingfood__lte=missing)
|
||||
self._queryset = self._queryset.distinct().filter(id__in=makenow_recipes.values('id'))
|
||||
|
||||
def _never_used_food_filter(self):
|
||||
# filters recipes to include foods that have never been used
|
||||
if not self._never_used_food:
|
||||
return
|
||||
self._queryset = self._queryset.filter(steps__ingredients__food__in=Food.objects.filter(~Q(ingredient__step__recipe__cooklog__isnull=False)).distinct())
|
||||
|
||||
@staticmethod
|
||||
def __children_substitute_filter(shopping_users=None):
|
||||
children_onhand_subquery = Food.objects.filter(path__startswith=OuterRef('path'), depth__gt=OuterRef('depth'), onhand_users__in=shopping_users)
|
||||
return (
|
||||
Food.objects.exclude( # list of foods that are onhand and children of: foods that are not onhand and are set to use children as substitutes
|
||||
Q(onhand_users__in=shopping_users) | Q(ignore_shopping=True, recipe__isnull=True) | Q(substitute__onhand_users__in=shopping_users)
|
||||
)
|
||||
.exclude(depth=1, numchild=0)
|
||||
.filter(substitute_children=True)
|
||||
.annotate(child_onhand_count=Exists(children_onhand_subquery))
|
||||
.filter(child_onhand_count=True)
|
||||
)
|
||||
return (Food.objects.exclude( # list of foods that are onhand and children of: foods that are not onhand and are set to use children as substitutes
|
||||
Q(onhand_users__in=shopping_users) | Q(ignore_shopping=True, recipe__isnull=True)
|
||||
| Q(substitute__onhand_users__in=shopping_users)).exclude(depth=1, numchild=0).filter(substitute_children=True).annotate(
|
||||
child_onhand_count=Exists(children_onhand_subquery)).filter(child_onhand_count=True))
|
||||
|
||||
@staticmethod
|
||||
def __sibling_substitute_filter(shopping_users=None):
|
||||
sibling_onhand_subquery = Food.objects.filter(
|
||||
path__startswith=Substr(OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)), depth=OuterRef('depth'), onhand_users__in=shopping_users
|
||||
)
|
||||
return (
|
||||
Food.objects.exclude( # list of foods that are onhand and siblings of: foods that are not onhand and are set to use siblings as substitutes
|
||||
Q(onhand_users__in=shopping_users) | Q(ignore_shopping=True, recipe__isnull=True) | Q(substitute__onhand_users__in=shopping_users)
|
||||
)
|
||||
.exclude(depth=1, numchild=0)
|
||||
.filter(substitute_siblings=True)
|
||||
.annotate(sibling_onhand=Exists(sibling_onhand_subquery))
|
||||
.filter(sibling_onhand=True)
|
||||
)
|
||||
sibling_onhand_subquery = Food.objects.filter(path__startswith=Substr(OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)),
|
||||
depth=OuterRef('depth'),
|
||||
onhand_users__in=shopping_users)
|
||||
return (Food.objects.exclude( # list of foods that are onhand and siblings of: foods that are not onhand and are set to use siblings as substitutes
|
||||
Q(onhand_users__in=shopping_users) | Q(ignore_shopping=True, recipe__isnull=True)
|
||||
| Q(substitute__onhand_users__in=shopping_users)).exclude(depth=1, numchild=0).filter(substitute_siblings=True).annotate(
|
||||
sibling_onhand=Exists(sibling_onhand_subquery)).filter(sibling_onhand=True))
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.2.22 on 2025-07-13 19:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0222_alter_shoppinglistrecipe_created_by_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userpreference',
|
||||
name='ingredient_context',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userpreference',
|
||||
name='use_fractions',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
@@ -191,7 +191,7 @@ class TreeModel(MP_Node):
|
||||
|
||||
return queryset.model.objects.filter(id__in=queryset.values_list('id')).exclude(descendants)
|
||||
|
||||
def include_ancestors(queryset=None):
|
||||
def include_ancestors(queryset=None, filter=None):
|
||||
"""
|
||||
:param queryset: Model Queryset to add ancestors
|
||||
:param filter: Filter (include) the ancestors nodes with the provided Q filter
|
||||
@@ -487,6 +487,7 @@ class UserPreference(models.Model, PermissionModelMixin):
|
||||
shopping_recent_days = models.PositiveIntegerField(default=7)
|
||||
csv_delim = models.CharField(max_length=2, default=",")
|
||||
csv_prefix = models.CharField(max_length=10, blank=True, )
|
||||
ingredient_context = models.BooleanField(default=False)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
objects = ScopedManager(space='space')
|
||||
@@ -767,6 +768,22 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
||||
obj.inherit_fields.set(fields)
|
||||
obj.save()
|
||||
|
||||
def get_substitutes(self, onhand=False, shopping_users=None):
|
||||
# filters = ~Q(id=self.id)
|
||||
filters = Q()
|
||||
if self.substitute:
|
||||
filters |= Q(id__in=self.substitute.values('id'))
|
||||
if self.substitute_children:
|
||||
filters |= Q(path__startswith=self.path, depth__gt=self.depth)
|
||||
if self.substitute_siblings:
|
||||
sibling_path = self.path[:Food.steplen * (self.depth - 1)]
|
||||
filters |= Q(path__startswith=sibling_path, depth=self.depth)
|
||||
|
||||
qs = Food.objects.filter(filters).exclude(id=self.id)
|
||||
if onhand:
|
||||
qs = qs.filter(onhand_users__in=shopping_users)
|
||||
return qs
|
||||
|
||||
@staticmethod
|
||||
def reset_inheritance(space=None, food=None):
|
||||
# resets inherited fields to the space defaults and updates all inherited fields to root object values
|
||||
|
||||
177
cookbook/static/pdfjs/LICENSE
Normal file
@@ -0,0 +1,177 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
0
cookbook/static/pdfjs/LICENSE:Zone.Identifier
Normal file
|
Before Width: | Height: | Size: 193 B |
|
Before Width: | Height: | Size: 296 B |
|
Before Width: | Height: | Size: 199 B |
|
Before Width: | Height: | Size: 304 B |
|
Before Width: | Height: | Size: 326 B |
|
Before Width: | Height: | Size: 326 B |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 403 B |
|
Before Width: | Height: | Size: 933 B |
|
Before Width: | Height: | Size: 179 B |
|
Before Width: | Height: | Size: 266 B |
|
Before Width: | Height: | Size: 301 B |
|
Before Width: | Height: | Size: 583 B |
|
Before Width: | Height: | Size: 175 B |
|
Before Width: | Height: | Size: 276 B |
|
Before Width: | Height: | Size: 360 B |
|
Before Width: | Height: | Size: 731 B |
|
Before Width: | Height: | Size: 359 B |
|
Before Width: | Height: | Size: 714 B |
|
Before Width: | Height: | Size: 218 B |
|
Before Width: | Height: | Size: 332 B |
|
Before Width: | Height: | Size: 228 B |
|
Before Width: | Height: | Size: 349 B |
|
Before Width: | Height: | Size: 297 B |
|
Before Width: | Height: | Size: 490 B |
|
Before Width: | Height: | Size: 461 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 347 B |
|
Before Width: | Height: | Size: 694 B |
|
Before Width: | Height: | Size: 179 B |
|
Before Width: | Height: | Size: 261 B |
|
Before Width: | Height: | Size: 344 B |
|
Before Width: | Height: | Size: 621 B |
|
Before Width: | Height: | Size: 290 B |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 174 B |
|
Before Width: | Height: | Size: 260 B |
|
Before Width: | Height: | Size: 259 B |
|
Before Width: | Height: | Size: 425 B |
|
Before Width: | Height: | Size: 107 B |
|
Before Width: | Height: | Size: 152 B |
|
Before Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 550 B |
|
Before Width: | Height: | Size: 238 B |
|
Before Width: | Height: | Size: 396 B |
|
Before Width: | Height: | Size: 246 B |
|
Before Width: | Height: | Size: 403 B |
|
Before Width: | Height: | Size: 321 B |
|
Before Width: | Height: | Size: 586 B |
|
Before Width: | Height: | Size: 257 B |
|
Before Width: | Height: | Size: 464 B |
|
Before Width: | Height: | Size: 309 B |
|
Before Width: | Height: | Size: 653 B |
|
Before Width: | Height: | Size: 243 B |
|
Before Width: | Height: | Size: 458 B |
|
Before Width: | Height: | Size: 225 B |
|
Before Width: | Height: | Size: 331 B |
|
Before Width: | Height: | Size: 384 B |
|
Before Width: | Height: | Size: 859 B |
|
Before Width: | Height: | Size: 178 B |
|
Before Width: | Height: | Size: 331 B |
|
Before Width: | Height: | Size: 185 B |
|
Before Width: | Height: | Size: 219 B |
|
Before Width: | Height: | Size: 136 B |
|
Before Width: | Height: | Size: 160 B |
|
Before Width: | Height: | Size: 88 B |
|
Before Width: | Height: | Size: 109 B |
|
Before Width: | Height: | Size: 128 B |
|
Before Width: | Height: | Size: 149 B |
|
Before Width: | Height: | Size: 125 B |
|
Before Width: | Height: | Size: 172 B |
@@ -1,207 +0,0 @@
|
||||
# Copyright 2012 Mozilla Foundation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Main toolbar buttons (tooltips and alt text for images)
|
||||
previous.title=Pot buk mukato
|
||||
previous_label=Mukato
|
||||
next.title=Pot buk malubo
|
||||
next_label=Malubo
|
||||
|
||||
# LOCALIZATION NOTE (page.title): The tooltip for the pageNumber input.
|
||||
page.title=Pot buk
|
||||
# LOCALIZATION NOTE (of_pages): "{{pagesCount}}" will be replaced by a number
|
||||
# representing the total number of pages in the document.
|
||||
of_pages=pi {{pagesCount}}
|
||||
# LOCALIZATION NOTE (page_of_pages): "{{pageNumber}}" and "{{pagesCount}}"
|
||||
# will be replaced by a number representing the currently visible page,
|
||||
# respectively a number representing the total number of pages in the document.
|
||||
page_of_pages=({{pageNumber}} me {{pagesCount}})
|
||||
|
||||
zoom_out.title=Jwik Matidi
|
||||
zoom_out_label=Jwik Matidi
|
||||
zoom_in.title=Kwot Madit
|
||||
zoom_in_label=Kwot Madit
|
||||
zoom.title=Kwoti
|
||||
presentation_mode.title=Lokke i kit me tyer
|
||||
presentation_mode_label=Kit me tyer
|
||||
open_file.title=Yab Pwail
|
||||
open_file_label=Yab
|
||||
print.title=Go
|
||||
print_label=Go
|
||||
download.title=Gam
|
||||
download_label=Gam
|
||||
bookmark.title=Neno ma kombedi (lok onyo yab i dirica manyen)
|
||||
bookmark_label=Neno ma kombedi
|
||||
|
||||
# Secondary toolbar and context menu
|
||||
tools.title=Gintic
|
||||
tools_label=Gintic
|
||||
first_page.title=Cit i pot buk mukwongo
|
||||
first_page.label=Cit i pot buk mukwongo
|
||||
first_page_label=Cit i pot buk mukwongo
|
||||
last_page.title=Cit i pot buk magiko
|
||||
last_page.label=Cit i pot buk magiko
|
||||
last_page_label=Cit i pot buk magiko
|
||||
page_rotate_cw.title=Wire i tung lacuc
|
||||
page_rotate_cw.label=Wire i tung lacuc
|
||||
page_rotate_cw_label=Wire i tung lacuc
|
||||
page_rotate_ccw.title=Wire i tung lacam
|
||||
page_rotate_ccw.label=Wire i tung lacam
|
||||
page_rotate_ccw_label=Wire i tung lacam
|
||||
|
||||
cursor_text_select_tool.title=Cak gitic me yero coc
|
||||
cursor_text_select_tool_label=Gitic me yero coc
|
||||
cursor_hand_tool.title=Cak gitic me cing
|
||||
cursor_hand_tool_label=Gitic cing
|
||||
|
||||
|
||||
|
||||
# Document properties dialog box
|
||||
document_properties.title=Jami me gin acoya…
|
||||
document_properties_label=Jami me gin acoya…
|
||||
document_properties_file_name=Nying pwail:
|
||||
document_properties_file_size=Dit pa pwail:
|
||||
# LOCALIZATION NOTE (document_properties_kb): "{{size_kb}}" and "{{size_b}}"
|
||||
# will be replaced by the PDF file size in kilobytes, respectively in bytes.
|
||||
document_properties_kb={{size_kb}} KB ({{size_b}} bytes)
|
||||
# LOCALIZATION NOTE (document_properties_mb): "{{size_mb}}" and "{{size_b}}"
|
||||
# will be replaced by the PDF file size in megabytes, respectively in bytes.
|
||||
document_properties_mb={{size_mb}} MB ({{size_b}} bytes)
|
||||
document_properties_title=Wiye:
|
||||
document_properties_author=Ngat mucoyo:
|
||||
document_properties_subject=Subjek:
|
||||
document_properties_keywords=Lok mapire tek:
|
||||
document_properties_creation_date=Nino dwe me cwec:
|
||||
document_properties_modification_date=Nino dwe me yub:
|
||||
# LOCALIZATION NOTE (document_properties_date_string): "{{date}}" and "{{time}}"
|
||||
# will be replaced by the creation/modification date, and time, of the PDF file.
|
||||
document_properties_date_string={{date}}, {{time}}
|
||||
document_properties_creator=Lacwec:
|
||||
document_properties_producer=Layub PDF:
|
||||
document_properties_version=Kit PDF:
|
||||
document_properties_page_count=Kwan me pot buk:
|
||||
document_properties_page_size=Dit pa potbuk:
|
||||
document_properties_page_size_unit_inches=i
|
||||
document_properties_page_size_unit_millimeters=mm
|
||||
document_properties_page_size_orientation_portrait=atir
|
||||
document_properties_page_size_orientation_landscape=arii
|
||||
document_properties_page_size_name_a3=A3
|
||||
document_properties_page_size_name_a4=A4
|
||||
document_properties_page_size_name_letter=Waraga
|
||||
document_properties_page_size_name_legal=Cik
|
||||
# LOCALIZATION NOTE (document_properties_page_size_dimension_string):
|
||||
# "{{width}}", "{{height}}", {{unit}}, and {{orientation}} will be replaced by
|
||||
# the size, respectively their unit of measurement and orientation, of the (current) page.
|
||||
document_properties_page_size_dimension_string={{width}} × {{height}} {{unit}} ({{orientation}})
|
||||
# LOCALIZATION NOTE (document_properties_page_size_dimension_name_string):
|
||||
# "{{width}}", "{{height}}", {{unit}}, {{name}}, and {{orientation}} will be replaced by
|
||||
# the size, respectively their unit of measurement, name, and orientation, of the (current) page.
|
||||
document_properties_page_size_dimension_name_string={{width}} × {{height}} {{unit}} ({{name}}, {{orientation}})
|
||||
# LOCALIZATION NOTE (document_properties_linearized): The linearization status of
|
||||
# the document; usually called "Fast Web View" in English locales of Adobe software.
|
||||
document_properties_linearized_yes=Eyo
|
||||
document_properties_linearized_no=Pe
|
||||
document_properties_close=Lor
|
||||
|
||||
print_progress_message=Yubo coc me agoya…
|
||||
# LOCALIZATION NOTE (print_progress_percent): "{{progress}}" will be replaced by
|
||||
# a numerical per cent value.
|
||||
print_progress_percent={{progress}}%
|
||||
print_progress_close=Juki
|
||||
|
||||
# Tooltips and alt text for side panel toolbar buttons
|
||||
# (the _label strings are alt text for the buttons, the .title strings are
|
||||
# tooltips)
|
||||
toggle_sidebar.title=Lok gintic ma inget
|
||||
toggle_sidebar_notification.title=Lok lanyut me nget (wiyewiye tye i gin acoya/attachments)
|
||||
toggle_sidebar_label=Lok gintic ma inget
|
||||
document_outline.title=Nyut Wiyewiye me Gin acoya (dii-kiryo me yaro/kano jami weng)
|
||||
document_outline_label=Pek pa gin acoya
|
||||
attachments.title=Nyut twec
|
||||
attachments_label=Twec
|
||||
thumbs.title=Nyut cal
|
||||
thumbs_label=Cal
|
||||
findbar.title=Nong iye gin acoya
|
||||
findbar_label=Nong
|
||||
|
||||
# Thumbnails panel item (tooltip and alt text for images)
|
||||
# LOCALIZATION NOTE (thumb_page_title): "{{page}}" will be replaced by the page
|
||||
# number.
|
||||
thumb_page_title=Pot buk {{page}}
|
||||
# LOCALIZATION NOTE (thumb_page_canvas): "{{page}}" will be replaced by the page
|
||||
# number.
|
||||
thumb_page_canvas=Cal me pot buk {{page}}
|
||||
|
||||
# Find panel button title and messages
|
||||
find_input.title=Nong
|
||||
find_input.placeholder=Nong i dokumen…
|
||||
find_previous.title=Nong timme pa lok mukato
|
||||
find_previous_label=Mukato
|
||||
find_next.title=Nong timme pa lok malubo
|
||||
find_next_label=Malubo
|
||||
find_highlight=Wer weng
|
||||
find_match_case_label=Lok marwate
|
||||
find_reached_top=Oo iwi gin acoya, omede ki i tere
|
||||
find_reached_bottom=Oo i agiki me gin acoya, omede ki iwiye
|
||||
find_not_found=Lok pe ononge
|
||||
|
||||
# Error panel labels
|
||||
error_more_info=Ngec Mukene
|
||||
error_less_info=Ngec Manok
|
||||
error_close=Lor
|
||||
# LOCALIZATION NOTE (error_version_info): "{{version}}" and "{{build}}" will be
|
||||
# replaced by the PDF.JS version and build ID.
|
||||
error_version_info=PDF.js v{{version}} (build: {{build}})
|
||||
# LOCALIZATION NOTE (error_message): "{{message}}" will be replaced by an
|
||||
# english string describing the error.
|
||||
error_message=Kwena: {{message}}
|
||||
# LOCALIZATION NOTE (error_stack): "{{stack}}" will be replaced with a stack
|
||||
# trace.
|
||||
error_stack=Can kikore {{stack}}
|
||||
# LOCALIZATION NOTE (error_file): "{{file}}" will be replaced with a filename
|
||||
error_file=Pwail: {{file}}
|
||||
# LOCALIZATION NOTE (error_line): "{{line}}" will be replaced with a line number
|
||||
error_line=Rek: {{line}}
|
||||
rendering_error=Bal otime i kare me nyuto pot buk.
|
||||
|
||||
# Predefined zoom values
|
||||
page_scale_width=Lac me iye pot buk
|
||||
page_scale_fit=Porre me pot buk
|
||||
page_scale_auto=Kwot pire kene
|
||||
page_scale_actual=Dite kikome
|
||||
# LOCALIZATION NOTE (page_scale_percent): "{{scale}}" will be replaced by a
|
||||
# numerical scale value.
|
||||
page_scale_percent={{scale}}%
|
||||
|
||||
# Loading indicator messages
|
||||
loading_error_indicator=Bal
|
||||
loading_error=Bal otime kun cano PDF.
|
||||
invalid_file_error=Pwail me PDF ma pe atir onyo obale woko.
|
||||
missing_file_error=Pwail me PDF tye ka rem.
|
||||
unexpected_response_error=Lagam mape kigeno pa lapok tic.
|
||||
|
||||
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
|
||||
# Some common types are e.g.: "Check", "Text", "Comment", "Note"
|
||||
text_annotation_type.alt=[{{type}} Lok angea manok]
|
||||
password_label=Ket mung me donyo me yabo pwail me PDF man.
|
||||
password_invalid=Mung me donyo pe atir. Tim ber i tem doki.
|
||||
password_ok=OK
|
||||
password_cancel=Juki
|
||||
|
||||
printing_not_supported=Ciko: Layeny ma pe teno goyo liweng.
|
||||
printing_not_ready=Ciko: PDF pe ocane weng me agoya.
|
||||
web_fonts_disabled=Kijuko dit pa coc me kakube woko: pe romo tic ki dit pa coc me PDF ma kiketo i kine.
|
||||
document_colors_not_allowed=Pe ki yee ki gin acoya me PDF me tic ki rangi gi kengi: Kijuko woko “Yee pot buk me yero rangi mamegi kengi” ki i layeny.
|
||||
@@ -1,184 +0,0 @@
|
||||
# Copyright 2012 Mozilla Foundation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Main toolbar buttons (tooltips and alt text for images)
|
||||
previous.title=Vorige bladsy
|
||||
previous_label=Vorige
|
||||
next.title=Volgende bladsy
|
||||
next_label=Volgende
|
||||
|
||||
# LOCALIZATION NOTE (page.title): The tooltip for the pageNumber input.
|
||||
page.title=Bladsy
|
||||
# LOCALIZATION NOTE (of_pages): "{{pagesCount}}" will be replaced by a number
|
||||
# representing the total number of pages in the document.
|
||||
of_pages=van {{pagesCount}}
|
||||
# LOCALIZATION NOTE (page_of_pages): "{{pageNumber}}" and "{{pagesCount}}"
|
||||
# will be replaced by a number representing the currently visible page,
|
||||
# respectively a number representing the total number of pages in the document.
|
||||
page_of_pages=({{pageNumber}} van {{pagesCount}})
|
||||
|
||||
zoom_out.title=Zoem uit
|
||||
zoom_out_label=Zoem uit
|
||||
zoom_in.title=Zoem in
|
||||
zoom_in_label=Zoem in
|
||||
zoom.title=Zoem
|
||||
presentation_mode.title=Wissel na voorleggingsmodus
|
||||
presentation_mode_label=Voorleggingsmodus
|
||||
open_file.title=Open lêer
|
||||
open_file_label=Open
|
||||
print.title=Druk
|
||||
print_label=Druk
|
||||
download.title=Laai af
|
||||
download_label=Laai af
|
||||
bookmark.title=Huidige aansig (kopieer of open in nuwe venster)
|
||||
bookmark_label=Huidige aansig
|
||||
|
||||
# Secondary toolbar and context menu
|
||||
tools.title=Nutsgoed
|
||||
tools_label=Nutsgoed
|
||||
first_page.title=Gaan na eerste bladsy
|
||||
first_page.label=Gaan na eerste bladsy
|
||||
first_page_label=Gaan na eerste bladsy
|
||||
last_page.title=Gaan na laaste bladsy
|
||||
last_page.label=Gaan na laaste bladsy
|
||||
last_page_label=Gaan na laaste bladsy
|
||||
page_rotate_cw.title=Roteer kloksgewys
|
||||
page_rotate_cw.label=Roteer kloksgewys
|
||||
page_rotate_cw_label=Roteer kloksgewys
|
||||
page_rotate_ccw.title=Roteer anti-kloksgewys
|
||||
page_rotate_ccw.label=Roteer anti-kloksgewys
|
||||
page_rotate_ccw_label=Roteer anti-kloksgewys
|
||||
|
||||
cursor_text_select_tool.title=Aktiveer gereedskap om teks te merk
|
||||
cursor_text_select_tool_label=Teksmerkgereedskap
|
||||
cursor_hand_tool.title=Aktiveer handjie
|
||||
cursor_hand_tool_label=Handjie
|
||||
|
||||
# Document properties dialog box
|
||||
document_properties.title=Dokumenteienskappe…
|
||||
document_properties_label=Dokumenteienskappe…
|
||||
document_properties_file_name=Lêernaam:
|
||||
document_properties_file_size=Lêergrootte:
|
||||
# LOCALIZATION NOTE (document_properties_kb): "{{size_kb}}" and "{{size_b}}"
|
||||
# will be replaced by the PDF file size in kilobytes, respectively in bytes.
|
||||
document_properties_kb={{size_kb}} kG ({{size_b}} grepe)
|
||||
# LOCALIZATION NOTE (document_properties_mb): "{{size_mb}}" and "{{size_b}}"
|
||||
# will be replaced by the PDF file size in megabytes, respectively in bytes.
|
||||
document_properties_mb={{size_mb}} MG ({{size_b}} grepe)
|
||||
document_properties_title=Titel:
|
||||
document_properties_author=Outeur:
|
||||
document_properties_subject=Onderwerp:
|
||||
document_properties_keywords=Sleutelwoorde:
|
||||
document_properties_creation_date=Skeppingsdatum:
|
||||
document_properties_modification_date=Wysigingsdatum:
|
||||
# LOCALIZATION NOTE (document_properties_date_string): "{{date}}" and "{{time}}"
|
||||
# will be replaced by the creation/modification date, and time, of the PDF file.
|
||||
document_properties_date_string={{date}}, {{time}}
|
||||
document_properties_creator=Skepper:
|
||||
document_properties_producer=PDF-vervaardiger:
|
||||
document_properties_version=PDF-weergawe:
|
||||
document_properties_page_count=Aantal bladsye:
|
||||
document_properties_close=Sluit
|
||||
|
||||
print_progress_message=Berei tans dokument voor om te druk…
|
||||
# LOCALIZATION NOTE (print_progress_percent): "{{progress}}" will be replaced by
|
||||
# a numerical per cent value.
|
||||
print_progress_percent={{progress}}%
|
||||
print_progress_close=Kanselleer
|
||||
|
||||
# Tooltips and alt text for side panel toolbar buttons
|
||||
# (the _label strings are alt text for the buttons, the .title strings are
|
||||
# tooltips)
|
||||
toggle_sidebar.title=Sypaneel aan/af
|
||||
toggle_sidebar_notification.title=Sypaneel aan/af (dokument bevat skema/aanhegsels)
|
||||
toggle_sidebar_label=Sypaneel aan/af
|
||||
document_outline.title=Wys dokumentskema (dubbelklik om alle items oop/toe te vou)
|
||||
document_outline_label=Dokumentoorsig
|
||||
attachments.title=Wys aanhegsels
|
||||
attachments_label=Aanhegsels
|
||||
thumbs.title=Wys duimnaels
|
||||
thumbs_label=Duimnaels
|
||||
findbar.title=Soek in dokument
|
||||
findbar_label=Vind
|
||||
|
||||
# Thumbnails panel item (tooltip and alt text for images)
|
||||
# LOCALIZATION NOTE (thumb_page_title): "{{page}}" will be replaced by the page
|
||||
# number.
|
||||
thumb_page_title=Bladsy {{page}}
|
||||
# LOCALIZATION NOTE (thumb_page_canvas): "{{page}}" will be replaced by the page
|
||||
# number.
|
||||
thumb_page_canvas=Duimnael van bladsy {{page}}
|
||||
|
||||
# Find panel button title and messages
|
||||
find_input.title=Vind
|
||||
find_input.placeholder=Soek in dokument…
|
||||
find_previous.title=Vind die vorige voorkoms van die frase
|
||||
find_previous_label=Vorige
|
||||
find_next.title=Vind die volgende voorkoms van die frase
|
||||
find_next_label=Volgende
|
||||
find_highlight=Verlig almal
|
||||
find_match_case_label=Kassensitief
|
||||
find_reached_top=Bokant van dokument is bereik; gaan voort van onder af
|
||||
find_reached_bottom=Einde van dokument is bereik; gaan voort van bo af
|
||||
find_not_found=Frase nie gevind nie
|
||||
|
||||
# Error panel labels
|
||||
error_more_info=Meer inligting
|
||||
error_less_info=Minder inligting
|
||||
error_close=Sluit
|
||||
# LOCALIZATION NOTE (error_version_info): "{{version}}" and "{{build}}" will be
|
||||
# replaced by the PDF.JS version and build ID.
|
||||
error_version_info=PDF.js v{{version}} (ID: {{build}})
|
||||
# LOCALIZATION NOTE (error_message): "{{message}}" will be replaced by an
|
||||
# english string describing the error.
|
||||
error_message=Boodskap: {{message}}
|
||||
# LOCALIZATION NOTE (error_stack): "{{stack}}" will be replaced with a stack
|
||||
# trace.
|
||||
error_stack=Stapel: {{stack}}
|
||||
# LOCALIZATION NOTE (error_file): "{{file}}" will be replaced with a filename
|
||||
error_file=Lêer: {{file}}
|
||||
# LOCALIZATION NOTE (error_line): "{{line}}" will be replaced with a line number
|
||||
error_line=Lyn: {{line}}
|
||||
rendering_error='n Fout het voorgekom toe die bladsy weergegee is.
|
||||
|
||||
# Predefined zoom values
|
||||
page_scale_width=Bladsywydte
|
||||
page_scale_fit=Pas bladsy
|
||||
page_scale_auto=Outomatiese zoem
|
||||
page_scale_actual=Werklike grootte
|
||||
# LOCALIZATION NOTE (page_scale_percent): "{{scale}}" will be replaced by a
|
||||
# numerical scale value.
|
||||
page_scale_percent={{scale}}%
|
||||
|
||||
# Loading indicator messages
|
||||
loading_error_indicator=Fout
|
||||
loading_error='n Fout het voorgekom met die laai van die PDF.
|
||||
invalid_file_error=Ongeldige of korrupte PDF-lêer.
|
||||
missing_file_error=PDF-lêer is weg.
|
||||
unexpected_response_error=Onverwagse antwoord van bediener.
|
||||
|
||||
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
|
||||
# Some common types are e.g.: "Check", "Text", "Comment", "Note"
|
||||
text_annotation_type.alt=[{{type}}-annotasie]
|
||||
password_label=Gee die wagwoord om dié PDF-lêer mee te open.
|
||||
password_invalid=Ongeldige wagwoord. Probeer gerus weer.
|
||||
password_ok=OK
|
||||
password_cancel=Kanselleer
|
||||
|
||||
printing_not_supported=Waarskuwing: Dié blaaier ondersteun nie drukwerk ten volle nie.
|
||||
printing_not_ready=Waarskuwing: Die PDF is nog nie volledig gelaai vir drukwerk nie.
|
||||
web_fonts_disabled=Webfonte is gedeaktiveer: kan nie PDF-fonte wat ingebed is, gebruik nie.
|
||||
document_colors_not_allowed=PDF-dokumente word nie toegelaat om hul eie kleure te gebruik nie: “Laat bladsye toe om hul eie kleure te kies” is gedeaktiveer in die blaaier.
|
||||
@@ -1,184 +0,0 @@
|
||||
# Copyright 2012 Mozilla Foundation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Main toolbar buttons (tooltips and alt text for images)
|
||||
previous.title=Pachina anterior
|
||||
previous_label=Anterior
|
||||
next.title=Pachina siguient
|
||||
next_label=Siguient
|
||||
|
||||
# LOCALIZATION NOTE (page.title): The tooltip for the pageNumber input.
|
||||
page.title=Pachina
|
||||
# LOCALIZATION NOTE (of_pages): "{{pagesCount}}" will be replaced by a number
|
||||
# representing the total number of pages in the document.
|
||||
of_pages=de {{pagesCount}}
|
||||
# LOCALIZATION NOTE (page_of_pages): "{{pageNumber}}" and "{{pagesCount}}"
|
||||
# will be replaced by a number representing the currently visible page,
|
||||
# respectively a number representing the total number of pages in the document.
|
||||
page_of_pages=({{pageNumber}} de {{pagesCount}})
|
||||
|
||||
zoom_out.title=Achiquir
|
||||
zoom_out_label=Achiquir
|
||||
zoom_in.title=Agrandir
|
||||
zoom_in_label=Agrandir
|
||||
zoom.title=Grandaria
|
||||
presentation_mode.title=Cambear t'o modo de presentación
|
||||
presentation_mode_label=Modo de presentación
|
||||
open_file.title=Ubrir o fichero
|
||||
open_file_label=Ubrir
|
||||
print.title=Imprentar
|
||||
print_label=Imprentar
|
||||
download.title=Descargar
|
||||
download_label=Descargar
|
||||
bookmark.title=Vista actual (copiar u ubrir en una nueva finestra)
|
||||
bookmark_label=Anvista actual
|
||||
|
||||
# Secondary toolbar and context menu
|
||||
tools.title=Ferramientas
|
||||
tools_label=Ferramientas
|
||||
first_page.title=Ir ta la primer pachina
|
||||
first_page.label=Ir ta la primer pachina
|
||||
first_page_label=Ir ta la primer pachina
|
||||
last_page.title=Ir ta la zaguer pachina
|
||||
last_page.label=Ir ta la zaguera pachina
|
||||
last_page_label=Ir ta la zaguer pachina
|
||||
page_rotate_cw.title=Chirar enta la dreita
|
||||
page_rotate_cw.label=Chirar enta la dreita
|
||||
page_rotate_cw_label=Chira enta la dreita
|
||||
page_rotate_ccw.title=Chirar enta la zurda
|
||||
page_rotate_ccw.label=Chirar en sentiu antihorario
|
||||
page_rotate_ccw_label=Chirar enta la zurda
|
||||
|
||||
cursor_text_select_tool.title=Activar la ferramienta de selección de texto
|
||||
cursor_text_select_tool_label=Ferramienta de selección de texto
|
||||
cursor_hand_tool.title=Activar la ferramienta man
|
||||
cursor_hand_tool_label=Ferramienta man
|
||||
|
||||
# Document properties dialog box
|
||||
document_properties.title=Propiedatz d'o documento...
|
||||
document_properties_label=Propiedatz d'o documento...
|
||||
document_properties_file_name=Nombre de fichero:
|
||||
document_properties_file_size=Grandaria d'o fichero:
|
||||
# LOCALIZATION NOTE (document_properties_kb): "{{size_kb}}" and "{{size_b}}"
|
||||
# will be replaced by the PDF file size in kilobytes, respectively in bytes.
|
||||
document_properties_kb={{size_kb}} KB ({{size_b}} bytes)
|
||||
# LOCALIZATION NOTE (document_properties_mb): "{{size_mb}}" and "{{size_b}}"
|
||||
# will be replaced by the PDF file size in megabytes, respectively in bytes.
|
||||
document_properties_mb={{size_mb}} MB ({{size_b}} bytes)
|
||||
document_properties_title=Titol:
|
||||
document_properties_author=Autor:
|
||||
document_properties_subject=Afer:
|
||||
document_properties_keywords=Parolas clau:
|
||||
document_properties_creation_date=Calendata de creyación:
|
||||
document_properties_modification_date=Calendata de modificación:
|
||||
# LOCALIZATION NOTE (document_properties_date_string): "{{date}}" and "{{time}}"
|
||||
# will be replaced by the creation/modification date, and time, of the PDF file.
|
||||
document_properties_date_string={{date}}, {{time}}
|
||||
document_properties_creator=Creyador:
|
||||
document_properties_producer=Creyador de PDF:
|
||||
document_properties_version=Versión de PDF:
|
||||
document_properties_page_count=Numero de pachinas:
|
||||
document_properties_close=Zarrar
|
||||
|
||||
print_progress_message=Se ye preparando la documentación pa imprentar…
|
||||
# LOCALIZATION NOTE (print_progress_percent): "{{progress}}" will be replaced by
|
||||
# a numerical per cent value.
|
||||
print_progress_percent={{progress}}%
|
||||
print_progress_close=Cancelar
|
||||
|
||||
# Tooltips and alt text for side panel toolbar buttons
|
||||
# (the _label strings are alt text for the buttons, the .title strings are
|
||||
# tooltips)
|
||||
toggle_sidebar.title=Amostrar u amagar a barra lateral
|
||||
toggle_sidebar_notification.title=Cambiar barra lateral (lo documento contiene esquema/adchuntos)
|
||||
toggle_sidebar_label=Amostrar a barra lateral
|
||||
document_outline.title=Amostrar esquema d'o documento (fer doble clic pa expandir/compactar totz los items)
|
||||
document_outline_label=Esquema d'o documento
|
||||
attachments.title=Amostrar os adchuntos
|
||||
attachments_label=Adchuntos
|
||||
thumbs.title=Amostrar as miniaturas
|
||||
thumbs_label=Miniaturas
|
||||
findbar.title=Trobar en o documento
|
||||
findbar_label=Trobar
|
||||
|
||||
# Thumbnails panel item (tooltip and alt text for images)
|
||||
# LOCALIZATION NOTE (thumb_page_title): "{{page}}" will be replaced by the page
|
||||
# number.
|
||||
thumb_page_title=Pachina {{page}}
|
||||
# LOCALIZATION NOTE (thumb_page_canvas): "{{page}}" will be replaced by the page
|
||||
# number.
|
||||
thumb_page_canvas=Miniatura d'a pachina {{page}}
|
||||
|
||||
# Find panel button title and messages
|
||||
find_input.title=Trobar
|
||||
find_input.placeholder=Trobar en o documento…
|
||||
find_previous.title=Trobar l'anterior coincidencia d'a frase
|
||||
find_previous_label=Anterior
|
||||
find_next.title=Trobar a siguient coincidencia d'a frase
|
||||
find_next_label=Siguient
|
||||
find_highlight=Resaltar-lo tot
|
||||
find_match_case_label=Coincidencia de mayusclas/minusclas
|
||||
find_reached_top=S'ha plegau a l'inicio d'o documento, se contina dende baixo
|
||||
find_reached_bottom=S'ha plegau a la fin d'o documento, se contina dende alto
|
||||
find_not_found=No s'ha trobau a frase
|
||||
|
||||
# Error panel labels
|
||||
error_more_info=Mas información
|
||||
error_less_info=Menos información
|
||||
error_close=Zarrar
|
||||
# LOCALIZATION NOTE (error_version_info): "{{version}}" and "{{build}}" will be
|
||||
# replaced by the PDF.JS version and build ID.
|
||||
error_version_info=PDF.js v{{version}} (build: {{build}})
|
||||
# LOCALIZATION NOTE (error_message): "{{message}}" will be replaced by an
|
||||
# english string describing the error.
|
||||
error_message=Mensache: {{message}}
|
||||
# LOCALIZATION NOTE (error_stack): "{{stack}}" will be replaced with a stack
|
||||
# trace.
|
||||
error_stack=Pila: {{stack}}
|
||||
# LOCALIZATION NOTE (error_file): "{{file}}" will be replaced with a filename
|
||||
error_file=Fichero: {{file}}
|
||||
# LOCALIZATION NOTE (error_line): "{{line}}" will be replaced with a line number
|
||||
error_line=Linia: {{line}}
|
||||
rendering_error=Ha ocurriu una error en renderizar a pachina.
|
||||
|
||||
# Predefined zoom values
|
||||
page_scale_width=Amplaria d'a pachina
|
||||
page_scale_fit=Achuste d'a pachina
|
||||
page_scale_auto=Grandaria automatica
|
||||
page_scale_actual=Grandaria actual
|
||||
# LOCALIZATION NOTE (page_scale_percent): "{{scale}}" will be replaced by a
|
||||
# numerical scale value.
|
||||
page_scale_percent={{scale}}%
|
||||
|
||||
# Loading indicator messages
|
||||
loading_error_indicator=Error
|
||||
loading_error=S'ha produciu una error en cargar o PDF.
|
||||
invalid_file_error=O PDF no ye valido u ye estorbau.
|
||||
missing_file_error=No i ha fichero PDF.
|
||||
unexpected_response_error=Respuesta a lo servicio inasperada.
|
||||
|
||||
# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
|
||||
# Some common types are e.g.: "Check", "Text", "Comment", "Note"
|
||||
text_annotation_type.alt=[Anotación {{type}}]
|
||||
password_label=Introduzca a clau ta ubrir iste fichero PDF.
|
||||
password_invalid=Clau invalida. Torna a intentar-lo.
|
||||
password_ok=Acceptar
|
||||
password_cancel=Cancelar
|
||||
|
||||
printing_not_supported=Pare cuenta: Iste navegador no maneya totalment as impresions.
|
||||
printing_not_ready=Aviso: Encara no se ha cargau completament o PDF ta imprentar-lo.
|
||||
web_fonts_disabled=As fuents web son desactivadas: no se puet incrustar fichers PDF.
|
||||
document_colors_not_allowed=Los documentos PDF no pueden fer servir las suyas propias colors: 'Permitir que as pachinas triguen as suyas propias colors' ye desactivau en o navegador.
|
||||