From 9335de6fd4ec07c3cebba7676004f15630b9b9b3 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Mon, 29 Dec 2025 13:48:22 +0700 Subject: [PATCH] first commit --- README.rst | 42 +++++ __init__.py | 1 + __manifest__.py | 25 +++ __pycache__/__init__.cpython-312.pyc | Bin 0 -> 207 bytes models/__init__.py | 3 + models/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 310 bytes .../survey_question.cpython-312.pyc | Bin 0 -> 2704 bytes .../survey_user_input.cpython-312.pyc | Bin 0 -> 6491 bytes .../survey_user_input_line.cpython-312.pyc | Bin 0 -> 2500 bytes models/survey_question.py | 86 +++++++++ models/survey_user_input.py | 166 ++++++++++++++++++ models/survey_user_input_line.py | 61 +++++++ static/src/js/SurveyFormWidget.js | 13 ++ static/src/js/survey_form_attachment.js | 134 ++++++++++++++ views/survey_question_views.xml | 22 +++ views/survey_templates.xml | 69 ++++++++ views/survey_user_views.xml | 49 ++++++ 17 files changed, 671 insertions(+) create mode 100755 README.rst create mode 100755 __init__.py create mode 100755 __manifest__.py create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100755 models/__init__.py create mode 100644 models/__pycache__/__init__.cpython-312.pyc create mode 100644 models/__pycache__/survey_question.cpython-312.pyc create mode 100644 models/__pycache__/survey_user_input.cpython-312.pyc create mode 100644 models/__pycache__/survey_user_input_line.cpython-312.pyc create mode 100755 models/survey_question.py create mode 100755 models/survey_user_input.py create mode 100755 models/survey_user_input_line.py create mode 100755 static/src/js/SurveyFormWidget.js create mode 100755 static/src/js/survey_form_attachment.js create mode 100755 views/survey_question_views.xml create mode 100755 views/survey_templates.xml create mode 100755 views/survey_user_views.xml diff --git a/README.rst b/README.rst new file mode 100755 index 0000000..263e45c --- /dev/null +++ b/README.rst @@ -0,0 +1,42 @@ +.. image:: https://img.shields.io/badge/license-LGPL--3-green.svg + :target: https://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 + +File Upload In Survey +===================== +This module is used for attachment of files in Survey Form + +Company +------- +* `Cybrosys Techno Solutions `__ + +License +------- +General Public License v3.0 (LGPL v3) +(https://www.gnu.org/licenses/lgpl-3.0-standalone.html) + +Credits +------- +Developer: (V17) Mohammed Dilshad Tk@ Cybrosys, Contact: odoo@cybrosys.com + +Contacts +-------- +* Mail Contact : odoo@cybrosys.com +* Website : https://cybrosys.com + +Bug Tracker +----------- +Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. + +Maintainer +========== +.. image:: https://cybrosys.com/images/logo.png + :target: https://cybrosys.com + +This module is maintained by Cybrosys Technologies. + +For support and more information, please visit `Our Website `__ + +Further information +=================== +HTML Description: ``__ diff --git a/__init__.py b/__init__.py new file mode 100755 index 0000000..0650744 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/__manifest__.py b/__manifest__.py new file mode 100755 index 0000000..2752e69 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,25 @@ +{ + 'name': "Image File Upload In Survey", + 'version': "18.0.1.0.3", + 'category': 'Extra Tools', + 'summary': 'Attachment of Image Files in Survey Form', + 'description': 'This module is used for attachments of image files in Survey Form, ' + 'You can also add multiple image file attachment to Survey Form .', + 'author': 'Suherdy Yacob', + 'depends': ['survey'], + 'assets': { + 'survey.survey_assets': [ + 'survey_upload_image/static/src/js/survey_form_attachment.js', + 'survey_upload_image/static/src/js/SurveyFormWidget.js', + ], + }, + 'data': [ + 'views/survey_question_views.xml', + 'views/survey_user_views.xml', + 'views/survey_templates.xml', + ], + 'license': 'LGPL-3', + 'installable': True, + 'auto_install': False, + 'application': False, +} diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca75b88b2784ffe765fb2c0d2520a6db4eae95e2 GIT binary patch literal 207 zcmX@j%ge<81Xf%@nW8}YF^B^LOi;#W0U%>KLkdF*V-7KB)0q!y)A>IYPoWaQ^10~PD%r{w1w zTIeU27MJAbCZ?q1=M@8$6_uq{#+Mf4OV literal 0 HcmV?d00001 diff --git a/models/__init__.py b/models/__init__.py new file mode 100755 index 0000000..74ee499 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,3 @@ +from . import survey_question +from . import survey_user_input_line +from . import survey_user_input diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0777634a9ad48e76261ad27fc01a75e5023692e6 GIT binary patch literal 310 zcmZXQze)r#5XL9FdvF|_Vq+&Z7A}Ip&O&?y5$(1K%O<#iB(vs^kSFmOd=}r};B2hy zgzK(ya@-&7JFbcA zu4a&%cJ{+4-m=wkNd8S_@tRbK8XZ}udn*Oyg?3Yl!f2dXnQHq1&ZzVNRz={3T^ur#Vv(R;DWZ$ lQHbj)Z|8SpCZq0s`I7R8a76N3Gsf0%wuXx@IQMh1{s8ExRxkhn literal 0 HcmV?d00001 diff --git a/models/__pycache__/survey_question.cpython-312.pyc b/models/__pycache__/survey_question.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c9c3c3ea58e83ddbca54f1167380eb4dd597512 GIT binary patch literal 2704 zcmZuzZA=`;8J^kQ`#v~g4##|%b@<8=bHuIfB#v!`7)&f1688WrSDfwTW)2SacIob3 zjyv61{$Z)9l&Vx}WoQ%`wSOGhQeCN1h#DzTl}eTR=P;JIOj`-5{nI}c4pgz+KYeHK z7D&rVH}lTSGw=J%%=>YFuB-DS`1%{Zm;NM#(BJvM`CK(&`#cD@5kUm2D1+lTBg6%a z1T=z(a0?NU3bSr!(_Vj1tjcjIFGXDc8k)hsoww5}DV{W5&xGig&!stMDS>4HTyV#?MGm3vJa9Q|Pp z-7DK?lPz6N5F%?dC#SMnk}FE6X)AB*O6i82N~=`P&Z~NYm{EJS5c(foaAScgLgDB_ zV8VvW7x|zkMpc@$Y!X&}p2~Vkc1UpRNrNUV8U-D7ri}y!3Q%Iw3{~d~;hL_|tZJp_ zfr_p4^$RmJIR`r7tncazayljF^{kvrXkZ9Vq^f#O&YIMK!|OHna>h}?Nt*nbP0L7Y z(}oY8AX#$+`!zh!^6~a6ywxx8ZB#_F*k1WobFVhSe7*Ln`vC;cJ&%y_FIaWFX5X^0 zNdzLQVhOy4NUD@^l_bz4&erV%)kLe_nY&lxQF0Zr0YBohZN!KhtQ_D=AEJ*0tI^qg zFvihIJkw&yU33qj>k))Xo}x4xc6RRdv(N6yj#TtGI!fN6*Tf$jD|&(1O9+A7TlAJN zMjDn7@s@nRJ7)8SRb$M)gJ8+;LssjqHf$YoWG4!YLq-2=yED7j7xAv;fF=}iDX>pZ zz&h$6cl9(`$DFxhz}ZWLxglWVDF%qp49kWxA``two>p$ z@PljBK8JH?L3|tKaHOu#JPEN(AiVY>E}@w@YD5k0;s*EGu?S`!gW4fk5Wat%d1|4| z(8ck>p~=B3mxcz%hvlmi!?E#8Z;r`@BWL7c!+_KM7xpBEu;-<0=R~cMh-JS)gYHpMb{{+bFd0e6RaVrYY?AGCZWLzbR*9^sy;mp z_Lxs6x*naeta*dyS>~Ij))iQz2J=VM3ZP5fm^t&`e}ZCIX5{v!<>we;Rn3 zRzgcs=CU&zlhm|nF;S%&^9?T~={(Pb%tiS@naj-1gT(6xd*;2`ohlCBNIjDvb)}fQ zdJ)W%)H4~XSqAswDn*aN$pgWWOIMOSgE0|?OazrIkT#X-eoBlMZYfY5DscP+ z!<6|H#m?)BV)Asrll|^@^J#owM$gazGdn{Kk{@_0Z_Vgh66)6gzpWE54<-*;QzrI}Qi7ZWi)&AU;53pN7)X}vV_^Ppa zDRo!+rS_L_$CeOm>iK0&&u5I zPCjaV`Loy;t?TDT%WqwMcGEsgv`EZiJrlre|{pd(%rA@vwvOH4m9xt~}l*1F1 z#-{f#{WTQ+`8zA)YoW8NlV4n04~>?+qfd51sIB{JBsBF`n&ms~%kAaJtE)qwk9<0^ z`bv3ds(k&O@|05U{lTN=M5XumN3H+aasl?K$eAN!3zoV zysqmiO=v7!a}%e4+Ew0m3RYM1$s|=(MTuaeA9k9&M!jZ4p|xYiZa8l<2F;UO=&>NW zyjxz>aO~zAo8hj-iyPstwQyJE*;D1irz>@*cHpQl-5lM5i|<=Hedql0`SOX8EkStk zHM}E=ZG*Th9r?x|b&cW2h_~CIwrXlD;$jkac>72OFT2b&%KydLBtAw4R2^hfd+I$~ py`b~Pt1!non{Ci+i5TN=jw3AZxXSt5`MqIXc{}&BY$!Guo literal 0 HcmV?d00001 diff --git a/models/__pycache__/survey_user_input.cpython-312.pyc b/models/__pycache__/survey_user_input.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d53a529ee8eaa8f2687a68b3a2d78894645a893 GIT binary patch literal 6491 zcmcIJYiv_ldgs3UHgRIdapF820tqI>2_ZlxAutdign_^iUXvMWU3_m68$UAl-UQ;k zo<$W>^MkE|surg+D}uTbS+P>HRnn`M&R*^S!^5U)t>!1nn)yg|NRKq5r@?^u`q@Yke@ej|3!8Q52)b zC<@VN2nqBZBrqZsVReMk_?Rio$Cz2x&%MAu{FG*hB?K`lhe~h=6#>!OVfgxIu#Jeu zC;^Sp0yV}6^cZVKW+WKr&^RS9cg$m)z}`V)27!a#C>WqO&7oV=n0XZWO#FtU%2%Qr8-+R99%Hu2MO;D>A8H~zBgTCnPKz?$ZVM8@@o-e^E2n}#`lhF% z30~Ne;yR(tlqW=moGnE`P|^3A2sEuSD*L#2&H}vOkPMWIzaY9dC{f@W_a=4*urM@x zN%ezaL8AsVtF9mTx3#DRSV~}Ej2NHM46?{ep-GK~b^AK7GCL(|wIw|d%Hh~lR4l5L zta0-6lqgA9hMKhmCIeMg7Ng^uNhhTl3U|?rp+qbu#ucq9F(XPhrLZCf#am%8i}-|Q zO+4x@^rR>Wvz_N>!8YR|@I{>oA(7bI z+gUJjF!w}U1~BYGbnOqv_zAJI;JDV8)}2LQYF4U(t&-l+Z#$mAh6{o&Ao+XHeSI6P z8c>b*{^Y&MOw+-vyEo@Pl64=++w0Sz#hyn8A0Euu+Vl3h^x&fBk@ulDW9wMu=%du$ zHAL5R&shQ6=E_-{v)1OtYrnE~=d0Ygs;+ER*RtnH|EGqPs&jLLt0v@d&RJe_$m#i{ zV#$@Q>U+}r^zx@y@~ypV4CU;b8+=|-HD_6a1EJ7H?Z=WK7WL;aA@)?%a}23Bl@chG zS=U}xr6s3IZ)IPgvqgLgsq`qe&yBkk2?{i^Poqx1G?wtewKK{7O)JSyR?sqpk?xycmg78Nomt)k&_-=`J3PJU|`eGkDr!K6&8={UOP2N!Ktjgf6SW@(I6mgf3QHS07|7S$qC zcbioUob({bhPvCPT2c&!;uMP%tOlg1Ovzn8rVHOBH{psUWi=z!8mTFQZWzFP^Z>#D zKN`P94-1X7b$Z(r2m^&y!W3*bU_XggSuiXJSJ;SBc9_}8Y}*7%RVd9m-bM-q2>k^5 zcpS}B*94&9Q0=ON(5O_m6=zjyRis6yRT>``r;(kdZ~9Ewrc;(Hfgcr7XUvsy;>1yP z3J$djOH_yWQwlis2kanyMmS!ba*@%Us)n&LS`+i6+@M$0k$w89N~7wEV52K*YNdWA zeN5G;?g(f=c~fiFAv~%l(p$cckWe*)q)J%p-(t;K!WLYqT7VkZ45ik>-Z(8TYmv9h zKEO|1Ti$eET~?_&*lTbzzFH?#3vQw2M;<-V07|aNiLD?bLz}wR z5m5-7^s^EUKN3!KHKTkZJT)akc3G@9aPC+L(%6fRBGPyJG2|zA;iJ*SO}L70I1V8{ED+c} zAJF24agyk#HTJ^Lv7;JwUSnY5r(YmQc%)8zWA^m<6GJb$VS3TeXa=Z!V!WamKb$7T zq-IJ?DdAW+DdPJK78we)pMIaDJuC%yMd3q}Bp)_h565|F7D%yiJ|=1`NxnI#xOn*m z4!fF@lxKxP4u6JRUiP<1I23BeXkualr_g4|gwDWRlr&Q?h$#hwni(g>fk{P~lCWdf z7+H}t!xS&^F1g-b&ZLN z@I~U|6CwtMI$1N}68vUZnba!Cexi;kOS|!|hU>g69_WD@BcUH8wE?)LERObYyVs+$dBW zP=XUGBB=y46I_h2jO2WhkDP%AC5;RI`VEQS4p<Lhltyqz z*kL0k7X$%`d`x4)u?fu-<8MKBnS{bFF|ELRhQ4Yk(U); z>UX>q905f6-@yc--m#&^?GLUjT*-S|9`rBt=WFWo-qx39&S9BjVbNBT?pU$5t=W*> zn|C+l+-+HR+mB2u?!9wEzj4;4FE1*8{U={m20nLr^Se5iO+T^z*t(R;v=01Se%kvl z{r}j%a^zg*-Oi}Tm=zK&Ojb+@FQ`KHzfA1!>8YucM_+PmDc(sUs0)asiS2bX&O z=AC@2KYjW0hV83nfPHO4j=FT~4_cm8w0`06uF`B}ZN8!T!Pvssl3}HxbN=K@3v$){ zpl<2p-yeV0{MOG$pSpkc{-=9?8q9m@=e^H8jp+{;%yYx}^+`k1+{k}c*L|`tJ+sI! z%zk`$rF!=o+HK!IPvvWTi+`ynOlv43gfpQ?X2*?{x@g7~T{YBJ);_PP-;D8vXXh85<}W=RKylaZN6ClD z<5WT}3o@j#jZ!1|^Ky$ijIsjRmrzkTPU_J{3D z;>z}(^dPM5=t`UN9`6JDf_-svnag#(o$Y!%*L5n}b?TXC7y$h3e{1=n<*_Yo$6%%f z(_+K&%u^|I@oL)i%=2EpX=kpfGuza;Y|nL_%yykz={mL2G@NEvEm$M2E?iwqWE&3T z8V0fr1Nr*qeEkmK`%5Exq-qVZjm~)s09H2T96Pg)or{r2v4^pLb@Z%WqJWdDA5f?x zu=L?_Ak%&{v-{ZG@l0iF*4CDQ&3uLRc}YOsI>LujY4-DS# z2hX;mQVQqAFiD<3VI`!9g0JvwA_u;8JsgH{wX}xwuQyek0)?x=Ta;vjoD)SbPFaHo z0D6Sv2Yy;29vDAKL*%8RGUld_IIM3>hoXG!y1*Y!9xnGE;I=3mr=X*}C3e)m?bPg9MRF5;PkL1cekx#-S1(hp|FnmrQP?)&Fnj zPVV{t-17TCSjO=K-Wc3Kyj}BR%sJgI50uS z!eepnA>lU>jXR6~IYCx_Sdgx+RE7H-mc9#MSldYWTxBSV`f3lNJg*IBXsUaD{C@Oa L^ecqtgem_6r7CE2 literal 0 HcmV?d00001 diff --git a/models/__pycache__/survey_user_input_line.cpython-312.pyc b/models/__pycache__/survey_user_input_line.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e4053c2179f2386e921d6d82aa2314a57e0d5bdd GIT binary patch literal 2500 zcmai0TW=Fb6rSDn`j(4RoP-jD7@^z@sRJpO3VKtEN`WR60xC5vtCeShH(BqRoi)W) zkb;B+NTBAW0-oAOP>|aH(TBzjG|{RLNPX&CAgPLY>N(>}E;a2)o;_#xJ9EyQ`OX>t z+}0K+Fn&p#u}*{Z2P*ZU*MyaiK)6R3VTw)iN=C`688xqEG=-?-7-8xS!ZfZ;M(c)a z&pE9sXY^7irT>XaN((fzURAr`JQdsD*KBrdoOhh6(njph0@;KVS#Q+zd>~4aKv3MPIi%cl2-+$t1=}sy#@K7riUIbWU*ZxKk+lpIHt!V+tX03gc|f z5;SX@LQsCy=MEFp&v7~+q*1yER)&RUX&dYYs7xjGU1~ChKg+jV$F${I)pS$SVKnPH z!uL$e@dcf5Jvw38oE8hVYci3R;8E`9TqX`n1tT%CIi8&|Oh18-#M)E|l9WFKzn1na$<7opzD&-FYm3a-O&HP|{HA-WlW?5}0M z7UmEJVmhUCO7XBXgGenILCd=2XMrJ&fz^U_wsIb(@05FFnj9rx_Y!hVnN}tnur+}x zQ-=r<%AKTNs}n-}dd-n(!}qr$0nWYDz1AMgRY(>3#-2%IXjeLa3KJ#@zLF$k|vb z;afy-rZPZ7`Kn&56>) z&wgCMgb6sZ8YZ#!+mTAN?@4rEsq@VjM2{WW%$B9(*2UzWUz2;528Vu(J&OI*yEt;9 zGIC;ZO&hlkTEsAzn*|{XHxCq*<$Fk&AU0lajnxW{jY1O}$-^X5(S+Q(lI#m>9Z}n zX8RYn?5b?pwY2rXeD}d+RcY^A)wN{*%tAlgk!025W*fKcP?|};UB6%AI1MW67z^u@yL!>(RZsEf8W~;DsEcrfN5D% d6y@(BqV!cr?`pVLIdZ@M`=JLze-RL+`9H&tlI8#a literal 0 HcmV?d00001 diff --git a/models/survey_question.py b/models/survey_question.py new file mode 100755 index 0000000..b6cc575 --- /dev/null +++ b/models/survey_question.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies() +# Author: Mohammed Dilshad Tk (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################## +from odoo import fields, models + + +class SurveyQuestion(models.Model): + """ + This class extends the 'survey.question' model to add new functionality + for file uploads. + """ + _inherit = 'survey.question' + + question_type = fields.Selection( + selection_add=[('upload_file', 'Upload File')], + help='Select the type of question to create.') + upload_multiple_file = fields.Boolean(string='Upload Multiple File', + help='Check this box if you want to ' + 'allow users to upload ' + 'multiple files') + + def validate_question(self, answer, comment=None): + """Validate question answer.""" + self.ensure_one() + if self.question_type == 'upload_file': + if self.constr_mandatory: + # Answer comes as a JSON string '[dataURLs, fileNames]' or raw list from previous steps? + # The controller passes what it received. + # If we parsed it in `_save_lines`, that's too late. Validation happens before. + + # We need to handle the string if it's not parsed yet, or parsed if Odoo does something. + # Standard Odoo validation receives raw inputs usually. + import logging + from odoo.http import request + _logger = logging.getLogger(__name__) + + # Fallback: Check for prefixed param if answer is empty + # because standard controller looks for str(id) + if not answer or answer == '[]': + # Try multiple key variants including empty string (common issue with t-att-name) + keys_to_check = [f'upload_{self.id}', str(self.id), ''] + for key in keys_to_check: + val = request.params.get(key) + if val and val != '[]': + answer = val + break + + is_answered = False + if answer and answer != '[]': + import json + try: + # If answer is a string, try parsing it + if isinstance(answer, str): + answer_data = json.loads(answer) + else: + answer_data = answer + + # Check if we have dataURLs (first element of list) + if isinstance(answer_data, list) and len(answer_data) > 0 and answer_data[0]: + is_answered = True + except Exception as e: + _logger.error(f"VALIDATE QUESTION {self.id}: Error parsing answer: {e}") + is_answered = False + + if not is_answered: + return {self.id: "CUSTOM TAG: This question requires an answer."} + return {} + return super(SurveyQuestion, self).validate_question(answer, comment) diff --git a/models/survey_user_input.py b/models/survey_user_input.py new file mode 100755 index 0000000..c3bb8f9 --- /dev/null +++ b/models/survey_user_input.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies() +# Author: Mohammed Dilshad Tk (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################## +from odoo import models + + +class SurveyUserInput(models.Model): + """ + This class extends the 'survey.user_input' model to add custom + functionality for saving user answers. + + Methods: + _save_lines: Save the user's answer for the given question + _save_line_file:Save the user's file upload answer for the given + question + _get_line_answer_file_upload_values: + Get the values to use when creating or updating a user input line + for a file upload answer + """ + _inherit = "survey.user_input" + + def _save_lines(self, question, answer, comment=None, + overwrite_existing=False): + """Save the user's answer for the given question.""" + old_answers = self.env['survey.user_input.line'].search([ + ('user_input_id', '=', self.id), + ('question_id', '=', question.id), ]) + if question.question_type == 'upload_file': + res = self._save_line_simple_answers(question, old_answers, answer) + else: + res = super(SurveyUserInput, self)._save_lines(question, answer, comment, + overwrite_existing) + return res + + def _save_line_simple_answers(self, question, old_answers, answer): + """ Save the user's file upload answer for the given question.""" + vals = self._get_line_answer_file_upload_values(question, + 'upload_file', answer) + if old_answers: + old_answers.write(vals) + return old_answers + else: + return self.env['survey.user_input.line'].create(vals) + + def _get_line_answer_file_upload_values(self, question, answer_type, + answer): + """Get the values to use when creating or updating a user input line + for a file upload answer. + Auto-compress images if they are uploaded.""" + vals = { + 'user_input_id': self.id, + 'question_id': question.id, + 'skipped': False, + 'answer_type': answer_type, + } + if answer_type == 'upload_file': + # --------------------------------------------------------- + # RETRIEVAL LOGIC: Same as in validate_question + # If standard controller failed to pass the answer (because key mismatch), + # we try to fetch it from request.params ourselves. + # --------------------------------------------------------- + import logging + _logger = logging.getLogger(__name__) + + if not answer or answer == '[]': + from odoo.http import request + if request: + keys_to_check = [f'upload_{question.id}', str(question.id), ''] + for key in keys_to_check: + val = request.params.get(key) + if val and val != '[]': + answer = val + break + + if isinstance(answer, str): + import json + try: + answer = json.loads(answer) + except Exception as e: + _logger.error(f"SAVE QUESTION {question.id}: JSON Decode Error: {e}") + pass + + if not answer or not isinstance(answer, list) or len(answer) < 2: + return vals # Skipped=False, but no files. + + file_data = answer[0] + file_name = answer[1] + attachment_ids = [] + + for i in range(len(answer[1])): + data = file_data[i] + fname = file_name[i] + + # Validation: Restrict to PNG, JPG, JPEG + if not fname.lower().endswith(('.png', '.jpg', '.jpeg')): + from odoo.exceptions import UserError + raise UserError(f"Only image files (PNG, JPG, JPEG) are allowed. Invalid file: {fname}") + + # Auto-compression logic + try: + # Check if file is an image based on extension or simple check + if fname.lower().endswith(('.png', '.jpg', '.jpeg')): + import base64 + import io + from PIL import Image + + # Decode base64 + image_stream = io.BytesIO(base64.b64decode(data)) + img = Image.open(image_stream) + + # Convert to RGB if RGBA/P to avoid issues saving as JPEG + if img.mode in ('RGBA', 'P'): + img = img.convert('RGB') + + # Resize if too large (max 1024x1024) + max_size = (1024, 1024) + img.thumbnail(max_size, Image.Resampling.LANCZOS) + + # Compress with lower quality + output_stream = io.BytesIO() + img.save(output_stream, format='JPEG', quality=50, optimize=True) + data = base64.b64encode(output_stream.getvalue()) + except Exception as e: + _logger.error(f"SAVE QUESTION {question.id}: Compression Error: {e}") + pass + + attachment = self.env['ir.attachment'].create({ + 'name': fname, + 'type': 'binary', + 'datas': data, + }) + attachment_ids.append(attachment.id) + + # Use Command tuples for Many2many relationship + # (6, 0, ids) replaces all existing records with the new list + vals['value_file_data_ids'] = [(6, 0, attachment_ids)] + return vals + + def action_delete_uploaded_files(self): + """Action to delete uploaded files for selected surveys.""" + for record in self: + file_answers = record.user_input_line_ids.filtered( + lambda l: l.answer_type == 'upload_file' and l.value_file_data_ids + ) + for line in file_answers: + # Unlink attachments + line.value_file_data_ids.unlink() + diff --git a/models/survey_user_input_line.py b/models/survey_user_input_line.py new file mode 100755 index 0000000..093e566 --- /dev/null +++ b/models/survey_user_input_line.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies() +# Author: Mohammed Dilshad Tk (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################## +from odoo import api, fields, models + + +class SurveyUserInputLine(models.Model): + """ + This class extends the 'survey.user_input.line' model to add additional + fields and constraints for file uploads. + Methods: + _check_answer_type_skipped:Check that a line's answer type is + not set to 'upload_file' if the line is skipped + """ + _inherit = "survey.user_input.line" + + answer_type = fields.Selection( + selection_add=[('upload_file', 'Upload File')], + help="The type of answer for this question (upload_file if the user " + "is uploading a file).") + value_file_data_ids = fields.Many2many('ir.attachment', + help="The attachments " + "corresponding to the user's " + "file upload answer, if any.") + + @api.constrains('skipped', 'answer_type') + def _check_answer_type_skipped(self): + """ Check that a line's answer type is not set to 'upload_file' if + the line is skipped.""" + for line in self: + if line.answer_type != 'upload_file': + super(SurveyUserInputLine, line)._check_answer_type_skipped() + + @api.depends('answer_type', 'value_file_data_ids') + def _compute_display_name(self): + """Override to include file names in the display name.""" + super()._compute_display_name() + for line in self: + if line.answer_type == 'upload_file': + if line.value_file_data_ids: + line.display_name = ', '.join(line.value_file_data_ids.mapped('name')) + else: + line.display_name = 'No file uploaded' diff --git a/static/src/js/SurveyFormWidget.js b/static/src/js/SurveyFormWidget.js new file mode 100755 index 0000000..e4c8e7d --- /dev/null +++ b/static/src/js/SurveyFormWidget.js @@ -0,0 +1,13 @@ +/** @odoo-module */ +import SurveyFormWidget from '@survey/js/survey_form'; +SurveyFormWidget.include({ + /** Get all question answers by question type */ + _prepareSubmitValues(formData, params) { + this._super(...arguments); + this.$('[data-question-type]').each(function () { + if ($(this).data('questionType') === 'upload_file'){ + params[this.name] = [$(this).data('oe-data'), $(this).data('oe-file_name')]; + } + }); + }, +}); diff --git a/static/src/js/survey_form_attachment.js b/static/src/js/survey_form_attachment.js new file mode 100755 index 0000000..5e62de7 --- /dev/null +++ b/static/src/js/survey_form_attachment.js @@ -0,0 +1,134 @@ +/** @odoo-module */ +import publicWidget from "@web/legacy/js/public/public_widget"; +import SurveyFormWidget from '@survey/js/survey_form'; +import SurveyPreloadImageMixin from "@survey/js/survey_preload_image_mixin"; +/** Extends publicWidget to create "SurveyFormUpload" */ +publicWidget.registry.SurveyFormUpload = publicWidget.Widget.extend(SurveyPreloadImageMixin, { + selector: '.o_survey_form', + events: { + 'change .o_survey_upload_file': '_onFileChange', + }, + init() { + this._super(...arguments); + // this.rpc = this.bindService("rpc"); + }, + /** On adding file function */ + _onFileChange: function (event) { + var self = this; + var files = event.target.files; + var fileNames = []; + var dataURLs = []; + + var $target = $(event.target); + // Find the container for this specific question + var $container = $target.closest('.o_survey_upload_container'); + + // Find elements scoped to this container + var $fileList = $container.find('.o_survey_upload_list'); + var fileListEl = $fileList[0]; + + var $hiddenInput = $container.find('input.o_survey_upload_file_value'); + + // Clear existing file list and delete button + if (fileListEl) { + fileListEl.innerHTML = ''; + } + + if (files.length === 0) { + $target.attr('data-oe-data', ''); + $target.attr('data-oe-file_name', ''); + $hiddenInput.val(''); + return; + } + + var loadedFiles = 0; + var totalFiles = files.length; + + // Create container for previews + var previewContainer = document.createElement('div'); + previewContainer.className = 'o_survey_file_previews d-flex flex-wrap gap-2 mb-2'; + if (fileListEl) fileListEl.appendChild(previewContainer); + + // Function to finish processing when all files are read + var checkAllFilesLoaded = function () { + if (loadedFiles === totalFiles) { + // Get question ID from data attribute - checking both input and hidden for flexibility + var questionId = $target.attr('data-question-id') || $hiddenInput.attr('data-question-id'); + + var finalPayload = JSON.stringify([dataURLs, fileNames]); + $target.attr('data-oe-data', JSON.stringify(dataURLs)); + $target.attr('data-oe-file_name', JSON.stringify(fileNames)); + + // Set name dynamically to ensure backend finds it with the correct key + if (questionId) { + $hiddenInput.attr('name', 'upload_' + questionId); + } + $hiddenInput.val(finalPayload); + + // Create delete button only once + var deleteBtn = document.createElement('button'); + deleteBtn.className = 'btn btn-danger btn-sm mt-2'; + deleteBtn.textContent = 'Delete All'; + deleteBtn.type = 'button'; // Prevent form submission + deleteBtn.addEventListener('click', function () { + // Clear file list + if (fileListEl) fileListEl.innerHTML = ''; + // Clear input field attributes + $target.attr('data-oe-data', ''); + $target.attr('data-oe-file_name', ''); + $hiddenInput.val(''); + // Reset file input + $target.val(''); + }); + if (fileListEl) fileListEl.appendChild(deleteBtn); + } + }; + + for (let i = 0; i < files.length; i++) { + var reader = new FileReader(); + reader.readAsDataURL(files[i]); + reader.onload = function (e) { + var file = files[i]; + var filename = file.name; + var dataURL = e.target.result.split(',')[1]; /** split base64 data */ + + // Ensure order is preserved might be tricky with async, but simple push is ok for now + // or use index if strict order needed. + // Detailed handling often requires mapping index. + fileNames.push(filename); + dataURLs.push(dataURL); + + // Create preview element + var previewItem = document.createElement('div'); + previewItem.className = 'card p-1'; + previewItem.style.width = '100px'; + + if (file.type.startsWith('image/')) { + var img = document.createElement('img'); + img.src = e.target.result; // Use full data URL for preview + img.className = 'card-img-top'; + img.style.height = '80px'; + img.style.objectFit = 'cover'; + img.title = filename; + previewItem.appendChild(img); + } else { + var icon = document.createElement('div'); + icon.className = 'text-center p-3'; + icon.innerHTML = ''; + previewItem.appendChild(icon); + } + + var nameDiv = document.createElement('div'); + nameDiv.className = 'card-body p-1 text-truncate small'; + nameDiv.textContent = filename; + previewItem.appendChild(nameDiv); + + previewContainer.appendChild(previewItem); + + loadedFiles++; + checkAllFilesLoaded(); + } + } + }, +}); +export default publicWidget.registry.SurveyFormUpload; diff --git a/views/survey_question_views.xml b/views/survey_question_views.xml new file mode 100755 index 0000000..39e9672 --- /dev/null +++ b/views/survey_question_views.xml @@ -0,0 +1,22 @@ + + + + + survey.question.view.form.inherit.survey.upload.file + survey.question + + + +
+

Upload Files + +

+
+
+ + + +
+
+
diff --git a/views/survey_templates.xml b/views/survey_templates.xml new file mode 100755 index 0000000..36fe216 --- /dev/null +++ b/views/survey_templates.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + diff --git a/views/survey_user_views.xml b/views/survey_user_views.xml new file mode 100755 index 0000000..f448de8 --- /dev/null +++ b/views/survey_user_views.xml @@ -0,0 +1,49 @@ + + + + + survey.user_input.line.view.form.inherit.survey.upload.image + survey.user_input.line + + + + + + + + + + + + + + + + + ir.attachment.form.survey.upload + ir.attachment + +
+ + + + + + + + +
+
+
+ + + Delete Uploaded Files + + + list,form + code + + action = records.action_delete_uploaded_files() + + +