From 4a82556fe237d3b0ba9369d6a9e2820101727fcd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:21:47 +0000 Subject: [PATCH 1/9] Initial plan From 6c4418519c55a26262e782a2ae6a597460f22319 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:42:01 +0000 Subject: [PATCH 2/9] Add ability to update custom Function module with tests Co-authored-by: kizniche <838427+kizniche@users.noreply.github.com> --- CHANGELOG.md | 1 + .../01987af5493a9c6166a4e6fdbd75dcf1 | Bin 0 -> 139 bytes .../078360605cb2a246770861c84ac472c4 | Bin 0 -> 301 bytes .../0bc9bdd24a2f5372502c0f729866255a | Bin 0 -> 301 bytes .../0c78d0a474c75ad4d2ea3c8433799335 | Bin 0 -> 301 bytes .../0fd425d158944cee593da0ee0e3485fb | Bin 0 -> 301 bytes .../1dedf38b3c4916d257ea9991bb480e1e | Bin 0 -> 301 bytes .../2029240f6d1128be89ddc32729463129 | Bin 0 -> 9 bytes .../2c130736753bda617d0a84184d9c24d8 | Bin 0 -> 301 bytes .../2f96d7c40fa3b9af86e1e8b0c0af7dd1 | Bin 0 -> 301 bytes .../313f60d1466b539ec09cec2becb48044 | Bin 0 -> 301 bytes .../31ef187a66c65347f49065dd505202fb | Bin 0 -> 301 bytes .../395d9745022ea6653d970269353b24d9 | Bin 0 -> 301 bytes .../3c7d006e67984c1423d4ffd9056d5fc5 | Bin 0 -> 301 bytes .../3e7f6c3b89441712b85b7d8fe13df046 | Bin 0 -> 301 bytes .../436e4352643df41dd65dae5295abe9c8 | Bin 0 -> 139 bytes .../45e3cff890de36d97f82f4cfd47931da | Bin 0 -> 139 bytes .../49194d42d967f5df394d7d52b038832b | Bin 0 -> 301 bytes .../5015b6c18a39a565717ebe3e64f3d5ab | Bin 0 -> 301 bytes .../55de68385c267787f6cd452ef4ff7ac9 | Bin 0 -> 301 bytes .../58c092e7334be9a69973f77d73d5af27 | Bin 0 -> 139 bytes .../6108b4a561fdc2e3afd7bef7ede6b7a3 | Bin 0 -> 301 bytes .../63955079c5cc9b44aaee27b44ff7702d | Bin 0 -> 301 bytes .../66a85678d992c2b6b26190f5bfeeae1d | Bin 0 -> 301 bytes .../68d56fe0acd5d184718e45caa9aacc32 | Bin 0 -> 90 bytes .../69c5ee10e583678d4740d2a3586f53bd | Bin 0 -> 725 bytes .../703cb8c41371a9138ff1d1bc2e61879e | Bin 0 -> 139 bytes .../73c6f0fa2f2a1f70e5d8e2ea84957a12 | Bin 0 -> 139 bytes .../78b7b71209a6d54d079b0b5dcfcd2932 | Bin 0 -> 139 bytes .../7b1057e3410c8ee11b9d75dedfb1d6b9 | Bin 0 -> 301 bytes .../92f35a16c1a854e8e6c30c538aa9d8e2 | Bin 0 -> 725 bytes .../94f0b37ae9fad6615027b9c1f369a87a | Bin 0 -> 301 bytes .../a35dc2b08df60c4241c9e0f37d17ec34 | Bin 0 -> 90 bytes .../a3d8277755b79928b59c9417200a5f0d | Bin 0 -> 301 bytes .../ba9498e3d098535be0d4c81e7a5cc812 | Bin 0 -> 301 bytes .../bad6067bd57625d3572702effd18f526 | Bin 0 -> 90 bytes .../c0d64073d520f09c42ccba09d543ec7e | Bin 0 -> 139 bytes .../cb6343994c21b06e8cc7717157b4249d | Bin 0 -> 139 bytes .../ccebaa38152e613c0b841950578a8b55 | Bin 0 -> 301 bytes .../e0b875b29b44499f189289fc1332f3ad | Bin 0 -> 301 bytes .../e54f91b8e744508c5afe56cbf8d73fa0 | Bin 0 -> 725 bytes .../e8dcaa5e5a4b5158383a0c155df406ba | Bin 0 -> 90 bytes .../eae245fe40282379771a113f74ff2766 | Bin 0 -> 139 bytes .../fe52e93f3bdacf578e2ab596922a0d66 | Bin 0 -> 301 bytes mycodo/mycodo_flask/forms/forms_settings.py | 6 ++ mycodo/mycodo_flask/routes_settings.py | 6 +- .../templates/settings/function.html | 15 ++- mycodo/mycodo_flask/utils/utils_settings.py | 98 ++++++++++++++++++ 48 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 mycodo/flask_session/01987af5493a9c6166a4e6fdbd75dcf1 create mode 100644 mycodo/flask_session/078360605cb2a246770861c84ac472c4 create mode 100644 mycodo/flask_session/0bc9bdd24a2f5372502c0f729866255a create mode 100644 mycodo/flask_session/0c78d0a474c75ad4d2ea3c8433799335 create mode 100644 mycodo/flask_session/0fd425d158944cee593da0ee0e3485fb create mode 100644 mycodo/flask_session/1dedf38b3c4916d257ea9991bb480e1e create mode 100644 mycodo/flask_session/2029240f6d1128be89ddc32729463129 create mode 100644 mycodo/flask_session/2c130736753bda617d0a84184d9c24d8 create mode 100644 mycodo/flask_session/2f96d7c40fa3b9af86e1e8b0c0af7dd1 create mode 100644 mycodo/flask_session/313f60d1466b539ec09cec2becb48044 create mode 100644 mycodo/flask_session/31ef187a66c65347f49065dd505202fb create mode 100644 mycodo/flask_session/395d9745022ea6653d970269353b24d9 create mode 100644 mycodo/flask_session/3c7d006e67984c1423d4ffd9056d5fc5 create mode 100644 mycodo/flask_session/3e7f6c3b89441712b85b7d8fe13df046 create mode 100644 mycodo/flask_session/436e4352643df41dd65dae5295abe9c8 create mode 100644 mycodo/flask_session/45e3cff890de36d97f82f4cfd47931da create mode 100644 mycodo/flask_session/49194d42d967f5df394d7d52b038832b create mode 100644 mycodo/flask_session/5015b6c18a39a565717ebe3e64f3d5ab create mode 100644 mycodo/flask_session/55de68385c267787f6cd452ef4ff7ac9 create mode 100644 mycodo/flask_session/58c092e7334be9a69973f77d73d5af27 create mode 100644 mycodo/flask_session/6108b4a561fdc2e3afd7bef7ede6b7a3 create mode 100644 mycodo/flask_session/63955079c5cc9b44aaee27b44ff7702d create mode 100644 mycodo/flask_session/66a85678d992c2b6b26190f5bfeeae1d create mode 100644 mycodo/flask_session/68d56fe0acd5d184718e45caa9aacc32 create mode 100644 mycodo/flask_session/69c5ee10e583678d4740d2a3586f53bd create mode 100644 mycodo/flask_session/703cb8c41371a9138ff1d1bc2e61879e create mode 100644 mycodo/flask_session/73c6f0fa2f2a1f70e5d8e2ea84957a12 create mode 100644 mycodo/flask_session/78b7b71209a6d54d079b0b5dcfcd2932 create mode 100644 mycodo/flask_session/7b1057e3410c8ee11b9d75dedfb1d6b9 create mode 100644 mycodo/flask_session/92f35a16c1a854e8e6c30c538aa9d8e2 create mode 100644 mycodo/flask_session/94f0b37ae9fad6615027b9c1f369a87a create mode 100644 mycodo/flask_session/a35dc2b08df60c4241c9e0f37d17ec34 create mode 100644 mycodo/flask_session/a3d8277755b79928b59c9417200a5f0d create mode 100644 mycodo/flask_session/ba9498e3d098535be0d4c81e7a5cc812 create mode 100644 mycodo/flask_session/bad6067bd57625d3572702effd18f526 create mode 100644 mycodo/flask_session/c0d64073d520f09c42ccba09d543ec7e create mode 100644 mycodo/flask_session/cb6343994c21b06e8cc7717157b4249d create mode 100644 mycodo/flask_session/ccebaa38152e613c0b841950578a8b55 create mode 100644 mycodo/flask_session/e0b875b29b44499f189289fc1332f3ad create mode 100644 mycodo/flask_session/e54f91b8e744508c5afe56cbf8d73fa0 create mode 100644 mycodo/flask_session/e8dcaa5e5a4b5158383a0c155df406ba create mode 100644 mycodo/flask_session/eae245fe40282379771a113f74ff2766 create mode 100644 mycodo/flask_session/fe52e93f3bdacf578e2ab596922a0d66 diff --git a/CHANGELOG.md b/CHANGELOG.md index 69a084e6e..60d581d3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ sudo service mycodoflask restart ### Features - Add API endpoints: /log and /dependency ([#1430](https://github.com/kizniche/Mycodo/issues/1430)) + - Add ability to update (overwrite) a custom Function module without deleting it - Add Input: ENS160 ([#1434](https://github.com/kizniche/Mycodo/pull/1434)) - Add Output: Waveshare 8-Channel Raspberry Pi Relay Board B ([#1434](https://github.com/kizniche/Mycodo/pull/1434)) - Add Function: Camera: rpicam ([#1487](https://github.com/kizniche/Mycodo/pull/1487)) diff --git a/mycodo/flask_session/01987af5493a9c6166a4e6fdbd75dcf1 b/mycodo/flask_session/01987af5493a9c6166a4e6fdbd75dcf1 new file mode 100644 index 0000000000000000000000000000000000000000..60939dd03275790a68aefccc4ec0087fc0007fec GIT binary patch literal 139 zcmWf0x|!L)I<v4rj%(gcufv#WIxV)BRlEO(v5fgR?D?;`&8Gv;d8oaub#FIWWqAh4MO)))F-9BDIsgZc$iA{ouDTcsIJXQGAS>-arusT$=qlj7+5>FF2Ic40XH literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/0bc9bdd24a2f5372502c0f729866255a b/mycodo/flask_session/0bc9bdd24a2f5372502c0f729866255a new file mode 100644 index 0000000000000000000000000000000000000000..f96b3e18afe1158467d895b6a07932f0abb727e6 GIT binary patch literal 301 zcmY+8y-ou$5QGyU5K4(eOF@+idDr&sx~1hIEdQ+GL~@7jE+tY*3YX^LcpHv%O!y-^^ylmEhwbK)&$h2S{~^;c3^FY3tM1Y~6~Bz{GPJrkMpG$}nTu0eLn){tuvpT> z&atr8*;s1zu7v&|*&oK&e(-5Lt+tmWwdx%x~+1 zs|!1`!$L(8Ca|n+*Qpc(Xf!z>0$PENq_Z)K&7z&jhPyz&Xx{owOEc2 hqROIFX2ED*66a!s4PfQ!ls>`egU-*UgrP+ literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/0c78d0a474c75ad4d2ea3c8433799335 b/mycodo/flask_session/0c78d0a474c75ad4d2ea3c8433799335 new file mode 100644 index 0000000000000000000000000000000000000000..b73ec47aada5f8786b2e5b93ca51edf8f01a1fd6 GIT binary patch literal 301 zcmY+8yG{c!5JeLq5K4(eOGA?idF=74rR5_mk3EJJ$u8R6lt?KlT$+#LZ!qb&<+-|# z$LF8neEt2f-CXq9_H`FNcs|CF$F+UcU0UW6*6E$cR`-Ao1zQuNB}Y!_5|IrE)Z|1W z9H!S{3~4&`wwF!4|A#4#VI7YA=iG+V!gCmMt6OOGCX|_%xBlSj z!p_diVhbmkK=)x|`d$PjF-IvuY6fH3Xd>2`mB)Vh4YdDeIq03!~l#;@wc{tt%la488G^1~J z&p*TE_WNOOq%e2OEFOZ%$(be>CCrgt7&J(wr}4M}4PMbMO#jLCWel?{5( zSw%;{qSgBnha=~3oL+}9q-pJIFPr-C&!#wrWjOI)vJ2~(=P=|}x6tZcC^MhmI>F7= zyEu=<7EYKzJE@HksUt{KIU)jT24mT1BG#FuS6Mj|tx~-pEU^G(!8@;uLEBe!=ZqvW e3y~co^)H@CG5Tm(ChH{n>@x}toCG`7)~8=K-Cu$L literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/1dedf38b3c4916d257ea9991bb480e1e b/mycodo/flask_session/1dedf38b3c4916d257ea9991bb480e1e new file mode 100644 index 0000000000000000000000000000000000000000..d0a3e9aa16979a2ec64dd33b6c1bf9b2c6f3ce84 GIT binary patch literal 301 zcmY+8Jx&8L5QP&V5K4(eOPvbYN)BOp>@loJcF|_HL`q5F(j0+9aT`oJUiqHB zr|&oL6XKPW35OOr2^e;K!Rauk*%R_lUaG4wX+em?tnmIAvH?xyeSs2zZgd{ glE^GXb_`@Fc_Pi=BeF{96!`2j2qQTOcDk*PzwVDd`3+hbUf?4r$XiIkFJOLH48!fja6@yhS% zdwSnbKi&EI>tVaOAAUkn?fjMbAzM&m4JWXs8m zuT0()(3TO}a#y)KNQsBxrR!rJ=5_6Pv)uo~iTgNpNBL`R<9w1acBL)b)xU{#l+)|_ z;OfH8fuLfQ3|YHiocAgx)&^{001ZyGNAWHtaYYQ-VuNS2b`=yRQQD%WK(2bgb;X6G hIjJH_l^~E@%9N57AVx76g8?d3Ksha0Nw95x`~@$vU|RqH literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/2f96d7c40fa3b9af86e1e8b0c0af7dd1 b/mycodo/flask_session/2f96d7c40fa3b9af86e1e8b0c0af7dd1 new file mode 100644 index 0000000000000000000000000000000000000000..270d0be50a75e2245b7203d5e6ac6d53c4a17af8 GIT binary patch literal 301 zcmY+8y-ou$5QGyU5K4(eOPvaN*Z(yo4`JEfHJnK9(B-Z~N=f0;)I1k&gGtAfGn!A| z_tVdCzW#dHZZ7#~`??G7JRjr86Cp+iqS{QGFvCnlP44!I16^Ft&hKRgkO#T literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/313f60d1466b539ec09cec2becb48044 b/mycodo/flask_session/313f60d1466b539ec09cec2becb48044 new file mode 100644 index 0000000000000000000000000000000000000000..3734f5970dc22fca2a93c7ca9ef6190e69579fc9 GIT binary patch literal 301 zcmY+8y-ou$5QGyU5K4(eOC3?5-97)dv^<36wbyVWxkGoC5-BBxOY?BN4M#er{6_O> z9-n{u^Y!<`c5}&R+t*$Akm(o)8J6}{cWItVSjKl5THS+FXdwfjAu<^|HsUb>2}WhG zWEV3hlveLb><^N|VSMd}kjB$$d)d_cf0*MCmi{Pz&22c%GKD_3x}{ccLYZWKTOV9q z*x7k1wn(Cc+I89(NF1>vl_L?77Bp6jW)_{L^C~N6qE%`gfMo$x7QOSj7_zHHJI2V6 f1t2>i)|@=E67tbXm#pi^XP*f)N&<1Jot}OH7?NLW literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/31ef187a66c65347f49065dd505202fb b/mycodo/flask_session/31ef187a66c65347f49065dd505202fb new file mode 100644 index 0000000000000000000000000000000000000000..ab6335eda3e5658e71f88b6c91914dc7cd22c437 GIT binary patch literal 301 zcmY+9y-ou$5QGyU5K4(eOPvbYYp;J=S{}l(z3XryxkL9;BBi8odAtj+!rS1aW6BxL z=$oIH-|l?-{j}X&^||irE`0EKiaq!9`c-#%8cUdmckb7^H-O#}DW4fADN-b)X7E^a zL`P(@N3Dxo>qCj%ktrO9L)V8qEN$&|Qy>4?DfVIRPJBu3!!q$0I$GR?sCG glOPs?97DpCeM*-v(Xz_cDe&Y81WL|=onDvcKPZD>RsaA1 literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/395d9745022ea6653d970269353b24d9 b/mycodo/flask_session/395d9745022ea6653d970269353b24d9 new file mode 100644 index 0000000000000000000000000000000000000000..24a861582fd1a14b642d3c7b86e7ebc72c4121ce GIT binary patch literal 301 zcmY+9y-ou$5QGyU5K4(eOMMlx*Z(ao4`F$|>u@5uL-$i6rKE6a9*(!+NXL}lXht*L z%TIr~{eIf-uKH|UJ;V>5&S~IbX;O5FM zK0tAWGh}JJGS-TWNh;8k03;Ze*0D8+Y=~9Rq`gf}>n$K0vXI)z;Dad^+}2_|%7lVg e2=WYQFb71<5Rzk+omUWOJ1LChEZFO|KK}w%RA8t8 literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/3c7d006e67984c1423d4ffd9056d5fc5 b/mycodo/flask_session/3c7d006e67984c1423d4ffd9056d5fc5 new file mode 100644 index 0000000000000000000000000000000000000000..1ba5a11da50551f098d44796b0d7b45f42c4faf0 GIT binary patch literal 301 zcmY+9y-ou$5QGzVgj*Ki`aL-$i6rKE6ao`xskZ8*{~Ce|+58KT}A8lWE;a#R<9Auc=XWgaKRKh&I$T-h?vA>2-Z@ zbzx`cRiPqDB{Z%x)15UIRcZfSS-dZ;Dm4wiw44 g8L|LmCyEUx&#XoHNYW*8o$}dd1qLO7INg@VU&`NK?EnA( literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/436e4352643df41dd65dae5295abe9c8 b/mycodo/flask_session/436e4352643df41dd65dae5295abe9c8 new file mode 100644 index 0000000000000000000000000000000000000000..d9f1d46cf5c95f3ac5d69e5d685824ab5260a87f GIT binary patch literal 139 zcmbQ}d^59wb!rU*1k_H^=;4YlNG-}u%uCHHnbOh2m0Vnu7GIK|otihLN5k06(8$ur zG%?LM#oXLD%``bNH7Uu=C?ze?BGn|#%p%#+%skD+GHptaP+DSUPHIYgPJVi3UVL(X ZDbO@;h8|HA;iSa8_>#=rRG>_$9st%JFirpf literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/45e3cff890de36d97f82f4cfd47931da b/mycodo/flask_session/45e3cff890de36d97f82f4cfd47931da new file mode 100644 index 0000000000000000000000000000000000000000..7f7af9b2134b065eb0decd3716fa60956985b85e GIT binary patch literal 139 zcmZQ`x|!L)I<4tVrrCRYL;l0Vw7TPVQOq(m|~Qek~pPDC@nEFCp9HLCqF$iFFrZH Z6lj_^Lystma8hDkd`V_*Dp00W4**m`F7^Nb literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/49194d42d967f5df394d7d52b038832b b/mycodo/flask_session/49194d42d967f5df394d7d52b038832b new file mode 100644 index 0000000000000000000000000000000000000000..0f91d406d614dda89152d7840e1724288718cf11 GIT binary patch literal 301 zcmY+9y-ou$5QGyU5K4(eOPvbYYp;K5N*==U+G{wG+@Z@|iIkGU6*3RU+hEc$<&5;r z%-7S;aK8S2*lsTRX#2Vg?>ryl$m7yJ>n@$<5|-(W$5!{zN=@1p)(M=KjxA=PY|I%{ z0gDPWYTbnPC-X9 glE^GXb_|4+JdtMb(XvX`De&265GXkb?w8ibUr#;hb~Epl#;@wc{tvNBOOzIqxm$C z&p*TY`ukzMy5zHO+a`U;yr)sdWB+QKe3)xGPVX}Iwv9|q$Ux43TrmPvC7N_*sR{sW zaTKD{z1>wBc2d%AdL71;r_*A4S+)Crm}yMMVK0BpZ8{xfPDAN!4ZYo@I?Lg0d2n@M z=Mb^_Dj74_psjN%Gl51^2ocZeYxD87>Uim$} zpYNxi;e7q|uwGsC(YI|A-(}vXQO0BcY@2+TYdlVGGWNC&B-JrPGN43IC-4Md!59Fl zuIA)8Xj^-`t7+ItiM#1#7-ODJOWX6R-T%X!#&{g|^5@*f(?RAql-|~=x0_gJIlL|p zt}g8C11;{ElyNHfK-HpKTFA3X g3`GEn7i8ibn2lgaPIPu&V<@2@kdh4IO+P*U0?T|~^#A|> literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/63955079c5cc9b44aaee27b44ff7702d b/mycodo/flask_session/63955079c5cc9b44aaee27b44ff7702d new file mode 100644 index 0000000000000000000000000000000000000000..9bb0d740de1347236de38211ded73395168bde46 GIT binary patch literal 301 zcmY+8J5B>J7=#lc5K4(eOPvb&_xjn=atO=6J%$y@F52vtNGT~?nzL{gZo`s}DZkM? zzMp>j^Yz!mc5~53+t*!q$8?MXhPi##T{=xA%;Ot|R`;H*g#fuiq>K@4MMBExKyZel zPRbise5-dQ_6Ou}7+?A!q;Xl>o;UUWALclOxj*91xedz+Q|NQ6TWa+tlnJNT^}*GJ zot>A(79^QSyH2m;i6eDV<{BhKln5*Yuodt-v7ff41Vs9^5@+8WwdxoL<)l zR~L4+P8TkcbW-aC83QQ>)oIK@Yo$q)VKEe=P?pYNMr#5it^wdEppccbj*8K|wvde~ hM92b=t>{QGxfoHj3oKn?+i90w)<8-Uh{d)%{sKM|Ut0hG literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/68d56fe0acd5d184718e45caa9aacc32 b/mycodo/flask_session/68d56fe0acd5d184718e45caa9aacc32 new file mode 100644 index 0000000000000000000000000000000000000000..22a8c6ae0622e7faa33c49510c24532c5630d3cd GIT binary patch literal 90 zcmZQ|x|!L)I@Oy20&1sd^l-%&q!#5S=B4J9OzG(1N-i!+i!aH~PR*OrqhX$!mXd0k pmS|$0YHXaEWRa9=W@2HMVvuBzm||j)YHDhcXknacU^%5!4*(_F9N7Q> literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/69c5ee10e583678d4740d2a3586f53bd b/mycodo/flask_session/69c5ee10e583678d4740d2a3586f53bd new file mode 100644 index 0000000000000000000000000000000000000000..e4684e7d5869dea2887b0629c9f727ffba30d6c9 GIT binary patch literal 725 zcmdnwbThMob?PxD1~90dqS3<@Uyxdqo0ylHS2Creha)~MC$TsqwRlP_P>waVs3^Z^ zN{_l{UU6w!T4r)4P?Z8meP(fSW`16=l|or&YI%HdYDr0EUV8DAwke}}M#F%f=^(XK F4*)p0Cqe)K literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/703cb8c41371a9138ff1d1bc2e61879e b/mycodo/flask_session/703cb8c41371a9138ff1d1bc2e61879e new file mode 100644 index 0000000000000000000000000000000000000000..fb96894c289b96267b72b1695f180718a4286d73 GIT binary patch literal 139 zcmWf2x|!L)I<|n3QIjXgQ@vC@nEFCp9HLCqF$iFFrZH Z6lj_^Lystma8hDkd`V_*Dp00W4*;m>Fe?B6 literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/73c6f0fa2f2a1f70e5d8e2ea84957a12 b/mycodo/flask_session/73c6f0fa2f2a1f70e5d8e2ea84957a12 new file mode 100644 index 0000000000000000000000000000000000000000..50cf0d62032d89c4ba6329ef6a179fad2a1eb56f GIT binary patch literal 139 zcmbQ>d^59wb!rU*1k_H^=;4YlNG-}u%uCHHnbOh2m0Vnu7GIK|otihLN5jz4&>}g_ z)Xdx{*&-#;(mXBIz#=ut$iO%;(Im;((9+!8B*`$<)N)FXP+DSUPHIYgPJVi3UVL(X ZDbO@;h8|HA;iSa8_>#=rRG>_$9ssbnFRTCn literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/78b7b71209a6d54d079b0b5dcfcd2932 b/mycodo/flask_session/78b7b71209a6d54d079b0b5dcfcd2932 new file mode 100644 index 0000000000000000000000000000000000000000..52db71b30860b079e6a5248cedea306759b368fb GIT binary patch literal 139 zcmZ4KbThMob!rU*1k_H^=;4YlNG-}u%uCHHnbOh2m0Vnu7GIK|otihLM*=$OWP+DSUPHIYgPJVi3UVL(X ZDbO@;h8|HA;iSa8_>#=rRG>_$9sspoFSGyv literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/7b1057e3410c8ee11b9d75dedfb1d6b9 b/mycodo/flask_session/7b1057e3410c8ee11b9d75dedfb1d6b9 new file mode 100644 index 0000000000000000000000000000000000000000..8656813f1183962d81ad28de58c0ae9484d8b2aa GIT binary patch literal 301 zcmY+9y-ou$5QGzZYm^IdNpF@A*wLHE!wWK67FTHq5+mul~qhUbLbdXxA F2LPB$Ca(Yh literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/94f0b37ae9fad6615027b9c1f369a87a b/mycodo/flask_session/94f0b37ae9fad6615027b9c1f369a87a new file mode 100644 index 0000000000000000000000000000000000000000..ae6fd947c5f47b550c752370eca94ae3dfaf5d4e GIT binary patch literal 301 zcmY+9y-ou$5QGyU5K4(eOPvbY>z}n-S{}mk+G{wG+@Z@|iIkGUrFj${hPT0_W6Ezd zqnYogpW%G{^|0Ms^wIWp7v6b3#*xRReb!w%%_S_;8;`B-0Wv0`Bq)p)y)Z=@R!B}H z7NU3-?2Jpg)w>dh1LtsLFyvOZvelbVWh#b{+~APLQN=%2+EBMX5mN2q3|*w8+*FvB|8w&f3|CTK9lJVj(q3@4P7%u(udT hGLp!Bc6JP8D0w2y;3Kk1=oI+uGYBI&^;f#BkH3tPU$g)K literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/a35dc2b08df60c4241c9e0f37d17ec34 b/mycodo/flask_session/a35dc2b08df60c4241c9e0f37d17ec34 new file mode 100644 index 0000000000000000000000000000000000000000..23126fedab5c5f636432138ad758f88a0e958994 GIT binary patch literal 90 zcmZ4PbThMob*eW51k_H^=;4YlNG-}u%uCHHnbOh2m0Vnu7GIK|otihLMUim$} zPw)Hbr$1kRJ#06Ze6)Stg?E{bagbqdpLLf`Qwj6-B2 zF)C33K**}qyAu0@P;wy*zvE6^wj#8F!we*r{9U~d2b literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/ba9498e3d098535be0d4c81e7a5cc812 b/mycodo/flask_session/ba9498e3d098535be0d4c81e7a5cc812 new file mode 100644 index 0000000000000000000000000000000000000000..f19966f37e689d47dedfcae47d049295c937bb54 GIT binary patch literal 301 zcmY+8y-ou$5QGyU5K4)J=%`bn_4;q!(((|N*WQB@$sM}8lt?KlT$+dDZ8*{~U0co<*%A?ES4+FmyG{vYNv#HByVUvnEzvrMrst!`DTH?f;!ep?@0 zUD%ldcD|F0Sy|H7Imn!pMpF`^LMzI7ah6$Z5gkl1#=rRG>_$9srkmFH-;j literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/cb6343994c21b06e8cc7717157b4249d b/mycodo/flask_session/cb6343994c21b06e8cc7717157b4249d new file mode 100644 index 0000000000000000000000000000000000000000..8055bbbf06e86eec925e41b3f36d117db7134ef6 GIT binary patch literal 139 zcmeBjzM0v;I<rMEToc9ZHXhu7u7 z)rFly#OkXsGT5N4bBZa0MpH5(pcQaloFx)lL`PFhaM>HP43sBPU_8VSZFT4uiwy*s gR1u|05F|?+iHu;(UNrllF_u_RSrAqdOg}yT0{vWIBme*a literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/e0b875b29b44499f189289fc1332f3ad b/mycodo/flask_session/e0b875b29b44499f189289fc1332f3ad new file mode 100644 index 0000000000000000000000000000000000000000..b8715b99a1b99d0f1ef7e5934a567891c1a5735b GIT binary patch literal 301 zcmY+8y-ou$5QGyU5K4(eOPvbY>-Aqv$wOFPdkrU&J9N1#ky28)G%fGN+hEc$JI}{B^0>6mx=W|Igk^f;vDLkUh|x0@uQ{RMERzElv(7A3 zDJamAMh~stl{g$Yhr{$Tj3G^He|z54`+u0?7?$D4f6Z-JPdtYqx4M9reN zUD(-qD7J8dB(+o87?CJS1(+j%1kKV|HkydLjPjV#nP?IFKv-fS(Ms>UE(UCW(T-#! f(Y-}>45TS}B4qHwaVs3^Z^ zN{_l{UU6w!T4r)4P?Z8meP(fSW`16=l|or&YI%HdYDr0EUV8DAwke}}M#F%f=^(XK F4*>UtCo2E| literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/e8dcaa5e5a4b5158383a0c155df406ba b/mycodo/flask_session/e8dcaa5e5a4b5158383a0c155df406ba new file mode 100644 index 0000000000000000000000000000000000000000..89a19116289cc9218c9cafb5911581ea948a1262 GIT binary patch literal 90 zcmbQ>d^59wb*eW51k_H^=;4YlNG-}u%uCHHnbOh2m0Vnu7GIK|otihLN5eSLB-O|= p*~G}iJkcP%9soT99FPD2 literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/eae245fe40282379771a113f74ff2766 b/mycodo/flask_session/eae245fe40282379771a113f74ff2766 new file mode 100644 index 0000000000000000000000000000000000000000..151d66a8aa69b7f2268de3393daebf22e0bf2b63 GIT binary patch literal 139 zcmZ4CbThMob!rU*1k_H^=;4YlNG-}u%uCHHnbOh2m0Vnu7GIK|otihLN5eeP(Adb# z#K<5GNE;bjrlgvurlzG@8k$%d8zvhVn;RM!CYzfZP3aLzOU%qkO^MIRPtVMYPtGp| Yn&!>WBZ?xNl$aM^l9`(dlquB%0Dw3y?EnA( literal 0 HcmV?d00001 diff --git a/mycodo/flask_session/fe52e93f3bdacf578e2ab596922a0d66 b/mycodo/flask_session/fe52e93f3bdacf578e2ab596922a0d66 new file mode 100644 index 0000000000000000000000000000000000000000..3ca66bb9f39234269c7914ac9582779926e3abe7 GIT binary patch literal 301 zcmY+8yG{c!6hspt5K4(eOPvbY*YBE=kFb30F|0^-(IzR8Qc}1yB_GD$u%u(kGn%9M ze)<{C*Iy6o)g>Q&+cxnX^FECjkNvZ4@?oy=IK5%)ZJP@fot0M}T(tnCLa5SGayGH& zY@LCUqPM%6h8;@WO)tY3^K@El&#QL-4|5vhaoFRpxs9g-<~Wqz*0Q&oSZ5qwmj_oD zcJ_g)tH_k4@yb{$GAF4>mqaAVKw1ZDm|+T3&_#QjoYu=gIEIkg$>4*jmfT`7o@HWD eAVl#XHk<>q28HCHvh#{U35A4_$O2yX)8j8%QDBS! literal 0 HcmV?d00001 diff --git a/mycodo/mycodo_flask/forms/forms_settings.py b/mycodo/mycodo_flask/forms/forms_settings.py index 1ad64a619..d2fe767a5 100644 --- a/mycodo/mycodo_flask/forms/forms_settings.py +++ b/mycodo/mycodo_flask/forms/forms_settings.py @@ -182,6 +182,12 @@ class ControllerDel(FlaskForm): delete_controller = SubmitField(TRANSLATIONS['delete']['title']) +class ControllerMod(FlaskForm): + controller_id = StringField(widget=widgets.HiddenInput()) + update_controller_file = FileField() + update_controller = SubmitField(lazy_gettext('Update Controller Module')) + + # # Settings (Action) # diff --git a/mycodo/mycodo_flask/routes_settings.py b/mycodo/mycodo_flask/routes_settings.py index b2cf7e2ac..96ca5f514 100644 --- a/mycodo/mycodo_flask/routes_settings.py +++ b/mycodo/mycodo_flask/routes_settings.py @@ -122,6 +122,7 @@ def settings_function(): form_controller = forms_settings.Controller() form_controller_delete = forms_settings.ControllerDel() + form_controller_update = forms_settings.ControllerMod() # Get list of custom functions excluded_files = ['__init__.py', '__pycache__'] @@ -134,6 +135,8 @@ def settings_function(): utils_settings.settings_function_import(form_controller) elif form_controller_delete.delete_controller.data: utils_settings.settings_function_delete(form_controller_delete) + elif form_controller_update.update_controller.data: + utils_settings.settings_function_update(form_controller_update) return redirect(url_for('routes_settings.settings_function')) @@ -155,7 +158,8 @@ def settings_function(): return render_template('settings/function.html', dict_controllers=dict_controllers, form_controller=form_controller, - form_controller_delete=form_controller_delete) + form_controller_delete=form_controller_delete, + form_controller_update=form_controller_update) @blueprint.route('/settings/action', methods=('GET', 'POST')) diff --git a/mycodo/mycodo_flask/templates/settings/function.html b/mycodo/mycodo_flask/templates/settings/function.html index 5174886c4..73e178251 100644 --- a/mycodo/mycodo_flask/templates/settings/function.html +++ b/mycodo/mycodo_flask/templates/settings/function.html @@ -42,16 +42,25 @@

{{_('Imported Function Modules')}}

{% for each_controller in dict_controllers %} -
+ {{form_controller_delete.csrf_token}} {{form_controller_delete.controller_id(value=each_controller)}} + {{form_controller_update.controller_id(value=each_controller)}} {{each_controller}} {{dict_controllers[each_controller]['function_name']}} -
- {{form_controller_delete.delete_controller(class_='btn btn-primary btn-block', **{'onclick':'return confirm("Are you sure you want to delete this?")'})}} +
+
+ {{form_controller_delete.delete_controller(class_='btn btn-primary btn-block', **{'onclick':'return confirm("Are you sure you want to delete this?")'})}} +
+
+ +
+
+ {{form_controller_update.update_controller(class_='btn btn-primary btn-block', **{'onclick':'return confirm("Are you sure you want to update this? This will overwrite the existing module.")'})}} +
diff --git a/mycodo/mycodo_flask/utils/utils_settings.py b/mycodo/mycodo_flask/utils/utils_settings.py index c744411c5..ec12e6850 100644 --- a/mycodo/mycodo_flask/utils/utils_settings.py +++ b/mycodo/mycodo_flask/utils/utils_settings.py @@ -617,6 +617,104 @@ def settings_function_delete(form): flash_success_errors(error, action, url_for('routes_settings.settings_function')) +def settings_function_update(form): + """ + Receive a function module file, check it for errors, replace the existing module + """ + action = '{action} {controller}'.format( + action=gettext("Update"), + controller=TRANSLATIONS['controller']['title']) + error = [] + + controller_info = None + + try: + install_dir = os.path.abspath(INSTALL_DIRECTORY) + tmp_directory = os.path.join(install_dir, 'mycodo/functions/tmp_functions') + assure_path_exists(tmp_directory) + assure_path_exists(PATH_FUNCTIONS_CUSTOM) + tmp_name = 'tmp_function_testing.py' + full_path_tmp = os.path.join(tmp_directory, tmp_name) + + controller_device_name = form.controller_id.data + + if not form.update_controller_file.data: + error.append('No file present') + elif form.update_controller_file.data.filename == '': + error.append('No file name') + else: + form.update_controller_file.data.save(full_path_tmp) + + try: + controller_info, status = load_module_from_file(full_path_tmp, 'functions') + if not controller_info or not hasattr(controller_info, 'FUNCTION_INFORMATION'): + error.append("Could not load FUNCTION_INFORMATION dictionary from " + "the uploaded controller module") + except Exception: + error.append("Could not load uploaded file as a python module:\n" + "{}".format(traceback.format_exc())) + + if not error: + if 'function_name_unique' not in controller_info.FUNCTION_INFORMATION: + error.append( + "'function_name_unique' not found in " + "FUNCTION_INFORMATION dictionary") + elif controller_info.FUNCTION_INFORMATION['function_name_unique'] == '': + error.append("'function_name_unique' is empty") + elif controller_info.FUNCTION_INFORMATION['function_name_unique'].lower() != controller_device_name.lower(): + error.append( + "'function_name_unique' must match the existing module name '{}', " + "but '{}' was found".format( + controller_device_name, + controller_info.FUNCTION_INFORMATION['function_name_unique'])) + + if 'function_name' not in controller_info.FUNCTION_INFORMATION: + error.append("'function_name' not found in FUNCTION_INFORMATION dictionary") + elif controller_info.FUNCTION_INFORMATION['function_name'] == '': + error.append("'function_name' is empty") + + if 'dependencies_module' in controller_info.FUNCTION_INFORMATION: + if not isinstance(controller_info.FUNCTION_INFORMATION['dependencies_module'], list): + error.append("'dependencies_module' must be a list of tuples") + else: + for each_dep in controller_info.FUNCTION_INFORMATION['dependencies_module']: + if not isinstance(each_dep, tuple): + error.append("'dependencies_module' must be a list of tuples") + elif len(each_dep) != 3: + error.append("'dependencies_module': tuples in list must have 3 items") + elif not each_dep[0] or not each_dep[1] or not each_dep[2]: + error.append( + "'dependencies_module': tuples in list must " + "not be empty") + elif each_dep[0] not in ['internal', 'pip-pypi', 'apt']: + error.append( + "'dependencies_module': first in tuple " + "must be 'internal', 'pip-pypi', " + "or 'apt'") + + if not error: + # Determine filename of existing module to overwrite. + # Module files are always stored lowercase (see settings_function_import), + # so lowercasing controller_device_name reliably targets the existing file. + unique_name = '{}.py'.format(controller_device_name.lower()) + + # Move module from temp directory to function directory, overwriting the existing module + full_path_final = os.path.join(PATH_FUNCTIONS_CUSTOM, unique_name) + os.rename(full_path_tmp, full_path_final) + + # Reload frontend to refresh the controllers + cmd = '{path}/mycodo/scripts/mycodo_wrapper frontend_reload 2>&1'.format( + path=install_dir) + subprocess.Popen(cmd, shell=True) + flash('Frontend reloaded to scan for updated Controller Modules', 'success') + + except Exception as err: + logger.exception("Function Update") + error.append("Exception: {}".format(err)) + + flash_success_errors(error, action, url_for('routes_settings.settings_function')) + + def settings_action_import(form): """ Receive an action module file, check it for errors, add it to Mycodo controller list From 9f7c96ad4e4051245b10f8c45e3790eff8395893 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:42:47 +0000 Subject: [PATCH 3/9] Remove flask_session files and add to .gitignore Co-authored-by: kizniche <838427+kizniche@users.noreply.github.com> --- .gitignore | 1 + .../01987af5493a9c6166a4e6fdbd75dcf1 | Bin 139 -> 0 bytes .../078360605cb2a246770861c84ac472c4 | Bin 301 -> 0 bytes .../0bc9bdd24a2f5372502c0f729866255a | Bin 301 -> 0 bytes .../0c78d0a474c75ad4d2ea3c8433799335 | Bin 301 -> 0 bytes .../0fd425d158944cee593da0ee0e3485fb | Bin 301 -> 0 bytes .../1dedf38b3c4916d257ea9991bb480e1e | Bin 301 -> 0 bytes .../2029240f6d1128be89ddc32729463129 | Bin 9 -> 0 bytes .../2c130736753bda617d0a84184d9c24d8 | Bin 301 -> 0 bytes .../2f96d7c40fa3b9af86e1e8b0c0af7dd1 | Bin 301 -> 0 bytes .../313f60d1466b539ec09cec2becb48044 | Bin 301 -> 0 bytes .../31ef187a66c65347f49065dd505202fb | Bin 301 -> 0 bytes .../395d9745022ea6653d970269353b24d9 | Bin 301 -> 0 bytes .../3c7d006e67984c1423d4ffd9056d5fc5 | Bin 301 -> 0 bytes .../3e7f6c3b89441712b85b7d8fe13df046 | Bin 301 -> 0 bytes .../436e4352643df41dd65dae5295abe9c8 | Bin 139 -> 0 bytes .../45e3cff890de36d97f82f4cfd47931da | Bin 139 -> 0 bytes .../49194d42d967f5df394d7d52b038832b | Bin 301 -> 0 bytes .../5015b6c18a39a565717ebe3e64f3d5ab | Bin 301 -> 0 bytes .../55de68385c267787f6cd452ef4ff7ac9 | Bin 301 -> 0 bytes .../58c092e7334be9a69973f77d73d5af27 | Bin 139 -> 0 bytes .../6108b4a561fdc2e3afd7bef7ede6b7a3 | Bin 301 -> 0 bytes .../63955079c5cc9b44aaee27b44ff7702d | Bin 301 -> 0 bytes .../66a85678d992c2b6b26190f5bfeeae1d | Bin 301 -> 0 bytes .../68d56fe0acd5d184718e45caa9aacc32 | Bin 90 -> 0 bytes .../69c5ee10e583678d4740d2a3586f53bd | Bin 725 -> 0 bytes .../703cb8c41371a9138ff1d1bc2e61879e | Bin 139 -> 0 bytes .../73c6f0fa2f2a1f70e5d8e2ea84957a12 | Bin 139 -> 0 bytes .../78b7b71209a6d54d079b0b5dcfcd2932 | Bin 139 -> 0 bytes .../7b1057e3410c8ee11b9d75dedfb1d6b9 | Bin 301 -> 0 bytes .../92f35a16c1a854e8e6c30c538aa9d8e2 | Bin 725 -> 0 bytes .../94f0b37ae9fad6615027b9c1f369a87a | Bin 301 -> 0 bytes .../a35dc2b08df60c4241c9e0f37d17ec34 | Bin 90 -> 0 bytes .../a3d8277755b79928b59c9417200a5f0d | Bin 301 -> 0 bytes .../ba9498e3d098535be0d4c81e7a5cc812 | Bin 301 -> 0 bytes .../bad6067bd57625d3572702effd18f526 | Bin 90 -> 0 bytes .../c0d64073d520f09c42ccba09d543ec7e | Bin 139 -> 0 bytes .../cb6343994c21b06e8cc7717157b4249d | Bin 139 -> 0 bytes .../ccebaa38152e613c0b841950578a8b55 | Bin 301 -> 0 bytes .../e0b875b29b44499f189289fc1332f3ad | Bin 301 -> 0 bytes .../e54f91b8e744508c5afe56cbf8d73fa0 | Bin 725 -> 0 bytes .../e8dcaa5e5a4b5158383a0c155df406ba | Bin 90 -> 0 bytes .../eae245fe40282379771a113f74ff2766 | Bin 139 -> 0 bytes .../fe52e93f3bdacf578e2ab596922a0d66 | Bin 301 -> 0 bytes 44 files changed, 1 insertion(+) delete mode 100644 mycodo/flask_session/01987af5493a9c6166a4e6fdbd75dcf1 delete mode 100644 mycodo/flask_session/078360605cb2a246770861c84ac472c4 delete mode 100644 mycodo/flask_session/0bc9bdd24a2f5372502c0f729866255a delete mode 100644 mycodo/flask_session/0c78d0a474c75ad4d2ea3c8433799335 delete mode 100644 mycodo/flask_session/0fd425d158944cee593da0ee0e3485fb delete mode 100644 mycodo/flask_session/1dedf38b3c4916d257ea9991bb480e1e delete mode 100644 mycodo/flask_session/2029240f6d1128be89ddc32729463129 delete mode 100644 mycodo/flask_session/2c130736753bda617d0a84184d9c24d8 delete mode 100644 mycodo/flask_session/2f96d7c40fa3b9af86e1e8b0c0af7dd1 delete mode 100644 mycodo/flask_session/313f60d1466b539ec09cec2becb48044 delete mode 100644 mycodo/flask_session/31ef187a66c65347f49065dd505202fb delete mode 100644 mycodo/flask_session/395d9745022ea6653d970269353b24d9 delete mode 100644 mycodo/flask_session/3c7d006e67984c1423d4ffd9056d5fc5 delete mode 100644 mycodo/flask_session/3e7f6c3b89441712b85b7d8fe13df046 delete mode 100644 mycodo/flask_session/436e4352643df41dd65dae5295abe9c8 delete mode 100644 mycodo/flask_session/45e3cff890de36d97f82f4cfd47931da delete mode 100644 mycodo/flask_session/49194d42d967f5df394d7d52b038832b delete mode 100644 mycodo/flask_session/5015b6c18a39a565717ebe3e64f3d5ab delete mode 100644 mycodo/flask_session/55de68385c267787f6cd452ef4ff7ac9 delete mode 100644 mycodo/flask_session/58c092e7334be9a69973f77d73d5af27 delete mode 100644 mycodo/flask_session/6108b4a561fdc2e3afd7bef7ede6b7a3 delete mode 100644 mycodo/flask_session/63955079c5cc9b44aaee27b44ff7702d delete mode 100644 mycodo/flask_session/66a85678d992c2b6b26190f5bfeeae1d delete mode 100644 mycodo/flask_session/68d56fe0acd5d184718e45caa9aacc32 delete mode 100644 mycodo/flask_session/69c5ee10e583678d4740d2a3586f53bd delete mode 100644 mycodo/flask_session/703cb8c41371a9138ff1d1bc2e61879e delete mode 100644 mycodo/flask_session/73c6f0fa2f2a1f70e5d8e2ea84957a12 delete mode 100644 mycodo/flask_session/78b7b71209a6d54d079b0b5dcfcd2932 delete mode 100644 mycodo/flask_session/7b1057e3410c8ee11b9d75dedfb1d6b9 delete mode 100644 mycodo/flask_session/92f35a16c1a854e8e6c30c538aa9d8e2 delete mode 100644 mycodo/flask_session/94f0b37ae9fad6615027b9c1f369a87a delete mode 100644 mycodo/flask_session/a35dc2b08df60c4241c9e0f37d17ec34 delete mode 100644 mycodo/flask_session/a3d8277755b79928b59c9417200a5f0d delete mode 100644 mycodo/flask_session/ba9498e3d098535be0d4c81e7a5cc812 delete mode 100644 mycodo/flask_session/bad6067bd57625d3572702effd18f526 delete mode 100644 mycodo/flask_session/c0d64073d520f09c42ccba09d543ec7e delete mode 100644 mycodo/flask_session/cb6343994c21b06e8cc7717157b4249d delete mode 100644 mycodo/flask_session/ccebaa38152e613c0b841950578a8b55 delete mode 100644 mycodo/flask_session/e0b875b29b44499f189289fc1332f3ad delete mode 100644 mycodo/flask_session/e54f91b8e744508c5afe56cbf8d73fa0 delete mode 100644 mycodo/flask_session/e8dcaa5e5a4b5158383a0c155df406ba delete mode 100644 mycodo/flask_session/eae245fe40282379771a113f74ff2766 delete mode 100644 mycodo/flask_session/fe52e93f3bdacf578e2ab596922a0d66 diff --git a/.gitignore b/.gitignore index 85142b006..be40e600c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ env/ .venv /databases/flask_secret_key /mycodo/config_override.py +/mycodo/flask_session/ *.db *.pem *.bak diff --git a/mycodo/flask_session/01987af5493a9c6166a4e6fdbd75dcf1 b/mycodo/flask_session/01987af5493a9c6166a4e6fdbd75dcf1 deleted file mode 100644 index 60939dd03275790a68aefccc4ec0087fc0007fec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139 zcmWf0x|!L)I<v4rj%(gcufv#WIxV)BRlEO(v5fgR?D?;`&8Gv;d8oaub#FIWWqAh4MO)))F-9BDIsgZc$iA{ouDTcsIJXQGAS>-arusT$=qlj7+5>FF2Ic40XH diff --git a/mycodo/flask_session/0bc9bdd24a2f5372502c0f729866255a b/mycodo/flask_session/0bc9bdd24a2f5372502c0f729866255a deleted file mode 100644 index f96b3e18afe1158467d895b6a07932f0abb727e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 301 zcmY+8y-ou$5QGyU5K4(eOF@+idDr&sx~1hIEdQ+GL~@7jE+tY*3YX^LcpHv%O!y-^^ylmEhwbK)&$h2S{~^;c3^FY3tM1Y~6~Bz{GPJrkMpG$}nTu0eLn){tuvpT> z&atr8*;s1zu7v&|*&oK&e(-5Lt+tmWwdx%x~+1 zs|!1`!$L(8Ca|n+*Qpc(Xf!z>0$PENq_Z)K&7z&jhPyz&Xx{owOEc2 hqROIFX2ED*66a!s4PfQ!ls>`egU-*UgrP+ diff --git a/mycodo/flask_session/0c78d0a474c75ad4d2ea3c8433799335 b/mycodo/flask_session/0c78d0a474c75ad4d2ea3c8433799335 deleted file mode 100644 index b73ec47aada5f8786b2e5b93ca51edf8f01a1fd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 301 zcmY+8yG{c!5JeLq5K4(eOGA?idF=74rR5_mk3EJJ$u8R6lt?KlT$+#LZ!qb&<+-|# z$LF8neEt2f-CXq9_H`FNcs|CF$F+UcU0UW6*6E$cR`-Ao1zQuNB}Y!_5|IrE)Z|1W z9H!S{3~4&`wwF!4|A#4#VI7YA=iG+V!gCmMt6OOGCX|_%xBlSj z!p_diVhbmkK=)x|`d$PjF-IvuY6fH3Xd>2`mB)Vh4YdDeIq03!~l#;@wc{tt%la488G^1~J z&p*TE_WNOOq%e2OEFOZ%$(be>CCrgt7&J(wr}4M}4PMbMO#jLCWel?{5( zSw%;{qSgBnha=~3oL+}9q-pJIFPr-C&!#wrWjOI)vJ2~(=P=|}x6tZcC^MhmI>F7= zyEu=<7EYKzJE@HksUt{KIU)jT24mT1BG#FuS6Mj|tx~-pEU^G(!8@;uLEBe!=ZqvW e3y~co^)H@CG5Tm(ChH{n>@x}toCG`7)~8=K-Cu$L diff --git a/mycodo/flask_session/1dedf38b3c4916d257ea9991bb480e1e b/mycodo/flask_session/1dedf38b3c4916d257ea9991bb480e1e deleted file mode 100644 index d0a3e9aa16979a2ec64dd33b6c1bf9b2c6f3ce84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 301 zcmY+8Jx&8L5QP&V5K4(eOPvbYN)BOp>@loJcF|_HL`q5F(j0+9aT`oJUiqHB zr|&oL6XKPW35OOr2^e;K!Rauk*%R_lUaG4wX+em?tnmIAvH?xyeSs2zZgd{ glE^GXb_`@Fc_Pi=BeF{96!`2j2qQTOcDk*PzwVDd`3+hbUf?4r$XiIkFJOLH48!fja6@yhS% zdwSnbKi&EI>tVaOAAUkn?fjMbAzM&m4JWXs8m zuT0()(3TO}a#y)KNQsBxrR!rJ=5_6Pv)uo~iTgNpNBL`R<9w1acBL)b)xU{#l+)|_ z;OfH8fuLfQ3|YHiocAgx)&^{001ZyGNAWHtaYYQ-VuNS2b`=yRQQD%WK(2bgb;X6G hIjJH_l^~E@%9N57AVx76g8?d3Ksha0Nw95x`~@$vU|RqH diff --git a/mycodo/flask_session/2f96d7c40fa3b9af86e1e8b0c0af7dd1 b/mycodo/flask_session/2f96d7c40fa3b9af86e1e8b0c0af7dd1 deleted file mode 100644 index 270d0be50a75e2245b7203d5e6ac6d53c4a17af8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 301 zcmY+8y-ou$5QGyU5K4(eOPvaN*Z(yo4`JEfHJnK9(B-Z~N=f0;)I1k&gGtAfGn!A| z_tVdCzW#dHZZ7#~`??G7JRjr86Cp+iqS{QGFvCnlP44!I16^Ft&hKRgkO#T diff --git a/mycodo/flask_session/313f60d1466b539ec09cec2becb48044 b/mycodo/flask_session/313f60d1466b539ec09cec2becb48044 deleted file mode 100644 index 3734f5970dc22fca2a93c7ca9ef6190e69579fc9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 301 zcmY+8y-ou$5QGyU5K4(eOC3?5-97)dv^<36wbyVWxkGoC5-BBxOY?BN4M#er{6_O> z9-n{u^Y!<`c5}&R+t*$Akm(o)8J6}{cWItVSjKl5THS+FXdwfjAu<^|HsUb>2}WhG zWEV3hlveLb><^N|VSMd}kjB$$d)d_cf0*MCmi{Pz&22c%GKD_3x}{ccLYZWKTOV9q z*x7k1wn(Cc+I89(NF1>vl_L?77Bp6jW)_{L^C~N6qE%`gfMo$x7QOSj7_zHHJI2V6 f1t2>i)|@=E67tbXm#pi^XP*f)N&<1Jot}OH7?NLW diff --git a/mycodo/flask_session/31ef187a66c65347f49065dd505202fb b/mycodo/flask_session/31ef187a66c65347f49065dd505202fb deleted file mode 100644 index ab6335eda3e5658e71f88b6c91914dc7cd22c437..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 301 zcmY+9y-ou$5QGyU5K4(eOPvbYYp;J=S{}l(z3XryxkL9;BBi8odAtj+!rS1aW6BxL z=$oIH-|l?-{j}X&^||irE`0EKiaq!9`c-#%8cUdmckb7^H-O#}DW4fADN-b)X7E^a zL`P(@N3Dxo>qCj%ktrO9L)V8qEN$&|Qy>4?DfVIRPJBu3!!q$0I$GR?sCG glOPs?97DpCeM*-v(Xz_cDe&Y81WL|=onDvcKPZD>RsaA1 diff --git a/mycodo/flask_session/395d9745022ea6653d970269353b24d9 b/mycodo/flask_session/395d9745022ea6653d970269353b24d9 deleted file mode 100644 index 24a861582fd1a14b642d3c7b86e7ebc72c4121ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 301 zcmY+9y-ou$5QGyU5K4(eOMMlx*Z(ao4`F$|>u@5uL-$i6rKE6a9*(!+NXL}lXht*L z%TIr~{eIf-uKH|UJ;V>5&S~IbX;O5FM zK0tAWGh}JJGS-TWNh;8k03;Ze*0D8+Y=~9Rq`gf}>n$K0vXI)z;Dad^+}2_|%7lVg e2=WYQFb71<5Rzk+omUWOJ1LChEZFO|KK}w%RA8t8 diff --git a/mycodo/flask_session/3c7d006e67984c1423d4ffd9056d5fc5 b/mycodo/flask_session/3c7d006e67984c1423d4ffd9056d5fc5 deleted file mode 100644 index 1ba5a11da50551f098d44796b0d7b45f42c4faf0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 301 zcmY+9y-ou$5QGzVgj*Ki`aL-$i6rKE6ao`xskZ8*{~Ce|+58KT}A8lWE;a#R<9Auc=XWgaKRKh&I$T-h?vA>2-Z@ zbzx`cRiPqDB{Z%x)15UIRcZfSS-dZ;Dm4wiw44 g8L|LmCyEUx&#XoHNYW*8o$}dd1qLO7INg@VU&`NK?EnA( diff --git a/mycodo/flask_session/436e4352643df41dd65dae5295abe9c8 b/mycodo/flask_session/436e4352643df41dd65dae5295abe9c8 deleted file mode 100644 index d9f1d46cf5c95f3ac5d69e5d685824ab5260a87f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139 zcmbQ}d^59wb!rU*1k_H^=;4YlNG-}u%uCHHnbOh2m0Vnu7GIK|otihLN5k06(8$ur zG%?LM#oXLD%``bNH7Uu=C?ze?BGn|#%p%#+%skD+GHptaP+DSUPHIYgPJVi3UVL(X ZDbO@;h8|HA;iSa8_>#=rRG>_$9st%JFirpf diff --git a/mycodo/flask_session/45e3cff890de36d97f82f4cfd47931da b/mycodo/flask_session/45e3cff890de36d97f82f4cfd47931da deleted file mode 100644 index 7f7af9b2134b065eb0decd3716fa60956985b85e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139 zcmZQ`x|!L)I<4tVrrCRYL;l0Vw7TPVQOq(m|~Qek~pPDC@nEFCp9HLCqF$iFFrZH Z6lj_^Lystma8hDkd`V_*Dp00W4**m`F7^Nb diff --git a/mycodo/flask_session/49194d42d967f5df394d7d52b038832b b/mycodo/flask_session/49194d42d967f5df394d7d52b038832b deleted file mode 100644 index 0f91d406d614dda89152d7840e1724288718cf11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 301 zcmY+9y-ou$5QGyU5K4(eOPvbYYp;K5N*==U+G{wG+@Z@|iIkGU6*3RU+hEc$<&5;r z%-7S;aK8S2*lsTRX#2Vg?>ryl$m7yJ>n@$<5|-(W$5!{zN=@1p)(M=KjxA=PY|I%{ z0gDPWYTbnPC-X9 glE^GXb_|4+JdtMb(XvX`De&265GXkb?w8ibUr#;hb~Epl#;@wc{tvNBOOzIqxm$C z&p*TY`ukzMy5zHO+a`U;yr)sdWB+QKe3)xGPVX}Iwv9|q$Ux43TrmPvC7N_*sR{sW zaTKD{z1>wBc2d%AdL71;r_*A4S+)Crm}yMMVK0BpZ8{xfPDAN!4ZYo@I?Lg0d2n@M z=Mb^_Dj74_psjN%Gl51^2ocZeYxD87>Uim$} zpYNxi;e7q|uwGsC(YI|A-(}vXQO0BcY@2+TYdlVGGWNC&B-JrPGN43IC-4Md!59Fl zuIA)8Xj^-`t7+ItiM#1#7-ODJOWX6R-T%X!#&{g|^5@*f(?RAql-|~=x0_gJIlL|p zt}g8C11;{ElyNHfK-HpKTFA3X g3`GEn7i8ibn2lgaPIPu&V<@2@kdh4IO+P*U0?T|~^#A|> diff --git a/mycodo/flask_session/63955079c5cc9b44aaee27b44ff7702d b/mycodo/flask_session/63955079c5cc9b44aaee27b44ff7702d deleted file mode 100644 index 9bb0d740de1347236de38211ded73395168bde46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 301 zcmY+8J5B>J7=#lc5K4(eOPvb&_xjn=atO=6J%$y@F52vtNGT~?nzL{gZo`s}DZkM? zzMp>j^Yz!mc5~53+t*!q$8?MXhPi##T{=xA%;Ot|R`;H*g#fuiq>K@4MMBExKyZel zPRbise5-dQ_6Ou}7+?A!q;Xl>o;UUWALclOxj*91xedz+Q|NQ6TWa+tlnJNT^}*GJ zot>A(79^QSyH2m;i6eDV<{BhKln5*Yuodt-v7ff41Vs9^5@+8WwdxoL<)l zR~L4+P8TkcbW-aC83QQ>)oIK@Yo$q)VKEe=P?pYNMr#5it^wdEppccbj*8K|wvde~ hM92b=t>{QGxfoHj3oKn?+i90w)<8-Uh{d)%{sKM|Ut0hG diff --git a/mycodo/flask_session/68d56fe0acd5d184718e45caa9aacc32 b/mycodo/flask_session/68d56fe0acd5d184718e45caa9aacc32 deleted file mode 100644 index 22a8c6ae0622e7faa33c49510c24532c5630d3cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90 zcmZQ|x|!L)I@Oy20&1sd^l-%&q!#5S=B4J9OzG(1N-i!+i!aH~PR*OrqhX$!mXd0k pmS|$0YHXaEWRa9=W@2HMVvuBzm||j)YHDhcXknacU^%5!4*(_F9N7Q> diff --git a/mycodo/flask_session/69c5ee10e583678d4740d2a3586f53bd b/mycodo/flask_session/69c5ee10e583678d4740d2a3586f53bd deleted file mode 100644 index e4684e7d5869dea2887b0629c9f727ffba30d6c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 725 zcmdnwbThMob?PxD1~90dqS3<@Uyxdqo0ylHS2Creha)~MC$TsqwRlP_P>waVs3^Z^ zN{_l{UU6w!T4r)4P?Z8meP(fSW`16=l|or&YI%HdYDr0EUV8DAwke}}M#F%f=^(XK F4*)p0Cqe)K diff --git a/mycodo/flask_session/703cb8c41371a9138ff1d1bc2e61879e b/mycodo/flask_session/703cb8c41371a9138ff1d1bc2e61879e deleted file mode 100644 index fb96894c289b96267b72b1695f180718a4286d73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139 zcmWf2x|!L)I<|n3QIjXgQ@vC@nEFCp9HLCqF$iFFrZH Z6lj_^Lystma8hDkd`V_*Dp00W4*;m>Fe?B6 diff --git a/mycodo/flask_session/73c6f0fa2f2a1f70e5d8e2ea84957a12 b/mycodo/flask_session/73c6f0fa2f2a1f70e5d8e2ea84957a12 deleted file mode 100644 index 50cf0d62032d89c4ba6329ef6a179fad2a1eb56f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139 zcmbQ>d^59wb!rU*1k_H^=;4YlNG-}u%uCHHnbOh2m0Vnu7GIK|otihLN5jz4&>}g_ z)Xdx{*&-#;(mXBIz#=ut$iO%;(Im;((9+!8B*`$<)N)FXP+DSUPHIYgPJVi3UVL(X ZDbO@;h8|HA;iSa8_>#=rRG>_$9ssbnFRTCn diff --git a/mycodo/flask_session/78b7b71209a6d54d079b0b5dcfcd2932 b/mycodo/flask_session/78b7b71209a6d54d079b0b5dcfcd2932 deleted file mode 100644 index 52db71b30860b079e6a5248cedea306759b368fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139 zcmZ4KbThMob!rU*1k_H^=;4YlNG-}u%uCHHnbOh2m0Vnu7GIK|otihLM*=$OWP+DSUPHIYgPJVi3UVL(X ZDbO@;h8|HA;iSa8_>#=rRG>_$9sspoFSGyv diff --git a/mycodo/flask_session/7b1057e3410c8ee11b9d75dedfb1d6b9 b/mycodo/flask_session/7b1057e3410c8ee11b9d75dedfb1d6b9 deleted file mode 100644 index 8656813f1183962d81ad28de58c0ae9484d8b2aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 301 zcmY+9y-ou$5QGzZYm^IdNpF@A*wLHE!wWK67FTHq5+mul~qhUbLbdXxA F2LPB$Ca(Yh diff --git a/mycodo/flask_session/94f0b37ae9fad6615027b9c1f369a87a b/mycodo/flask_session/94f0b37ae9fad6615027b9c1f369a87a deleted file mode 100644 index ae6fd947c5f47b550c752370eca94ae3dfaf5d4e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 301 zcmY+9y-ou$5QGyU5K4(eOPvbY>z}n-S{}mk+G{wG+@Z@|iIkGUrFj${hPT0_W6Ezd zqnYogpW%G{^|0Ms^wIWp7v6b3#*xRReb!w%%_S_;8;`B-0Wv0`Bq)p)y)Z=@R!B}H z7NU3-?2Jpg)w>dh1LtsLFyvOZvelbVWh#b{+~APLQN=%2+EBMX5mN2q3|*w8+*FvB|8w&f3|CTK9lJVj(q3@4P7%u(udT hGLp!Bc6JP8D0w2y;3Kk1=oI+uGYBI&^;f#BkH3tPU$g)K diff --git a/mycodo/flask_session/a35dc2b08df60c4241c9e0f37d17ec34 b/mycodo/flask_session/a35dc2b08df60c4241c9e0f37d17ec34 deleted file mode 100644 index 23126fedab5c5f636432138ad758f88a0e958994..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90 zcmZ4PbThMob*eW51k_H^=;4YlNG-}u%uCHHnbOh2m0Vnu7GIK|otihLMUim$} zPw)Hbr$1kRJ#06Ze6)Stg?E{bagbqdpLLf`Qwj6-B2 zF)C33K**}qyAu0@P;wy*zvE6^wj#8F!we*r{9U~d2b diff --git a/mycodo/flask_session/ba9498e3d098535be0d4c81e7a5cc812 b/mycodo/flask_session/ba9498e3d098535be0d4c81e7a5cc812 deleted file mode 100644 index f19966f37e689d47dedfcae47d049295c937bb54..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 301 zcmY+8y-ou$5QGyU5K4)J=%`bn_4;q!(((|N*WQB@$sM}8lt?KlT$+dDZ8*{~U0co<*%A?ES4+FmyG{vYNv#HByVUvnEzvrMrst!`DTH?f;!ep?@0 zUD%ldcD|F0Sy|H7Imn!pMpF`^LMzI7ah6$Z5gkl1#=rRG>_$9srkmFH-;j diff --git a/mycodo/flask_session/cb6343994c21b06e8cc7717157b4249d b/mycodo/flask_session/cb6343994c21b06e8cc7717157b4249d deleted file mode 100644 index 8055bbbf06e86eec925e41b3f36d117db7134ef6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139 zcmeBjzM0v;I<rMEToc9ZHXhu7u7 z)rFly#OkXsGT5N4bBZa0MpH5(pcQaloFx)lL`PFhaM>HP43sBPU_8VSZFT4uiwy*s gR1u|05F|?+iHu;(UNrllF_u_RSrAqdOg}yT0{vWIBme*a diff --git a/mycodo/flask_session/e0b875b29b44499f189289fc1332f3ad b/mycodo/flask_session/e0b875b29b44499f189289fc1332f3ad deleted file mode 100644 index b8715b99a1b99d0f1ef7e5934a567891c1a5735b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 301 zcmY+8y-ou$5QGyU5K4(eOPvbY>-Aqv$wOFPdkrU&J9N1#ky28)G%fGN+hEc$JI}{B^0>6mx=W|Igk^f;vDLkUh|x0@uQ{RMERzElv(7A3 zDJamAMh~stl{g$Yhr{$Tj3G^He|z54`+u0?7?$D4f6Z-JPdtYqx4M9reN zUD(-qD7J8dB(+o87?CJS1(+j%1kKV|HkydLjPjV#nP?IFKv-fS(Ms>UE(UCW(T-#! f(Y-}>45TS}B4qHwaVs3^Z^ zN{_l{UU6w!T4r)4P?Z8meP(fSW`16=l|or&YI%HdYDr0EUV8DAwke}}M#F%f=^(XK F4*>UtCo2E| diff --git a/mycodo/flask_session/e8dcaa5e5a4b5158383a0c155df406ba b/mycodo/flask_session/e8dcaa5e5a4b5158383a0c155df406ba deleted file mode 100644 index 89a19116289cc9218c9cafb5911581ea948a1262..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90 zcmbQ>d^59wb*eW51k_H^=;4YlNG-}u%uCHHnbOh2m0Vnu7GIK|otihLN5eSLB-O|= p*~G}iJkcP%9soT99FPD2 diff --git a/mycodo/flask_session/eae245fe40282379771a113f74ff2766 b/mycodo/flask_session/eae245fe40282379771a113f74ff2766 deleted file mode 100644 index 151d66a8aa69b7f2268de3393daebf22e0bf2b63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139 zcmZ4CbThMob!rU*1k_H^=;4YlNG-}u%uCHHnbOh2m0Vnu7GIK|otihLN5eeP(Adb# z#K<5GNE;bjrlgvurlzG@8k$%d8zvhVn;RM!CYzfZP3aLzOU%qkO^MIRPtVMYPtGp| Yn&!>WBZ?xNl$aM^l9`(dlquB%0Dw3y?EnA( diff --git a/mycodo/flask_session/fe52e93f3bdacf578e2ab596922a0d66 b/mycodo/flask_session/fe52e93f3bdacf578e2ab596922a0d66 deleted file mode 100644 index 3ca66bb9f39234269c7914ac9582779926e3abe7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 301 zcmY+8yG{c!6hspt5K4(eOPvbY*YBE=kFb30F|0^-(IzR8Qc}1yB_GD$u%u(kGn%9M ze)<{C*Iy6o)g>Q&+cxnX^FECjkNvZ4@?oy=IK5%)ZJP@fot0M}T(tnCLa5SGayGH& zY@LCUqPM%6h8;@WO)tY3^K@El&#QL-4|5vhaoFRpxs9g-<~Wqz*0Q&oSZ5qwmj_oD zcJ_g)tH_k4@yb{$GAF4>mqaAVKw1ZDm|+T3&_#QjoYu=gIEIkg$>4*jmfT`7o@HWD eAVl#XHk<>q28HCHvh#{U35A4_$O2yX)8j8%QDBS! From e372f4933aa0f22e2aa32961124dbb4e12060428 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 10 Mar 2026 14:51:52 -0400 Subject: [PATCH 4/9] Shorten button text --- mycodo/mycodo_flask/forms/forms_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycodo/mycodo_flask/forms/forms_settings.py b/mycodo/mycodo_flask/forms/forms_settings.py index d2fe767a5..6c0ca30f5 100644 --- a/mycodo/mycodo_flask/forms/forms_settings.py +++ b/mycodo/mycodo_flask/forms/forms_settings.py @@ -185,7 +185,7 @@ class ControllerDel(FlaskForm): class ControllerMod(FlaskForm): controller_id = StringField(widget=widgets.HiddenInput()) update_controller_file = FileField() - update_controller = SubmitField(lazy_gettext('Update Controller Module')) + update_controller = SubmitField(lazy_gettext('Replace')) # From a50ffbaeaeb02c28bb6332acf1589e3655f01a35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:12:33 +0000 Subject: [PATCH 5/9] Fix unique name comparison to read from both file dicts; add daemon restart when activated; add tests Co-authored-by: kizniche <838427+kizniche@users.noreply.github.com> --- mycodo/mycodo_flask/utils/utils_settings.py | 35 +++++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/mycodo/mycodo_flask/utils/utils_settings.py b/mycodo/mycodo_flask/utils/utils_settings.py index ec12e6850..5cea2eca8 100644 --- a/mycodo/mycodo_flask/utils/utils_settings.py +++ b/mycodo/mycodo_flask/utils/utils_settings.py @@ -627,6 +627,7 @@ def settings_function_update(form): error = [] controller_info = None + existing_controller_info = None try: install_dir = os.path.abspath(INSTALL_DIRECTORY) @@ -645,6 +646,7 @@ def settings_function_update(form): else: form.update_controller_file.data.save(full_path_tmp) + # Load and validate the uploaded (new) module try: controller_info, status = load_module_from_file(full_path_tmp, 'functions') if not controller_info or not hasattr(controller_info, 'FUNCTION_INFORMATION'): @@ -654,6 +656,18 @@ def settings_function_update(form): error.append("Could not load uploaded file as a python module:\n" "{}".format(traceback.format_exc())) + # Load the existing (old) module from disk to extract its function_name_unique + existing_file_path = os.path.join( + PATH_FUNCTIONS_CUSTOM, '{}.py'.format(controller_device_name.lower())) + try: + existing_controller_info, status = load_module_from_file(existing_file_path, 'functions') + if not existing_controller_info or not hasattr(existing_controller_info, 'FUNCTION_INFORMATION'): + error.append("Could not load FUNCTION_INFORMATION dictionary from " + "the existing controller module") + except Exception: + error.append("Could not load existing controller module as a python module:\n" + "{}".format(traceback.format_exc())) + if not error: if 'function_name_unique' not in controller_info.FUNCTION_INFORMATION: error.append( @@ -661,11 +675,12 @@ def settings_function_update(form): "FUNCTION_INFORMATION dictionary") elif controller_info.FUNCTION_INFORMATION['function_name_unique'] == '': error.append("'function_name_unique' is empty") - elif controller_info.FUNCTION_INFORMATION['function_name_unique'].lower() != controller_device_name.lower(): + elif (controller_info.FUNCTION_INFORMATION['function_name_unique'].lower() != + existing_controller_info.FUNCTION_INFORMATION['function_name_unique'].lower()): error.append( "'function_name_unique' must match the existing module name '{}', " "but '{}' was found".format( - controller_device_name, + existing_controller_info.FUNCTION_INFORMATION['function_name_unique'], controller_info.FUNCTION_INFORMATION['function_name_unique'])) if 'function_name' not in controller_info.FUNCTION_INFORMATION: @@ -693,10 +708,8 @@ def settings_function_update(form): "or 'apt'") if not error: - # Determine filename of existing module to overwrite. - # Module files are always stored lowercase (see settings_function_import), - # so lowercasing controller_device_name reliably targets the existing file. - unique_name = '{}.py'.format(controller_device_name.lower()) + # Determine filename from the uploaded module's function_name_unique + unique_name = '{}.py'.format(controller_info.FUNCTION_INFORMATION['function_name_unique'].lower()) # Move module from temp directory to function directory, overwriting the existing module full_path_final = os.path.join(PATH_FUNCTIONS_CUSTOM, unique_name) @@ -708,6 +721,16 @@ def settings_function_update(form): subprocess.Popen(cmd, shell=True) flash('Frontend reloaded to scan for updated Controller Modules', 'success') + # Restart the backend if any Controller using this module is currently activated + controller_activated = CustomController.query.filter( + CustomController.device == controller_device_name, + CustomController.is_activated == True).count() + if controller_activated: + cmd = '{path}/mycodo/scripts/mycodo_wrapper daemon_restart 2>&1'.format( + path=install_dir) + subprocess.Popen(cmd, shell=True) + flash('Backend restarted to apply updated Controller Module', 'success') + except Exception as err: logger.exception("Function Update") error.append("Exception: {}".format(err)) From 50aeba5d8dbd06a19d06a2b71470558da4ffd1dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:13:20 +0000 Subject: [PATCH 6/9] Add test_utils_settings.py and fix .gitignore to track mycodo/tests/ Co-authored-by: kizniche <838427+kizniche@users.noreply.github.com> --- .gitignore | 1 + .../test_mycodo_flask/test_utils_settings.py | 283 ++++++++++++++++++ 2 files changed, 284 insertions(+) create mode 100644 mycodo/tests/software_tests/test_mycodo_flask/test_utils_settings.py diff --git a/.gitignore b/.gitignore index be40e600c..a940ae11b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Mycodo files test/ tests/ +!mycodo/tests/ site/ env/ .venv diff --git a/mycodo/tests/software_tests/test_mycodo_flask/test_utils_settings.py b/mycodo/tests/software_tests/test_mycodo_flask/test_utils_settings.py new file mode 100644 index 000000000..6cdcf8b7d --- /dev/null +++ b/mycodo/tests/software_tests/test_mycodo_flask/test_utils_settings.py @@ -0,0 +1,283 @@ +# coding=utf-8 +"""Tests for settings function update utility function.""" +import os +import shutil +import tempfile + +import mock +import pytest +from unittest.mock import MagicMock, patch + + +# A minimal valid custom function module content +VALID_FUNCTION_CONTENT = b"""FUNCTION_INFORMATION = { + 'function_name_unique': 'MY_CUSTOM_FUNCTION', + 'function_name': 'My Custom Function', +} +""" + +VALID_FUNCTION_UNIQUE_NAME = 'MY_CUSTOM_FUNCTION' + + +class MockFileStorage: + """Mock Werkzeug FileStorage for testing file uploads.""" + + def __init__(self, filename, content): + self.filename = filename + self._content = content + + def save(self, dst): + with open(dst, 'wb') as f: + f.write(self._content) + + +def make_update_form(controller_id, file_storage): + """Create a mock ControllerMod form object for testing.""" + form = MagicMock() + form.controller_id.data = controller_id + form.update_controller_file.data = file_storage + return form + + +@pytest.fixture() +def custom_functions_dir(): + """Create a temporary directory to act as PATH_FUNCTIONS_CUSTOM.""" + tmp_dir = tempfile.mkdtemp() + yield tmp_dir + shutil.rmtree(tmp_dir, ignore_errors=True) + + +@pytest.fixture() +def tmp_install_dir(): + """Create a temporary directory to act as INSTALL_DIRECTORY.""" + tmp_dir = tempfile.mkdtemp() + yield tmp_dir + shutil.rmtree(tmp_dir, ignore_errors=True) + + +@pytest.fixture() +def mock_mycodo_user(): + """ + Mock pwd.getpwnam and grp.getgrnam so that looking up the 'mycodo' + user/group resolves to the current process's UID/GID. This lets + os.chown succeed in CI without a real mycodo system account, while + still exercising the full assure_path_exists / set_user_grp code path. + """ + current_uid = os.getuid() + current_gid = os.getgid() + + mock_pw = MagicMock() + mock_pw.pw_uid = current_uid + + mock_gr = MagicMock() + mock_gr.gr_gid = current_gid + + with patch('mycodo.utils.system_pi.pwd.getpwnam', return_value=mock_pw), \ + patch('mycodo.utils.system_pi.grp.getgrnam', return_value=mock_gr): + yield + + +def write_existing_module(custom_functions_dir, content=None): + """Write the existing module file to PATH_FUNCTIONS_CUSTOM so the update function can load it.""" + if content is None: + content = VALID_FUNCTION_CONTENT + path = os.path.join(custom_functions_dir, 'my_custom_function.py') + with open(path, 'wb') as f: + f.write(content) + return path + + +class TestSettingsFunctionUpdate: + """Tests for the settings_function_update utility function.""" + + @mock.patch('subprocess.Popen') + def test_update_valid_module_no_activated_functions( + self, mock_popen, app, custom_functions_dir, tmp_install_dir, + mock_mycodo_user): + """A valid update with no activated functions: only frontend reload, no daemon restart.""" + from mycodo.mycodo_flask.utils.utils_settings import settings_function_update + + # Write the existing module that will be read from disk for comparison + write_existing_module(custom_functions_dir) + + file_storage = MockFileStorage('my_custom_function.py', VALID_FUNCTION_CONTENT) + form = make_update_form(VALID_FUNCTION_UNIQUE_NAME, file_storage) + + with patch('mycodo.mycodo_flask.utils.utils_settings.INSTALL_DIRECTORY', tmp_install_dir), \ + patch('mycodo.mycodo_flask.utils.utils_settings.PATH_FUNCTIONS_CUSTOM', custom_functions_dir), \ + patch('mycodo.mycodo_flask.utils.utils_settings.CustomController') as mock_cc: + mock_cc.query.filter.return_value.count.return_value = 0 + settings_function_update(form) + + # File should be written to the custom functions directory + expected_file = os.path.join(custom_functions_dir, 'my_custom_function.py') + assert os.path.exists(expected_file) + with open(expected_file, 'rb') as f: + assert b'MY_CUSTOM_FUNCTION' in f.read() + + # Only the frontend reload should have been triggered + assert mock_popen.call_count == 1 + assert 'frontend_reload' in mock_popen.call_args[0][0] + + @mock.patch('subprocess.Popen') + def test_update_valid_module_with_activated_function( + self, mock_popen, app, custom_functions_dir, tmp_install_dir, + mock_mycodo_user): + """A valid update with at least one activated function triggers both frontend reload and daemon restart.""" + from mycodo.mycodo_flask.utils.utils_settings import settings_function_update + + write_existing_module(custom_functions_dir) + + file_storage = MockFileStorage('my_custom_function.py', VALID_FUNCTION_CONTENT) + form = make_update_form(VALID_FUNCTION_UNIQUE_NAME, file_storage) + + with patch('mycodo.mycodo_flask.utils.utils_settings.INSTALL_DIRECTORY', tmp_install_dir), \ + patch('mycodo.mycodo_flask.utils.utils_settings.PATH_FUNCTIONS_CUSTOM', custom_functions_dir), \ + patch('mycodo.mycodo_flask.utils.utils_settings.CustomController') as mock_cc: + # One activated CustomController entry exists for this module + mock_cc.query.filter.return_value.count.return_value = 1 + settings_function_update(form) + + # File should exist + assert os.path.exists(os.path.join(custom_functions_dir, 'my_custom_function.py')) + + # Both frontend_reload and daemon_restart should have been triggered + assert mock_popen.call_count == 2 + calls = [c[0][0] for c in mock_popen.call_args_list] + assert any('frontend_reload' in c for c in calls) + assert any('daemon_restart' in c for c in calls) + + @mock.patch('subprocess.Popen') + def test_update_overwrites_existing_module( + self, mock_popen, app, custom_functions_dir, tmp_install_dir, + mock_mycodo_user): + """Updating an existing module overwrites its file content.""" + from mycodo.mycodo_flask.utils.utils_settings import settings_function_update + + # Write a recognisably "old" version of the existing module + write_existing_module(custom_functions_dir, VALID_FUNCTION_CONTENT + b'# old version\n') + + new_content = VALID_FUNCTION_CONTENT + b'# new version\n' + file_storage = MockFileStorage('my_custom_function.py', new_content) + form = make_update_form(VALID_FUNCTION_UNIQUE_NAME, file_storage) + + with patch('mycodo.mycodo_flask.utils.utils_settings.INSTALL_DIRECTORY', tmp_install_dir), \ + patch('mycodo.mycodo_flask.utils.utils_settings.PATH_FUNCTIONS_CUSTOM', custom_functions_dir), \ + patch('mycodo.mycodo_flask.utils.utils_settings.CustomController') as mock_cc: + mock_cc.query.filter.return_value.count.return_value = 0 + settings_function_update(form) + + with open(os.path.join(custom_functions_dir, 'my_custom_function.py'), 'rb') as f: + content = f.read() + assert b'# old version' not in content + assert b'# new version' in content + + @mock.patch('subprocess.Popen') + def test_update_no_file_fails( + self, mock_popen, app, custom_functions_dir, tmp_install_dir, + mock_mycodo_user): + """Submitting with no file should not update or trigger any subprocess.""" + from mycodo.mycodo_flask.utils.utils_settings import settings_function_update + + write_existing_module(custom_functions_dir) + form = make_update_form(VALID_FUNCTION_UNIQUE_NAME, None) + + with patch('mycodo.mycodo_flask.utils.utils_settings.INSTALL_DIRECTORY', tmp_install_dir), \ + patch('mycodo.mycodo_flask.utils.utils_settings.PATH_FUNCTIONS_CUSTOM', custom_functions_dir): + settings_function_update(form) + + mock_popen.assert_not_called() + + @mock.patch('subprocess.Popen') + def test_update_empty_filename_fails( + self, mock_popen, app, custom_functions_dir, tmp_install_dir, + mock_mycodo_user): + """Submitting with an empty filename should not update or trigger any subprocess.""" + from mycodo.mycodo_flask.utils.utils_settings import settings_function_update + + write_existing_module(custom_functions_dir) + file_storage = MockFileStorage('', VALID_FUNCTION_CONTENT) + form = make_update_form(VALID_FUNCTION_UNIQUE_NAME, file_storage) + + with patch('mycodo.mycodo_flask.utils.utils_settings.INSTALL_DIRECTORY', tmp_install_dir), \ + patch('mycodo.mycodo_flask.utils.utils_settings.PATH_FUNCTIONS_CUSTOM', custom_functions_dir): + settings_function_update(form) + + mock_popen.assert_not_called() + + @mock.patch('subprocess.Popen') + def test_update_different_filename_same_unique_name_succeeds( + self, mock_popen, app, custom_functions_dir, tmp_install_dir, + mock_mycodo_user): + """Uploaded file with a different filename but the same function_name_unique in its dict + should succeed — the comparison is purely dict-based, not filename-based.""" + from mycodo.mycodo_flask.utils.utils_settings import settings_function_update + + # Existing module on disk: my_custom_function.py, unique name 'MY_CUSTOM_FUNCTION' + write_existing_module(custom_functions_dir) + + # Uploaded file has a DIFFERENT filename but the SAME function_name_unique in its dict + file_storage = MockFileStorage('renamed_upload.py', VALID_FUNCTION_CONTENT) + form = make_update_form(VALID_FUNCTION_UNIQUE_NAME, file_storage) + + with patch('mycodo.mycodo_flask.utils.utils_settings.INSTALL_DIRECTORY', tmp_install_dir), \ + patch('mycodo.mycodo_flask.utils.utils_settings.PATH_FUNCTIONS_CUSTOM', custom_functions_dir), \ + patch('mycodo.mycodo_flask.utils.utils_settings.CustomController') as mock_cc: + mock_cc.query.filter.return_value.count.return_value = 0 + settings_function_update(form) + + # The existing module file should have been overwritten (keyed by unique name, not upload filename) + expected_file = os.path.join(custom_functions_dir, 'my_custom_function.py') + assert os.path.exists(expected_file) + with open(expected_file, 'rb') as f: + assert b'MY_CUSTOM_FUNCTION' in f.read() + mock_popen.assert_called_once() + + @mock.patch('subprocess.Popen') + def test_update_different_filename_different_unique_name_fails( + self, mock_popen, app, custom_functions_dir, tmp_install_dir, + mock_mycodo_user): + """Uploaded file with a different filename AND a different function_name_unique in its dict + should fail — even though the filename is different, the dict value must match.""" + from mycodo.mycodo_flask.utils.utils_settings import settings_function_update + + # Existing module on disk: my_custom_function.py, unique name 'MY_CUSTOM_FUNCTION' + write_existing_module(custom_functions_dir) + + # Uploaded file has a different filename AND a different function_name_unique in its dict + different_content = b"""FUNCTION_INFORMATION = { + 'function_name_unique': 'DIFFERENT_FUNCTION', + 'function_name': 'Different Function', +} +""" + file_storage = MockFileStorage('renamed_upload.py', different_content) + form = make_update_form(VALID_FUNCTION_UNIQUE_NAME, file_storage) + + with patch('mycodo.mycodo_flask.utils.utils_settings.INSTALL_DIRECTORY', tmp_install_dir), \ + patch('mycodo.mycodo_flask.utils.utils_settings.PATH_FUNCTIONS_CUSTOM', custom_functions_dir): + settings_function_update(form) + + mock_popen.assert_not_called() + + @mock.patch('subprocess.Popen') + def test_update_invalid_python_fails( + self, mock_popen, app, custom_functions_dir, tmp_install_dir, + mock_mycodo_user): + """An uploaded file with invalid Python should fail without touching the module.""" + from mycodo.mycodo_flask.utils.utils_settings import settings_function_update + + original_content = VALID_FUNCTION_CONTENT + b'# original\n' + write_existing_module(custom_functions_dir, original_content) + + invalid_content = b'this is not valid python !@#$%' + file_storage = MockFileStorage('my_custom_function.py', invalid_content) + form = make_update_form(VALID_FUNCTION_UNIQUE_NAME, file_storage) + + with patch('mycodo.mycodo_flask.utils.utils_settings.INSTALL_DIRECTORY', tmp_install_dir), \ + patch('mycodo.mycodo_flask.utils.utils_settings.PATH_FUNCTIONS_CUSTOM', custom_functions_dir): + settings_function_update(form) + + # Existing module should be untouched + with open(os.path.join(custom_functions_dir, 'my_custom_function.py'), 'rb') as f: + assert b'# original' in f.read() + mock_popen.assert_not_called() From d73bb952ae4646dc40422b559faac1b090400823 Mon Sep 17 00:00:00 2001 From: Kyle Gabriel Date: Wed, 11 Mar 2026 10:16:10 -0400 Subject: [PATCH 7/9] Update mycodo/mycodo_flask/utils/utils_settings.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- mycodo/mycodo_flask/utils/utils_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycodo/mycodo_flask/utils/utils_settings.py b/mycodo/mycodo_flask/utils/utils_settings.py index 5cea2eca8..6e513ce15 100644 --- a/mycodo/mycodo_flask/utils/utils_settings.py +++ b/mycodo/mycodo_flask/utils/utils_settings.py @@ -724,7 +724,7 @@ def settings_function_update(form): # Restart the backend if any Controller using this module is currently activated controller_activated = CustomController.query.filter( CustomController.device == controller_device_name, - CustomController.is_activated == True).count() + CustomController.is_activated.is_(True)).count() if controller_activated: cmd = '{path}/mycodo/scripts/mycodo_wrapper daemon_restart 2>&1'.format( path=install_dir) From 01800f10630a3121ce1c5cccbbdee657e5390e30 Mon Sep 17 00:00:00 2001 From: Kyle Gabriel Date: Wed, 11 Mar 2026 10:19:20 -0400 Subject: [PATCH 8/9] Update mycodo/mycodo_flask/utils/utils_settings.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- mycodo/mycodo_flask/utils/utils_settings.py | 43 +++++++++++---------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/mycodo/mycodo_flask/utils/utils_settings.py b/mycodo/mycodo_flask/utils/utils_settings.py index 6e513ce15..f863dba3e 100644 --- a/mycodo/mycodo_flask/utils/utils_settings.py +++ b/mycodo/mycodo_flask/utils/utils_settings.py @@ -646,27 +646,28 @@ def settings_function_update(form): else: form.update_controller_file.data.save(full_path_tmp) - # Load and validate the uploaded (new) module - try: - controller_info, status = load_module_from_file(full_path_tmp, 'functions') - if not controller_info or not hasattr(controller_info, 'FUNCTION_INFORMATION'): - error.append("Could not load FUNCTION_INFORMATION dictionary from " - "the uploaded controller module") - except Exception: - error.append("Could not load uploaded file as a python module:\n" - "{}".format(traceback.format_exc())) - - # Load the existing (old) module from disk to extract its function_name_unique - existing_file_path = os.path.join( - PATH_FUNCTIONS_CUSTOM, '{}.py'.format(controller_device_name.lower())) - try: - existing_controller_info, status = load_module_from_file(existing_file_path, 'functions') - if not existing_controller_info or not hasattr(existing_controller_info, 'FUNCTION_INFORMATION'): - error.append("Could not load FUNCTION_INFORMATION dictionary from " - "the existing controller module") - except Exception: - error.append("Could not load existing controller module as a python module:\n" - "{}".format(traceback.format_exc())) + if not error: + # Load and validate the uploaded (new) module + try: + controller_info, status = load_module_from_file(full_path_tmp, 'functions') + if not controller_info or not hasattr(controller_info, 'FUNCTION_INFORMATION'): + error.append("Could not load FUNCTION_INFORMATION dictionary from " + "the uploaded controller module") + except Exception: + error.append("Could not load uploaded file as a python module:\n" + "{}".format(traceback.format_exc())) + + # Load the existing (old) module from disk to extract its function_name_unique + existing_file_path = os.path.join( + PATH_FUNCTIONS_CUSTOM, '{}.py'.format(controller_device_name.lower())) + try: + existing_controller_info, status = load_module_from_file(existing_file_path, 'functions') + if not existing_controller_info or not hasattr(existing_controller_info, 'FUNCTION_INFORMATION'): + error.append("Could not load FUNCTION_INFORMATION dictionary from " + "the existing controller module") + except Exception: + error.append("Could not load existing controller module as a python module:\n" + "{}".format(traceback.format_exc())) if not error: if 'function_name_unique' not in controller_info.FUNCTION_INFORMATION: From 2c2ee32d2b468bbe19d6d5af3a97e4b9accbba05 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:23:45 +0000 Subject: [PATCH 9/9] Remove duplicate controller_id from ControllerMod; use form_del.controller_id in update handler Co-authored-by: kizniche <838427+kizniche@users.noreply.github.com> --- mycodo/mycodo_flask/forms/forms_settings.py | 1 - mycodo/mycodo_flask/routes_settings.py | 2 +- .../templates/settings/function.html | 1 - mycodo/mycodo_flask/utils/utils_settings.py | 4 +- .../test_mycodo_flask/test_utils_settings.py | 58 ++++++++++--------- 5 files changed, 35 insertions(+), 31 deletions(-) diff --git a/mycodo/mycodo_flask/forms/forms_settings.py b/mycodo/mycodo_flask/forms/forms_settings.py index 6c0ca30f5..af3daaa86 100644 --- a/mycodo/mycodo_flask/forms/forms_settings.py +++ b/mycodo/mycodo_flask/forms/forms_settings.py @@ -183,7 +183,6 @@ class ControllerDel(FlaskForm): class ControllerMod(FlaskForm): - controller_id = StringField(widget=widgets.HiddenInput()) update_controller_file = FileField() update_controller = SubmitField(lazy_gettext('Replace')) diff --git a/mycodo/mycodo_flask/routes_settings.py b/mycodo/mycodo_flask/routes_settings.py index 96ca5f514..8f5abe908 100644 --- a/mycodo/mycodo_flask/routes_settings.py +++ b/mycodo/mycodo_flask/routes_settings.py @@ -136,7 +136,7 @@ def settings_function(): elif form_controller_delete.delete_controller.data: utils_settings.settings_function_delete(form_controller_delete) elif form_controller_update.update_controller.data: - utils_settings.settings_function_update(form_controller_update) + utils_settings.settings_function_update(form_controller_delete, form_controller_update) return redirect(url_for('routes_settings.settings_function')) diff --git a/mycodo/mycodo_flask/templates/settings/function.html b/mycodo/mycodo_flask/templates/settings/function.html index 73e178251..0a7e0e68f 100644 --- a/mycodo/mycodo_flask/templates/settings/function.html +++ b/mycodo/mycodo_flask/templates/settings/function.html @@ -45,7 +45,6 @@

{{_('Imported Function Modules')}}

{{form_controller_delete.csrf_token}} {{form_controller_delete.controller_id(value=each_controller)}} - {{form_controller_update.controller_id(value=each_controller)}} {{each_controller}} diff --git a/mycodo/mycodo_flask/utils/utils_settings.py b/mycodo/mycodo_flask/utils/utils_settings.py index f863dba3e..586b36823 100644 --- a/mycodo/mycodo_flask/utils/utils_settings.py +++ b/mycodo/mycodo_flask/utils/utils_settings.py @@ -617,7 +617,7 @@ def settings_function_delete(form): flash_success_errors(error, action, url_for('routes_settings.settings_function')) -def settings_function_update(form): +def settings_function_update(form_del, form): """ Receive a function module file, check it for errors, replace the existing module """ @@ -637,7 +637,7 @@ def settings_function_update(form): tmp_name = 'tmp_function_testing.py' full_path_tmp = os.path.join(tmp_directory, tmp_name) - controller_device_name = form.controller_id.data + controller_device_name = form_del.controller_id.data if not form.update_controller_file.data: error.append('No file present') diff --git a/mycodo/tests/software_tests/test_mycodo_flask/test_utils_settings.py b/mycodo/tests/software_tests/test_mycodo_flask/test_utils_settings.py index 6cdcf8b7d..eac522647 100644 --- a/mycodo/tests/software_tests/test_mycodo_flask/test_utils_settings.py +++ b/mycodo/tests/software_tests/test_mycodo_flask/test_utils_settings.py @@ -31,10 +31,16 @@ def save(self, dst): f.write(self._content) -def make_update_form(controller_id, file_storage): - """Create a mock ControllerMod form object for testing.""" +def make_del_form(controller_id): + """Create a mock ControllerDel form (supplies controller_id).""" form = MagicMock() form.controller_id.data = controller_id + return form + + +def make_mod_form(file_storage): + """Create a mock ControllerMod form (supplies the replacement file).""" + form = MagicMock() form.update_controller_file.data = file_storage return form @@ -100,14 +106,14 @@ def test_update_valid_module_no_activated_functions( # Write the existing module that will be read from disk for comparison write_existing_module(custom_functions_dir) - file_storage = MockFileStorage('my_custom_function.py', VALID_FUNCTION_CONTENT) - form = make_update_form(VALID_FUNCTION_UNIQUE_NAME, file_storage) + form_del = make_del_form(VALID_FUNCTION_UNIQUE_NAME) + form = make_mod_form(MockFileStorage('my_custom_function.py', VALID_FUNCTION_CONTENT)) with patch('mycodo.mycodo_flask.utils.utils_settings.INSTALL_DIRECTORY', tmp_install_dir), \ patch('mycodo.mycodo_flask.utils.utils_settings.PATH_FUNCTIONS_CUSTOM', custom_functions_dir), \ patch('mycodo.mycodo_flask.utils.utils_settings.CustomController') as mock_cc: mock_cc.query.filter.return_value.count.return_value = 0 - settings_function_update(form) + settings_function_update(form_del, form) # File should be written to the custom functions directory expected_file = os.path.join(custom_functions_dir, 'my_custom_function.py') @@ -128,15 +134,15 @@ def test_update_valid_module_with_activated_function( write_existing_module(custom_functions_dir) - file_storage = MockFileStorage('my_custom_function.py', VALID_FUNCTION_CONTENT) - form = make_update_form(VALID_FUNCTION_UNIQUE_NAME, file_storage) + form_del = make_del_form(VALID_FUNCTION_UNIQUE_NAME) + form = make_mod_form(MockFileStorage('my_custom_function.py', VALID_FUNCTION_CONTENT)) with patch('mycodo.mycodo_flask.utils.utils_settings.INSTALL_DIRECTORY', tmp_install_dir), \ patch('mycodo.mycodo_flask.utils.utils_settings.PATH_FUNCTIONS_CUSTOM', custom_functions_dir), \ patch('mycodo.mycodo_flask.utils.utils_settings.CustomController') as mock_cc: # One activated CustomController entry exists for this module mock_cc.query.filter.return_value.count.return_value = 1 - settings_function_update(form) + settings_function_update(form_del, form) # File should exist assert os.path.exists(os.path.join(custom_functions_dir, 'my_custom_function.py')) @@ -158,14 +164,14 @@ def test_update_overwrites_existing_module( write_existing_module(custom_functions_dir, VALID_FUNCTION_CONTENT + b'# old version\n') new_content = VALID_FUNCTION_CONTENT + b'# new version\n' - file_storage = MockFileStorage('my_custom_function.py', new_content) - form = make_update_form(VALID_FUNCTION_UNIQUE_NAME, file_storage) + form_del = make_del_form(VALID_FUNCTION_UNIQUE_NAME) + form = make_mod_form(MockFileStorage('my_custom_function.py', new_content)) with patch('mycodo.mycodo_flask.utils.utils_settings.INSTALL_DIRECTORY', tmp_install_dir), \ patch('mycodo.mycodo_flask.utils.utils_settings.PATH_FUNCTIONS_CUSTOM', custom_functions_dir), \ patch('mycodo.mycodo_flask.utils.utils_settings.CustomController') as mock_cc: mock_cc.query.filter.return_value.count.return_value = 0 - settings_function_update(form) + settings_function_update(form_del, form) with open(os.path.join(custom_functions_dir, 'my_custom_function.py'), 'rb') as f: content = f.read() @@ -180,11 +186,12 @@ def test_update_no_file_fails( from mycodo.mycodo_flask.utils.utils_settings import settings_function_update write_existing_module(custom_functions_dir) - form = make_update_form(VALID_FUNCTION_UNIQUE_NAME, None) + form_del = make_del_form(VALID_FUNCTION_UNIQUE_NAME) + form = make_mod_form(None) with patch('mycodo.mycodo_flask.utils.utils_settings.INSTALL_DIRECTORY', tmp_install_dir), \ patch('mycodo.mycodo_flask.utils.utils_settings.PATH_FUNCTIONS_CUSTOM', custom_functions_dir): - settings_function_update(form) + settings_function_update(form_del, form) mock_popen.assert_not_called() @@ -196,12 +203,12 @@ def test_update_empty_filename_fails( from mycodo.mycodo_flask.utils.utils_settings import settings_function_update write_existing_module(custom_functions_dir) - file_storage = MockFileStorage('', VALID_FUNCTION_CONTENT) - form = make_update_form(VALID_FUNCTION_UNIQUE_NAME, file_storage) + form_del = make_del_form(VALID_FUNCTION_UNIQUE_NAME) + form = make_mod_form(MockFileStorage('', VALID_FUNCTION_CONTENT)) with patch('mycodo.mycodo_flask.utils.utils_settings.INSTALL_DIRECTORY', tmp_install_dir), \ patch('mycodo.mycodo_flask.utils.utils_settings.PATH_FUNCTIONS_CUSTOM', custom_functions_dir): - settings_function_update(form) + settings_function_update(form_del, form) mock_popen.assert_not_called() @@ -217,14 +224,14 @@ def test_update_different_filename_same_unique_name_succeeds( write_existing_module(custom_functions_dir) # Uploaded file has a DIFFERENT filename but the SAME function_name_unique in its dict - file_storage = MockFileStorage('renamed_upload.py', VALID_FUNCTION_CONTENT) - form = make_update_form(VALID_FUNCTION_UNIQUE_NAME, file_storage) + form_del = make_del_form(VALID_FUNCTION_UNIQUE_NAME) + form = make_mod_form(MockFileStorage('renamed_upload.py', VALID_FUNCTION_CONTENT)) with patch('mycodo.mycodo_flask.utils.utils_settings.INSTALL_DIRECTORY', tmp_install_dir), \ patch('mycodo.mycodo_flask.utils.utils_settings.PATH_FUNCTIONS_CUSTOM', custom_functions_dir), \ patch('mycodo.mycodo_flask.utils.utils_settings.CustomController') as mock_cc: mock_cc.query.filter.return_value.count.return_value = 0 - settings_function_update(form) + settings_function_update(form_del, form) # The existing module file should have been overwritten (keyed by unique name, not upload filename) expected_file = os.path.join(custom_functions_dir, 'my_custom_function.py') @@ -250,12 +257,12 @@ def test_update_different_filename_different_unique_name_fails( 'function_name': 'Different Function', } """ - file_storage = MockFileStorage('renamed_upload.py', different_content) - form = make_update_form(VALID_FUNCTION_UNIQUE_NAME, file_storage) + form_del = make_del_form(VALID_FUNCTION_UNIQUE_NAME) + form = make_mod_form(MockFileStorage('renamed_upload.py', different_content)) with patch('mycodo.mycodo_flask.utils.utils_settings.INSTALL_DIRECTORY', tmp_install_dir), \ patch('mycodo.mycodo_flask.utils.utils_settings.PATH_FUNCTIONS_CUSTOM', custom_functions_dir): - settings_function_update(form) + settings_function_update(form_del, form) mock_popen.assert_not_called() @@ -269,13 +276,12 @@ def test_update_invalid_python_fails( original_content = VALID_FUNCTION_CONTENT + b'# original\n' write_existing_module(custom_functions_dir, original_content) - invalid_content = b'this is not valid python !@#$%' - file_storage = MockFileStorage('my_custom_function.py', invalid_content) - form = make_update_form(VALID_FUNCTION_UNIQUE_NAME, file_storage) + form_del = make_del_form(VALID_FUNCTION_UNIQUE_NAME) + form = make_mod_form(MockFileStorage('my_custom_function.py', b'this is not valid python !@#$%')) with patch('mycodo.mycodo_flask.utils.utils_settings.INSTALL_DIRECTORY', tmp_install_dir), \ patch('mycodo.mycodo_flask.utils.utils_settings.PATH_FUNCTIONS_CUSTOM', custom_functions_dir): - settings_function_update(form) + settings_function_update(form_del, form) # Existing module should be untouched with open(os.path.join(custom_functions_dir, 'my_custom_function.py'), 'rb') as f: