From 6ff71e5688d8dd2bbd8aafc865425040ffa2eac3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Henrik=20tom=20W=C3=B6rden?= <h.tomwoerden@indiscale.com>
Date: Mon, 4 Mar 2024 13:45:57 +0100
Subject: [PATCH] ENH: xlsx template generator is complete

---
 .../table_json_conversion/table_generator.py  |  30 ++++++++++-------
 .../example_template.xlsx                     | Bin 10059 -> 10190 bytes
 .../test_table_template_generator.py          |  31 ++++++++++--------
 3 files changed, 36 insertions(+), 25 deletions(-)

diff --git a/src/caosadvancedtools/table_json_conversion/table_generator.py b/src/caosadvancedtools/table_json_conversion/table_generator.py
index c69c4984..1e8f9392 100644
--- a/src/caosadvancedtools/table_json_conversion/table_generator.py
+++ b/src/caosadvancedtools/table_json_conversion/table_generator.py
@@ -124,20 +124,21 @@ class TableTemplateGenerator(ABC):
             if cpath[0] not in keys:
                 raise ValueError(f"A foreign key definition is missing for path: \n{path}\n{keys}")
             keys = keys[cpath[0]]
-            if isinstance(keys, tuple):
-                selected_keys, keys = keys
-            elif isinstance(keys, list):
+            if isinstance(keys, list):
                 selected_keys, keys = keys, None
             else:
                 selected_keys, keys = None, keys
             cpath = cpath[1:]
+        if isinstance(keys, dict) and "__this__" in keys:
+            selected_keys = keys["__this__"]
+
         if selected_keys is None:
             raise ValueError(f"A foreign key definition is missing for path:"
                              f"\n{path}\n{foreign_keys}")
         return selected_keys
 
     def _treat_schema_element(self, schema: dict, sheets: dict = None, path: list = None,
-                              foreign_keys: dict = None
+                              foreign_keys: dict = None, level_in_sheet_name: int = 1
                               ) -> dict[str, tuple[str, list]]:
         """ recursively transforms elements from the schema into column definitions """
         if not ("type" in schema or "enum" in schema or "oneOf" in schema or "anyOf" in schema):
@@ -150,10 +151,11 @@ class TableTemplateGenerator(ABC):
         if 'type' in schema and schema['type'] == 'array':
             if 'type' in schema['items'] and schema['items']['type'] == 'object' and len(path) > 1:  # list of references; special treatment
                 # we add a new sheet
-                sheets[".".join(path)] = self._treat_schema_element(schema['items'], sheets, path, foreign_keys)
+                sheets[path[-1]] = self._treat_schema_element(schema['items'], sheets, path,
+                                                              foreign_keys,
+                                                              len(path))
                 for c in self._get_foreign_keys(foreign_keys, path[:-1]):
-                    sheets[".".join(path)].update({".".join(path[:-1]+[c]): (
-                        ColumnType.FOREIGN, f"see sheet '{path[0]}'", path[:-1]+[c])})
+                    sheets[path[-1]].update({c: (ColumnType.FOREIGN, f"see sheet '{path[0]}'", path[:-1]+[c])})
                 # columns are added to the new sheet, thus we do not return columns
                 return {}
             else:
@@ -171,7 +173,8 @@ class TableTemplateGenerator(ABC):
             cols = {}
             for pname in schema["properties"].keys():
                 cols.update(self._treat_schema_element(
-                    schema["properties"][pname], sheets, path+[pname], foreign_keys))
+                    schema["properties"][pname], sheets, path+[pname], foreign_keys,
+                    level_in_sheet_name))
             return cols
         else:
             description = schema['description'] if 'description' in schema else None
@@ -179,17 +182,17 @@ class TableTemplateGenerator(ABC):
             # those are the leaves
             if 'type' not in schema:
                 if 'enum' in schema:
-                    return {".".join(path[1:]): (ctype, description, path)}
+                    return {".".join(path[level_in_sheet_name:]): (ctype, description, path)}
                 if 'anyOf' in schema:
                     for d in schema['anyOf']:
                         # currently the only case where this occurs is date formats
                         assert d['type'] == 'string'
                         assert d['format'] == 'date' or d['format'] == 'date-time'
-                    return {".".join(path[1:]): (ctype, description, path)}
+                    return {".".join(path[level_in_sheet_name:]): (ctype, description, path)}
             elif schema["type"] in ['string', 'number', 'integer', 'boolean']:
                 if 'format' in schema and schema['format'] == 'data-url':
                     return {}  # file; ignore for now
-                return {".".join(path[1:]): (ctype, description, path)}
+                return {".".join(path[level_in_sheet_name:]): (ctype, description, path)}
             else:
                 raise ValueError("Inappropriate JSON schema: The following part should define an"
                                  f" object with properties or a primitive type:\n{schema}\n")
@@ -285,7 +288,10 @@ class XLSXTemplateGenerator(TableTemplateGenerator):
         del wb['Sheet']
 
         # order sheets
-        for index, sn in enumerate(sorted(wb.sheetnames)):
+        # for index, sn in enumerate(sorted(wb.sheetnames)):
+        # wb.move_sheet(sn, index-wb.index(wb[sn]))
+        # reverse sheets
+        for index, sn in enumerate(wb.sheetnames[::-1]):
             wb.move_sheet(sn, index-wb.index(wb[sn]))
 
         return wb
diff --git a/unittests/table_json_conversion/example_template.xlsx b/unittests/table_json_conversion/example_template.xlsx
index e6b24f25ad52b40991708c4fc710d25eaabd741e..feb8e5e2d7150d015bef49c0dd9a4bbb1648f809 100644
GIT binary patch
delta 8126
zcmZ`;1yEc|v&MqU;)?{AV8I;%EUqEAJHcH7K{ogz!4?TlaCi5>LP&6Tmjr@41bJ}Z
ztN-TS`d?R_t*Nu!J#(hJzv<~7P#J!WrK*UCL;#11iVCM?VNuHf&kK+EIDi@<D1k8o
zDd^&B+fw*XxA_Ydyii6%SxBH&lmHw$b;0C?Prgfe?P$kQ`CNq{dB`^s%x2xTG|I$f
zOND)r!DI%8{QC33ODvYpIR%xru|Js-DvEjwmv0KD#og2qW)-g90HW?9T;=tGQ|>7-
zH3`zcC{+7B8*iW@MXH}|XYzLFHv=0v!@p*uj+s8U(lcE^BaFI=oN1t}OMcU^FvBGC
zH7~mx4v%f>Sn&euleCNm>iX6sNB}F;u-^&r-Ha|t!ra_z-I=HJc4&fnnQ6#4p-!VL
zMGFLj?cU{+^4SXhPPSUYw3XzZAbCAvu)Pb!HyFF#^>?4K%3Q!B7NAfH%>eIXUp3Tm
zrUXwwyprC=3}u^fp>ve4&g*EyKC4=|!j%z2i%fTnIbDT+sl09pc(odwXs;)|;1`{k
zp~BjNy?CT#^7Vb+*yqDji6F0x_OT}Kgjr4hGnxCN5#NFrES2#nPINKk8PSNtscR?A
z&psZ9&ns?H@*$3?`l<<nSmVIodml=o27I1%tEFp;p>`t^sa)h%AGG!Byl8u%xBSA5
zRqmA$9o{%2!mzRulk37oBHM0Fg8kmqOL*50h$-8C#;5FJ_l-HFKumQ4+auI?yuqog
zZ$}hV{VKOYrMx4#f}S#WzVp}M;bzknnyMlSs`cqMI}rjL+%H5pI8{ZAKOLhM$rRx)
z#~?<b0*^VSaS-^eXzTdT>SM^`PgjU1dYEPKC~KzKLZ?NWOu9;{Q(WdpE3-ZZh%eY{
zme|-x8}0ct{aD_xBO`B>9f%4fV@2;yL6L3cL;4gVLc@Bz+AA_U7nU6hk%qX2it4N*
zP+U7Lf|3Zb3W18*G*#U}#R=Cw1I*n7Rz<U0V8OS$@`KW$d0f(Fgw0(qn0c6Tr)L|U
zvq%O@tXM{}_FKS%I!wlceAtkbP6gHG)6tL^7CB=Q5?+5Y4R}#U;gNZfp?Kc9_^oCj
z<Aq+2^G#G^Cy@b#e_BR*%JLd1)h9neu~(da&*w@|Hd;BYN;#E3!}~TiA@V~?qCDx5
zEWxw0+!BM<W(l}@q|Rjl9MU<eiF|d;;b`>|;y$H2)P)9caR%NTsP^X=fmX!DwrB1D
zg@qlUB;u80bRI15*>^Y(u+g?HJV*Vi@~7{I?91WeTh=1`fD)Ol_H`OHva)X?fmld1
zfq27!_I^<yt)9s&i1M1vPJAU_Ac?t(3M^lu|IJMiB`)5oWWw3q7QcDF=#0AH%;0eP
z@YwdO0lg)P^5CTw$u50%{fr@gF`sJY1lOt%gaE?;k&-!?f+v^Gl%(T_7jqNrbD0qz
z;a+Qz3lbCYrb@^({-?<_{mwKxG+v87i~yLTcOO01n}h=WfRko~1sXge&G4yO8Zcq7
z;fS~R{MQmCZx%#V3Zso8+?05^H^Q0ITpij>KAMGN1s)|H%j_D&2U;b%$n3I%UVvJe
z!9E{505^mLhG~Y(nFdMObC^hW<(g;ag_f&vFEj9>Jm^MEiFTy*`2HXlTRH=FMo}^!
z?s^xTC3_ZtRpv5ZtyTU4tNQhxfGd;d?O61zJ}ZZYT-2os_JDjqV`VoRqs=EaVg%TG
z+2Y29-bbY!dwCfxhHR*b!6XCk<pj2MLrEXT>SSm272(A8ve>@u#HGv1V^DKo3?!Xa
zd`-CxpBSiL*=YBPdKV#Gw4I$#e)eLU%SltovaF*0ZsjRH{3w_GjuGJS;Dm{v0_2bQ
zfXZPoLk-Y(o<gKn4g>RlL6no24$S$|%fY@~TiIzr0MLF|32^^uV>cIJ$ymHcb1th-
z^)X0b3Bf0q{yPmQoLPQO-v17}__g}7z|Sv&blZayX({|QC!1h@m7@9{13;3>o<yaS
zz5tev-1t81{<`Vb%SIoR!y}cD?`76$VhYZ@-#R1#<8k(lBLr$F?dg2q1JAoi0XC4r
zDF&R;Qy|Ak@&YI2v|Q0|ir5v3HZFeCGD^_ZV}#n>Q(zNG({;HKZKA1IN_2Bd<INLu
zev|6GAn%A|-?YZb)w-+GOD9fAb>i4NqNiI`s*<}vNJ=R8C~R}Bz~1)VKFH$45&?S1
zP8KXiu%az<TLxaKXZj%>fH4#10ScRu6tkH(sSla@xS#P(DOrQ*Wg+O8&6p0yqyt$g
z!vnIOI04N2N`8fHUb|sP7oUY{1)>=)hF0HmYN29Xg{`(*`!z@nUMT)#FO<`87rIJ>
z9wp@gbK-wk9tG@MMfoGLt@1fA-uF`J5w2R0Nrp8F#PjA$rSTRBfMW$jUetc44`Ke5
zY|&#q1XxGi(kP+Grr6N*MPYlrr?Dd+0JSXX!&EQXt}7Qe?UW1{8*MNmQJjX)$sRYE
zb;>wA+j^_9;~i<FQ{fR{Pg%2DT5q~1A41?{>>wZW`D`fmhBV1=mMHKiHt`Z`+l8(n
z`hNJzds^f6Ux%5`AmH~75gm8U^P_4j0#(OqIHT*tXqK4vL7DH?u-f8&P1$~N?J%Vm
z&0C@nVKp*k(#%UpCv;s|e_rrG8~-^h8^oFKHYz?hLN3p9F1Vm?fJ3kes>y?!qEEfy
zM3YRP4oA3?k!&HGrIt>yc7zsF7P;DXg0w>-pGm!&=s4WVo&aY{`R>plhe77#VYhT<
z$3)%m4z>Y{+!&jlq;6ONEf5UJY$)Vf9=6T~3lJx^BKo<WCvFHE1r$XdVLtvHN3BZC
z`Re89(I5hr@JB;Z1P|N-al>FHRbo^0N$J}d@#U0fCn}Fa1Ckz1)UTf`-IsMcqEcT)
z41H|D;oCq13yUFeQpU(q9UO0c91%I1CHL}uze)=@b$N$mhP_vJ@AEc2&o$8|*}iu*
zuWOEP*lwZeB{#xBWsL;q#%5wZ-WZ9F)1ZDP`Z6UQS_@iXRje3b1@DDYJ^!!EzGU-C
z9{V9&@{j%xM~g0ZhzqkEXeX2duz3I<60UiTho|jtaH^F_D%ar5CSm#n;R|h29?OQh
z^730{?=q~94one*GajIZ*Ocbz6()L3hynqo=RRLy!@y*6+$wXjh`_=$o?AkEU13X%
zWiGu69ym8X^YHfpxZkx4bf&gI%=})-jARDD41?8t5<YFt5!72n^&kylYZ~|g)C$KA
zP<j3|xF21f;7g`j@pT$s=@By~aqG4)$GI+$=-%fBo_?+{<^FE%-SB3itn+<$3!?Tz
z7hb~s-G@e#U*2((O#!73i&rX=xxmP`_fc<b%yvdDQyVr<GA`1c%Z$|vAK;%riFXMh
zOBD$YZWsGsn4|;uNKigvN~jPnK6ox;&di=8nc!z09TMKWyFyFG9Di?;1pb1nB|LSf
zf6OL^%yeIh3RL6+o8PO;To_|+Ow1eWJq|^*k6#OxY}1gVMR%cIqI{hzL(4cyv|BGO
z-pHAY@ZkvXimBWK_nCa;_<?SSNU)vRMd!?PU9DXQ=iqT7WoJxbcuU-MXYjzZRw_r7
z@3ks{o=WRdEsc~JH`KY$h$aXb{`lzGc>`{=H$(6mz=cC>DVB~cPxOX8{s(s*iZ+H1
zGlP+ZaK1>ZK9m?YW-I}@ZENfWfBC`IC~4-^b$_}s)S`8DHyWZO7*f^m6P}D~hO@9K
z+OI;Tc1cHfOZA+Ok(TL!1<Z|r=Q{XGoid(0o1?cXIh)tW^j<XX_ieO)NQl`&YE;?n
z+P8kjSRB)oFU}l%Bm1?M#sIuW=<(BQ<{QUa8tLkAfQ*CE4cg3L#__-&W~&9?W3r{!
zHUeQf;wf7E_q~8Rvw*mBWLAUq56LuwEyimAAM)RfwvLS=NNlB&VE5NOYJo9`by&?+
z@Mmwd$_~3Wt4SigLaCq(psxgBpN}SZM83Qfm#DNOeVrh~f)fIuZ(XQZ1jogcmeOP@
zx@YT?5YIaFR9Kh$C{V)UV;6{Y;~gPa403g*jv+QwI!kMVsnV72*(si#j%RVvY9GvX
z^L}_S!2r{)jKmzW0I!MnoqCD0`UA3@#8a1Cg)8<i$Qz@G=&Yipf0{1Xd@+4I8<@0C
zVkcM^72A5KkGgZAyrZ$+HW@$;5(#VdYo<W*9J=z&v4w(knPyAf96s2kOPMB%hs;kG
zd(YK0xJ`)LJ{A5mYWC-Q?$W{jmERpJYD(1~bGkdvkB${%;3*q-y&i4nnyk`28x0}P
z(o1wBY}Z7@_V2?iMdaJeVM2*_+Qk&D8NUdKdGmv`3Lnsg_CbZ^PWV4~$vx2XTb9Y|
zLg&^JpQFzmTEoUHU_X~l@P>G-e#jQ@dGRo5S)6fOCGwPSQ#P?phUbbU>aDF2XuX48
z^v|Oqx~;2cfU91I+t9j<$r;!}4w$&?2wbP+2Fb#uX<e4&7J2XnexPC~d@e)z@$uJ}
zVv_9_=c<hM+DwqQn>i%S_A+mW`Zc+^oMecG`aQT^C!$!hq*a)6Px}(bY|5g9KGz-9
zsmzMzbMVL+Ght$JsiI2;VJ3E4%WMzzKZ;KysR)`ofq5YB&8z{k#=s^1mr;)Drh~h|
zYCvrdki)ppXN}<;m9ukr*A)SA$03e<1F<QzdbeGvayK5xydbI6e!9@QdxC`xpm(tu
z`Av)lR*si@xy0n~7?tf0>)3~#^kp()naXO8-C)-irC(Cz96S&;VC|2y^x&e02I`y1
z<WRwJaFxKXsDsfrgIOJrmi{7@dcY*|&WxS30{|=M-quED+aoVG;Q|y?1dh;()A?O!
zO<O$(ag;|wK2c2xox67Rxc*^G%Oc>%-yhLk($-~<jrKNnDy+^m18ZDrbB-invnB+m
z;fhWf<PJ>xeb*sv)<kGd$H|*;D5EKlM$6d=R^GIjoHe17Y58iia3f%&uC4Vc`OqOD
zj*--jK*eQ{v$Sn%pJM}7>ZCI_d&F&KhY%qt7-QFX6V#E0vlHEz&fvPeCv<m$a<iK0
z`oec<SOyzD1_43Aduy3lHfaGS2Y{JLXF6XrWa4bWJ}5_XMW$%^>tk0dNkW<|)nZ<Q
zFOQdyzc#M2ol(%PmZkH?3<Yxy1J+C&A#Gu&vk%qOZRydZuOt)Y^TK+Z6=*r@RC-xC
zU!fgHgR)zLZKO3~SSXZHlR{9JE$>J2W>hgG;NoJgqpHq|N(?&A?*^8RMN*=Q4z1KD
zzX=o2Qx=?@E{(8lQ69(zt8ZW|ldV`Q1+RB@uATEqu`VQ}@Ap@p#(5CX(!K4wO_k!j
z=h-~hX5veJ=3#J*?>GSFQ@TfdLUv99w~Cr)aBx+?e<3@RllUcA2;kS**}W0=YUvTz
zDVav)sYZwMlo{lmtPawuo=wA&4CKZ8D-RX;`uaOu3ZRM<aFx-+2bqS#Mju~^F;>_2
z8USRzHZ0gK5EZ~<pc%&OQ^uUeATHqHjRAV2)~nmUcHfI!+tr!h%1Z$iF;I(u3vmfM
zAsY~?ZZtf$Br`BN3rin9-CBI*^ElTiP&-rj^U9?IK{T4lVc^RveSuk9X#^4TFAQHa
z-qMkOl>=ST&=d<)iH13?V+hVml!SRHP!HE629E4&UE$$5Gt4S9AS7*m)~qEL>Vb-6
zHD6jORp1mmhL<F~qf!iPv`C@4uag%;c!#dQCa<(<#ApwGEtC2Xt<Qp|IM`VYh-d><
zcU2|j@cMV1$d=4((Y7M?n9ij{rTd;*5a1PsMGUTc@iwq8MfQqQ^Q=+^zRNjVz{6b_
zqV7<Rf(59M5vK?92b)zXbbR}GY0qzYOgxG(U)OfTjy#5qtTK%&<rSn|lbv9F{lT1E
zjy&md$<YFgsX}q_Exz}e1Vq~!Uy%@@$^6!21q3rsV-_^ewkUSx>~#@o_)+2T4E;CZ
zm5<@Vds9|#R8>rx<|WUao7QDVy~K2W>k&RZA{~dGl9t+}C{ZTpx<e_=4r@XiliE8|
ziVz3tDn}Qhp>B3#D}A<OMPiFJW8d=_diBMvm9Sm#pvQ!Gy&I=EB#SNqH+Z|@Hu~^J
zbc8)Gu-o^Gf~U@f-chz!Kp;7*H=0htIo9$z@JOhj21~?(Nr(|%NhZ5=G)rL(m=Bub
zV8r4B)zHPTma0j0119tu=dA)Wzj~r{pnVWIS6cYR9I)&i*kZggW6Xa!Q+_<NQx|%8
zI5q+ny{RW@SiHQP9T2_P6}ixV2r9W-G=Ct}%qa7#VHAwF=32M6a={w9eVd`xB*a&Z
zD;esaU+Q6!xJPBPb%1w^wok~1QRT?EA6KghzYQs6&e_NS6%57d60T>`RFL7G%D~on
zgAvh*xqPgW=1P&?qPlBJH+4|Eaa&AdHamht2h*q{wW%4^-VL*Q6u#MI?%pR+n-RYW
zoz#>oG?!b#OyQR#ssyRz9c1e6IzlIFnRD!{HKac+=|}^o^fYkq-Ur-1roq|>WWaVE
zX^r>Q$YvL2gl69jq(5<K;}#5R!odBD!nK3#GAX5y=+C2dBwVGS&~t>d5tc4ZV-)cA
zPM5XT!@($bi$&^Zp{<3f4+fW|?Qh#~2JNV}JJ(1d+U%37gCMMBy2)(aB8gg6Ucl*?
z5h7=ZtHL2<>{Ty0mmgmK%A8bb!~i=g9cB*&AXF-kb3SH&$#I5;;N_HfVc)y+8yiQ8
z1U?_-v<z1u6Qjm*!HsWEgb6Z(yB|0`W#+5*TwT|qQJW^WiV-mJMWz|Lr2o!y-^gY<
z36C$|LLZzUm1qLd=Ne0n=O}(+l=U&m98tSa+Ld3#;Mk*h`JZ_$iE=R~vVnx=HSYTa
zC^%^Z+lHg-0c~CS_kf6$k!f3%X%xJ$9iC3szBdbM1dRs2(dMFG$$VM{HH?A>)5U9U
zVAl49QHs&1O5+2-XRniaZb@);(_M6^W%Eyi;kI{~6eq`^s<H@YQj59@*~OaHD4H=z
zAM%lYX=yBdKF#`7_L*3VMK$xg;M)4n5{{M7wet(FAQc%_%-Y)6Zzu_QSp_$zJ(XC{
zEDHI+kn`t<Wj;w%H<zMDPG&Grt@KAX(K`=8*Jz#yPK@ml;1pA6K6u4uJ{{wxDS^>|
zNH!Gu*R8z*K5U-q5k@@;{tctU<n-XgL}jOS4nXMPZx&X{iw3mTDgPp@VjT_7IBSVm
ziVF?pi~uFF1>G3Lt(BR&lJCW&j-I!JvPQq_BL^54O4yJY4nuG3_(st)j(MAhR9Cd1
z?45P%s1>2p!sNT6H65&CgoAJLRliCu!Dcgl2&N&kId2QQ+gR=gg?uV7g}Q->0k)2S
zT!&Mqpf53t2!iENhlWa)BmTBi!Z#}$SLXwB?+l5QERWT;(uFySy%c(Y4MhXTzMk55
z`UZsFtdX~H{&1CjxpUS`AmZHJN=&7AZ${8_9fAP6s>y>x*llRT&&?|l`z~BbI@65_
zG5*thrS%dC4{NFG`UsmgsSU7Xd5-pFk<$Dfq~5-g;*#coG*fDE>Z*~I@*7WzvUZ|U
zT&#+hYaD6?^c77NPB~=b{vKZ0f_gC@z8a_I?QbeE$>+gxp+e5xZxQs^2{zgfmFxp^
zKdJ9~61>kjji!0cx8~S5&_4x3)*Fm!1o^<W&BpdpqLeN)VdZ;P*mTn0Mt9DoV9MjX
z(3fPmb|Rl-3WRdrd%I0q{S+~76Fl*_$fSAgq<f9|bRgEysGsM<!@;4U{L7MYSQOwd
zDm@&Sp@%*Z%$K}N;;5esT9xElsd*^6=p4!F!*gLlCl^xqc$gn+dZJzY!S`nQ%Sw){
z%Nz`fM&X*e;Rx*v=6;2+fqNHkcD|t;|2|v+Sv4m~X4jb43TK0B65sQD++!8cf1WBU
zj=}#h-?M-d6%TVY)O^QvZvoj3*4+a~;kj68<;;SS+JcoG?06mJDaqfz;$GLLfl_<+
zD=6jq77DcCVOfTvdoNr1H4m&DCSa3ba>yy;CR1-8^b<`qMpNG<4D5XRgx}J<?doiH
zhCI}U7tCSh?xRfWC#Q6fMfjZCSRqm`^LAhx|7kZuQh8Dj9<O-Aj}_a0R%@Z>6g1!v
zwSmVS0koYG#vO*AGJ~|@6;$8n;NeMAIrc3Vgu<%r8$W|%pgcAK3X7heeYNaXr<E-y
z7ug@JEuwR%q$uR3ov8=NMJto<u}-4$<0neuVsjCJJnR)*1*mpBBD*pH4G2*`qVlMf
zwyf>SX@F!6A*rcdOnJ2YA1jJIRKdwQZ!Y8&yIg=q8>FmTlWL13H@2=)vWmkDiuDtk
zPF*CS=(e%bt1lo&@^%t4+v`D@g6h-V6a)qd(VKbqTxNI;4rF!m>>hWrS03j_lQFv(
zbLA26%r)k;!8X}MO+PUERUIvi;#+&)herrRsf$J33Fv;sO&O8{c357`{00v@L&yx>
z_t2U?1)1u-_~j@ND~5-yt&t?$coZ1a*>p&+ULtHM{n4;F*9qxpA?UOzmQ1)LrWq`&
zaLZvRizcrYKRhf$igojy^djfCk97QnCgWnggXbY-*B-|Nl7W3lxrtoNS{9D3?_bH<
z+8C3n&7*HF{$&kBB((oDRiJ59bYQ!`obxx~m$TH_Yn>OpE^|O(!Ujn#PmIjmI*MbI
z2K8~USfi!G$dvpz3xzzp;fSpg_G%kXi-fm~DuUi8qg}(Gj`>UXa6HPUgKKLZo}Z``
z<#YIV?tOt6c)oaO^ZeG7frh4gy%YqOx(OZ{$QuMW)$YRfp%Um`+-7RvY{ZwS#hioi
z?l^w7mE!Ky71(QUtes#xuR0xK_)dTh5_54VCIl<v(@3ux)ZkgZTT&$2DI1qP&tIiZ
z?dCiA`sJ%P#><kjemSj|d}^=VF5AmmTNDl$TCV!oAs<QuS&bM4FKpzGMGs)hMz?RT
z-DJ$tQSN;RR>~~FPreFnF6;`sdvw;U$1VCt<@B#A#Z#r?iSQpE3nz0`S0`sTc5@Fm
zcPEFZ@@d9!HF`G(LCD%tA?n<M21?v`U8I1p#%1hs1H7A4zjCCX4{hK<bQt9;l?l;1
z$uhtku5B|r3lkh07B(ZB(_Ov*SpLhmRTGNf>@8&0R?is&+Gmh&@R6)kc7lZjw)wNe
zt60f`<}Ycbozya2)OezYyHobo;h{nrQ*Y)%@ULln1+R~HdG8dlEmjV%mYt}59CH1w
z7Kop|Y;79uL-OOYj~}1^|HGi`)IhM-H^(3jK>H;NU<N&1x}*d>UNYI7tazu{)fA53
zM85&b0A8t<&*ibheKU?hl<7U-Wv|4OuAR@1n4hv*y@Dm&fg*y-wBfzi`7tX{A1u%m
z(T9OH-(yf=-pL`ejGF^RU@noQ4lt*~FUdhR)>NTWX5c{?|K;u|N7DrFhyXShyp=eE
zBgshW^GV-uFOPIyX{|gh)7VpIQcO|}GBwM!U{sMP#(}dES#l<3%?N?G3$~c386RrL
zimU)k63YjN4`qB6X0cr0VQs6fEJ=a<Ip0Ml(DD1*=nflzC-jq>k`Q@FQa65bqpX)<
zO-gD%SCTk$yiG`~s^T-ADe$uo5~q%sEZ?ti{el)ycdZ)hQDohuRMVu<ZYH;c#>H6c
z5r0~?E-5|?4lR3*wnicH*BxFT%9LG^-Tb`6r@X_s9(|@6+d&$Vbq}SC_BfDywJYi!
zodHpiRgn8Q0}b5Xy8o%`P8ElmU>Y{^pnzMfU&%g;!5C6k?8!AHqrqCfWtZx3TncG2
z4*A0WWIKaGnjevm4ylX!WIK2S0=U0}c7&GfF_NKsv@Z-FYx+8Ww#INCP5n0sJSy<A
zXMgA9YG>x;WcTE+|I(#{Dw94%`>1W^ah@j^|C=Hap)PdPubyW3zvjYUy#=hlIUYxc
zkIBp93~uh;_Lgq{ML%?ujtqee4Jv_74}Hue|7z1w(~}`^qCdVOgX+=~Q9VuYXQA-%
zM)?PcV?f1;388uPxPLMJdGGuu<224+jJyBG_-AJIPsU-wM@CZCe{aN}2l!()_6PBh
zK}{KGz)$P^^IXIDg9Q$b>^U5qv8$!M8~e+r-+ycW6M}L7Aqb-Q6IYyEE&t0d>O4Xe
z_&*p0_=o&1<sb5<&d&c${7)eGr;+KY|B(J0)&9NwM{flF#~uHv;SNg4NP&$5hXZ%`
M*qZ%D`?R?K0dmV$m;e9(

delta 7942
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

diff --git a/unittests/table_json_conversion/test_table_template_generator.py b/unittests/table_json_conversion/test_table_template_generator.py
index a64ec083..b83682f9 100644
--- a/unittests/table_json_conversion/test_table_template_generator.py
+++ b/unittests/table_json_conversion/test_table_template_generator.py
@@ -112,7 +112,7 @@ def test_generate_sheets_from_schema():
     with pytest.raises(ValueError, match="A foreign key definition is missing.*"):
         generator._generate_sheets_from_schema(schema)
     sdef = generator._generate_sheets_from_schema(schema,
-                                                  foreign_keys={'Training': (['date', 'url'], {})})
+                                                  foreign_keys={'Training': {"__this__": ['date', 'url']}})
     assert "Training" in sdef
     tdef = sdef['Training']
     assert tdef['date'] == (ColumnType.SCALAR, 'date', ["Training", 'date'])
@@ -127,14 +127,13 @@ def test_generate_sheets_from_schema():
     assert tdef['participants'] == (ColumnType.SCALAR, None, ["Training", 'participants'])
     assert tdef['subjects'] == (ColumnType.LIST, None, ["Training", 'subjects'])
     assert tdef['remote'] == (ColumnType.SCALAR, None, ["Training", 'remote'])
-    cdef = sdef['Training.coach']
-    assert cdef['coach.family_name'] == (ColumnType.SCALAR, None, ["Training", 'coach', 'family_name'])
-    assert cdef['coach.given_name'] == (ColumnType.SCALAR, None, ["Training", 'coach', 'given_name'])
-    assert cdef['coach.Organisation'] == (ColumnType.SCALAR, None, ["Training", 'coach',
-                                                                    'Organisation'])
-    print(cdef)
-    assert cdef['Training.date'] == (ColumnType.FOREIGN, "see sheet 'Training'", ["Training", 'date'])
-    assert cdef['Training.url'] == (ColumnType.FOREIGN, "see sheet 'Training'", ["Training", 'url'])
+    cdef = sdef['coach']
+    assert cdef['family_name'] == (ColumnType.SCALAR, None, ["Training", 'coach', 'family_name'])
+    assert cdef['given_name'] == (ColumnType.SCALAR, None, ["Training", 'coach', 'given_name'])
+    assert cdef['Organisation'] == (ColumnType.SCALAR, None, ["Training", 'coach',
+                                                              'Organisation'])
+    assert cdef['date'] == (ColumnType.FOREIGN, "see sheet 'Training'", ["Training", 'date'])
+    assert cdef['url'] == (ColumnType.FOREIGN, "see sheet 'Training'", ["Training", 'url'])
 
 
 def test_get_foreign_keys():
@@ -142,14 +141,14 @@ def test_get_foreign_keys():
     fkd = {"Training": ['a']}
     assert ['a'] == generator._get_foreign_keys(fkd, ['Training'])
 
-    fkd = {"Training": (['a'], {})}
+    fkd = {"Training": {"__this__": ['a']}}
     assert ['a'] == generator._get_foreign_keys(fkd, ['Training'])
 
     fkd = {"Training": {'hallo'}}
     with pytest.raises(ValueError, match=r"A foreign key definition is missing for path:\n\['Training'\]\n{'Training': \{'hallo'\}\}"):
         generator._get_foreign_keys(fkd, ['Training'])
 
-    fkd = {"Training": (['a'], {'b': ['c']})}
+    fkd = {"Training": {"__this__": ['a'], 'b': ['c']}}
     assert ['c'] == generator._get_foreign_keys(fkd, ['Training', 'b'])
 
     with pytest.raises(ValueError, match=r"A foreign key definition is missing for.*"):
@@ -167,7 +166,7 @@ def test_template_generator():
         schema = json.load(sfi)
     path = os.path.join(tempfile.mkdtemp(), 'test.xlsx')
     assert not os.path.exists(path)
-    generator.generate(schema=schema, foreign_keys={'Training': (['date', 'url'], {})}, filepath=path)
+    generator.generate(schema=schema, foreign_keys={'Training': {"__this__": ['date', 'url']}}, filepath=path)
     assert os.path.exists(path)
     generated = load_workbook(path)  # workbook can be read
     example = load_workbook(rfp("example_template.xlsx"))
@@ -190,8 +189,14 @@ def test_template_generator():
         rp = os.path.join(di, fi)
         with open(rp) as sfi:
             schema = json.load(sfi)
+        fk_path = os.path.join(di, "foreign_keys"+fi[len('schema'):])
+        if not os.path.exists(fk_path):
+            print(f"No foreign keys file for:\n{rp}")
+            continue
+        with open(fk_path) as sfi:
+            fk = json.load(sfi)
         generator.generate(schema=schema,
-                           foreign_keys={},
+                           foreign_keys=fk,
                            filepath=path)
         os.system(f'libreoffice {path}')
 
-- 
GitLab