355 lines
19 KiB
Plaintext
355 lines
19 KiB
Plaintext
<!---
|
||
bean (child tag)
|
||
msyu@mail.ru
|
||
2013-08-29 v0.1
|
||
2013-09-04 v0.2 tested on CF8 and Railo
|
||
2014-01-20 v0.3 added exception for unsupported parameter type
|
||
2014-10-24 v0.5 ATTRIBUTES.field became default for ATTRIBUTES.param
|
||
2014-11-18 v0.6 rework
|
||
2015-07-21 v0.7 EXITTAG moved to the end of file
|
||
2015-07-22 v0.8 Changed protocol for skipInsert, skipUpdate, key, readonly, autoincrement, required attributes, now we can set value at runtime like skipInsert="#boolean#" instead of just including or omitting the attribute
|
||
2015-12-02 v0.9 Added preprocessor attribute. Fixed timestamp bug ('01.12.2015' treated as 12 JAN 2015)
|
||
2015-12-04 v0.12 for dates, submitted value is now back-overwritten
|
||
2015-12-04 v0.13 bug fix
|
||
v0.94
|
||
2017-03-10 v0.15 - scope resolution order changed to CALLER-FORM-URL
|
||
2018-05-31 v0.16 - strip whitespace from numbers
|
||
2019-02-09 v0.17 - call preprocessor before checking required field
|
||
2022-01-06 v0.18 - added BIT type (for PostgreSQL) - в базе представляется boolean!
|
||
2023-04-17 v0.19 - форматирование, добавлен other к idstamp (для postgres)
|
||
2023-04-18 v0.20 - добавлен атрибут format, для даты
|
||
2024-03-22 v0.21 - добавлены синонимы int,boolean,bool. Перенумерованы версии 91->11
|
||
2024-10-16 v0.23 - добавлены атрибуты minlength, maxlength, regex, validate
|
||
--->
|
||
|
||
<!--- проверка на неподдерживаемые поля --->
|
||
<cfloop collection=#ATTRIBUTES# item="a">
|
||
<cfif NOT ListFindNoCase("param,field,type,displayName,default,init,size,scale,skipInsert,skipUpdate,key,readonly,autoincrement,value,null,forNull,eval,required,preprocessor,dtlocale,format,maxlength,minlength,regex,validate", #a#)>
|
||
<cfthrow type="UnsupportedAttribute" message='Attribute #a# is unsupported by bean/param' detail='Specified attribute "#a#" is not supported by custom tag bean/param'/>
|
||
</cfif>
|
||
</cfloop>
|
||
|
||
<cfparam name="ATTRIBUTES.default" default=""/><!--- подставляется при сохранении, если параметр не передавался. Важно для чекбоксов, они не передаются, если не отмечены --->
|
||
<cfparam name="ATTRIBUTES.init" default=#ATTRIBUTES.default#/><!--- передается в форму для новой записи (предзаполнение). Для чекбокса может быть init=1, default=0 - отмеченный по умолчанию --->
|
||
<cfparam name="ATTRIBUTES.field"/><!--- имя поля в таблице БД --->
|
||
<cfparam name="ATTRIBUTES.param" default="#ATTRIBUTES.field#"/><!--- имя поля в URL/FORM и экспортируемой переменной --->
|
||
<cfparam name="ATTRIBUTES.type" default="varchar"/><!--- тип поля в терминах SQL (CF_SQL_*) --->
|
||
<cfparam name="ATTRIBUTES.displayName" default=#ATTRIBUTES.param#/><!--- для сообщений об ошибках--->
|
||
<cfparam name="ATTRIBUTES.size" default="0"/><!--- for strings, soft limit (causes truncation). 0=unlimited --->
|
||
<cfparam name="ATTRIBUTES.scale" default="1"/> <!--- Множитель. Только для чисел, только при записи. --->
|
||
<!--- если атрибут записан без значения, он получает значение true! --->
|
||
<cfparam name="ATTRIBUTES.skipInsert" type="boolean" default="false"/>
|
||
<cfparam name="ATTRIBUTES.skipUpdate" type="boolean" default="false"/>
|
||
<cfparam name="ATTRIBUTES.key" type="boolean" default="false"/>
|
||
<cfparam name="ATTRIBUTES.readonly" type="boolean" default="false"/>
|
||
<cfparam name="ATTRIBUTES.autoincrement" type="boolean" default="false"/><!--- правильнее autogenerated, потому что GUID тоже бывает генеренным --->
|
||
<cfparam name="ATTRIBUTES.required" type="boolean" default="false"/>
|
||
<cfparam name="ATTRIBUTES.dtlocale" type="string" default="ru"/><!--- datetime locale--->
|
||
<cfparam name="ATTRIBUTES.format" type="string" default=""/><!--- datetime format, normally yyyy-MM-dd, CASE SENSITIVE, NOT YYYY-DD-MM!!!--->
|
||
<cfparam name="ATTRIBUTES.maxlength" default="0"/><!--- max length hard limit, throws exception unlike ATTRIBUTES.size. 0=unlimited, специально не указываем тип - то есть это по умолчанию строка - чтобы не ломалось от пустой строки--->
|
||
<cfparam name="ATTRIBUTES.minlength" default="0"/><!--- min length hard limit, throws exception unlike ATTRIBUTES.size. 0=unlimited, специально не указываем тип - то есть это по умолчанию строка - чтобы не ломалось от пустой строки--->
|
||
<!--- <cfparam name="ATTRIBUTES.regex" type="string" default=""/> мы его закомментировали и оставили, чтобы про него не забыть, а дальше мы проверяем его наличие в скоупе атрибутов ---><!--- validation regex --->
|
||
<cfparam name="ATTRIBUTES.validate" type="any" default=#function(x){return ""}#/><!--- non-empty return indicates error --->
|
||
|
||
<!--- ATTRIBUTES.forNull ---><!--- если forNull не задано, параметр считается обязательным ***?--->
|
||
<!--- ATTRIBUTES.preprocessor ---><!--- функция --->
|
||
|
||
<!--- in-out --->
|
||
<cfparam name="ATTRIBUTES.null" type="boolean" default="false"/>
|
||
<!--- ATTRIBUTES.value - входной-выходной параметр. --->
|
||
<!---/in-out --->
|
||
|
||
<!--- *** выходные параметры. Странно выглядит. Хоть как-то в имени это отразить, что выходные... --->
|
||
<cfparam name="ATTRIBUTES.hasInput" type="boolean" default="false" />
|
||
<cfparam name="ATTRIBUTES.submittedValue" type="string" default=""/><!--- необработанное входное значение --->
|
||
<cfparam name="ATTRIBUTES.errorState" type="boolean" default="false"/>
|
||
<cfparam name="ATTRIBUTES.errorMessage" type="string" default=""/>
|
||
<cfparam name="ATTRIBUTES.warningState" type="boolean" default="false"/>
|
||
<cfparam name="ATTRIBUTES.warningMessage" type="string" default=""/>
|
||
|
||
<!--- Тут намешано 2 стиля, и оба не вызывают восторга
|
||
Атрибут декларируется с дефолтом (потому что, в отличие от аргумента функции, мы не можем декларировать необязательный атрибут, так как используем cfparam)
|
||
Или атрибут не декларируется, а проверяется его наличие в скоупе атрибутов, что не улучшает читаемость кода --->
|
||
|
||
<cfassociate basetag="cf_bean" datacollection="params" />
|
||
|
||
<!--- Поле "только для чтения" не обрабатываем никак, если только оно не ключевое. Зачем может понадобиться readonly ключевое поле - непонятно, но вдруг --->
|
||
<cfif NOT (ATTRIBUTES.key)>
|
||
<cfif (ATTRIBUTES.readonly)>
|
||
<cfexit method="exittag"/>
|
||
</cfif>
|
||
</cfif>
|
||
<!--- Если атрибуту явно указан NULL, дальше ничего не делаем --->
|
||
<cfif ATTRIBUTES.null>
|
||
<cfset ATTRIBUTES.value="0"/><!--- значение не используется, но переменная должна быть определена --->
|
||
<cfexit method="exittag" />
|
||
</cfif>
|
||
|
||
<!--- принимаем значение из передаваемых данных или подставляем значение по умолчанию --->
|
||
<!--- можно было бы попробовать cfparam CALLER.#ATTRIBUTES.param# и обрабатывать исключение, но так обходимся без исключений, хотя и не изящно --->
|
||
<cfif StructKeyExists(ATTRIBUTES,"value")>
|
||
<cfset submittedValue=ATTRIBUTES.value/>
|
||
<cfset ATTRIBUTES.hasInput="true"/>
|
||
<cfelseif StructKeyExists(CALLER, ATTRIBUTES.param)>
|
||
<cfset submittedValue=structFind(CALLER, ATTRIBUTES.param)/>
|
||
<cfset ATTRIBUTES.hasInput="true"/>
|
||
<cfelseif StructKeyExists(FORM, ATTRIBUTES.param)>
|
||
<cfset submittedValue=structFind(FORM, ATTRIBUTES.param)/>
|
||
<cfset ATTRIBUTES.hasInput="true"/>
|
||
<cfelseif StructKeyExists(URL, ATTRIBUTES.param)>
|
||
<cfset submittedValue=structFind(URL, ATTRIBUTES.param)/>
|
||
<cfset ATTRIBUTES.hasInput="true"/>
|
||
|
||
<cfelse>
|
||
<!--- если параметр отсутствует в передаваемых данных --->
|
||
<cfset submittedValue=ATTRIBUTES.default />
|
||
</cfif>
|
||
|
||
<cfif !StructKeyExists(ATTRIBUTES,"value")>
|
||
<cfset ATTRIBUTES.value="" /><!---*** dirty fix--->
|
||
</cfif>
|
||
|
||
<cfset ATTRIBUTES.submittedValue=submittedValue />
|
||
|
||
|
||
<cfif StructKeyExists(ATTRIBUTES,"preprocessor")>
|
||
<cfset submittedValue=#ATTRIBUTES.preprocessor(submittedValue)#/>
|
||
</cfif>
|
||
|
||
<!--- check required fields --->
|
||
<cfif ATTRIBUTES.required>
|
||
<cfif submittedValue EQ "">
|
||
<cfset ATTRIBUTES.errorState = "true"/>
|
||
<cfset ATTRIBUTES.errorMessage = "Не заполнено обязательное поле #ATTRIBUTES.displayName# "/>
|
||
<cfset ATTRIBUTES.value="" />
|
||
<cfexit method="exittag" />
|
||
</cfif>
|
||
</cfif>
|
||
|
||
<cfif StructKeyExists(ATTRIBUTES, "forNull")>
|
||
<cfif #submittedValue# EQ #ATTRIBUTES.forNull#>
|
||
<cfset ATTRIBUTES.null="true" />
|
||
<cfset ATTRIBUTES.value="" />
|
||
<cfexit method="exittag" />
|
||
</cfif>
|
||
</cfif>
|
||
|
||
<!--- type validation --->
|
||
<cfswitch expression=#ATTRIBUTES.type#>
|
||
<!--- для integer, bigint наличие атрибутa autoincrement подавляет сообщение об ошибке --->
|
||
<!--- *** можно было сделать для всех, но тогда надо обрабатывать исключения --->
|
||
<cfcase value="integer,int,bigint"><!--- *** формат с экспонентой не будет работать --->
|
||
<cfset s=reReplace(submittedValue, "[[:space:]]", "", "ALL")/>
|
||
<cfif FindOneOf(",.",s)>
|
||
<cfset s=ListFirst(s,",.")/>
|
||
<cfset ATTRIBUTES.warningState = "true"/>
|
||
<cfset ATTRIBUTES.warningMessage = "Number is truncated #ATTRIBUTES.displayName#: #submittedValue#->#s#"/>
|
||
</cfif>
|
||
|
||
<!---*** не слишком явно - подавление ошибки для автоинкрементного поля --->
|
||
<cfif (NOT isNumeric(s))>
|
||
<!---<cfif (NOT structKeyExists(ATTRIBUTES, "autoincrement"))>
|
||
<cfif (NOT structKeyExists(ATTRIBUTES, "autoincrement")) AND (NOT structKeyExists(ATTRIBUTES, "key"))> --->
|
||
<cfset ATTRIBUTES.errorState = "true"/>
|
||
<cfset ATTRIBUTES.errorMessage = "Invalid number (#ATTRIBUTES.displayName#: #submittedValue#)"/>
|
||
<!---<cfthrow type="custom" message="Invalid number" detail="Invalid number (#ATTRIBUTES.displayName#: #submittedValue#)"/>--->
|
||
|
||
<!--- по-хорошему, выбрасывать исключение нужно только при попытке сохранения
|
||
Но можно и не разбирать в некоторых режимах
|
||
--->
|
||
<cfelse>
|
||
<cfset ATTRIBUTES.value=s*ATTRIBUTES.scale/>
|
||
</cfif>
|
||
</cfcase>
|
||
|
||
<cfcase value="guid,uuid,idstamp">
|
||
<!--- guid = PostgreSQL hack --->
|
||
<cfif NOT isValid("guid", submittedValue) AND len(attributes.submittedvalue) GT 0>
|
||
<cfset ATTRIBUTES.errorState = "true" />
|
||
<cfset ATTRIBUTES.errorMessage = "[Error]: Value is not a GUID type." />
|
||
<!--- <cfthrow type="UnsupportedParameterType"
|
||
message="[Error]: Value is not a GUID type."
|
||
detail='Value is: #attributes.submittedvalue#'/> --->
|
||
<cfelse>
|
||
<cfset ATTRIBUTES.value = submittedValue />
|
||
</cfif>
|
||
</cfcase>
|
||
|
||
<cfcase value="numeric,decimal,float,real,double">
|
||
<cfset s=replace(reReplace(submittedValue, "[[:space:]]", "", "ALL"), ",", ".", "ALL")/>
|
||
<cfif NOT isNumeric(s)>
|
||
<cfset ATTRIBUTES.errorState = "true"/>
|
||
<cfset ATTRIBUTES.errorMessage = "Invalid number (#ATTRIBUTES.displayName#: #submittedValue#)"/>
|
||
<!---<cfthrow type="custom" message="Invalid number" detail="Invalid number (#ATTRIBUTES.displayName#: #submittedValue#)"/>--->
|
||
<cfelse>
|
||
<cfset ATTRIBUTES.value=s*ATTRIBUTES.scale/>
|
||
</cfif>
|
||
</cfcase>
|
||
|
||
<cfcase value="bit,boolean,bool"><!--- потому что транслируем примитивно в CF_SQL_BIT--->
|
||
<cfset s=trim(submittedValue)/> <!--- нужно ли это? пробелы действительно дают ошибку --->
|
||
<cfif NOT isBoolean(s)>
|
||
<cfset ATTRIBUTES.errorState = "true"/>
|
||
<cfset ATTRIBUTES.errorMessage = "Invalid boolean (#ATTRIBUTES.displayName#: #submittedValue#)"/>
|
||
<cfelse>
|
||
<!---<cfif s>
|
||
<cfset ATTRIBUTES.value=true/>
|
||
<cfelse>
|
||
<cfset ATTRIBUTES.value=false/>
|
||
</cfif>--->
|
||
<cfset ATTRIBUTES.value=s/>
|
||
</cfif>
|
||
</cfcase>
|
||
|
||
<cfcase value="date,time,timestamp">
|
||
<cftry>
|
||
<!--- <cfset ATTRIBUTES.value=LSParseDateTime(submittedValue,"#ATTRIBUTES.dtlocale#")/> --->
|
||
<cfset ATTRIBUTES.value=LSParseDateTime(submittedValue,"#ATTRIBUTES.dtlocale#","#ATTRIBUTES.format#")/>
|
||
<cfset ATTRIBUTES.submittedValue=ATTRIBUTES.value/><!--- *** dirty fix--->
|
||
<cfcatch type="ANY">
|
||
<cfset ATTRIBUTES.errorState = "true"/>
|
||
<cfset ATTRIBUTES.errorMessage = "Invalid datetime (#ATTRIBUTES.displayName#: #submittedValue#)"/>
|
||
<!---<cfthrow type="custom" message="Invalid datetime" detail="Invalid datetime (#ATTRIBUTES.displayName#: #submittedValue#)"/>--->
|
||
</cfcatch>
|
||
</cftry>
|
||
<!--- пробрасывать сырое значение при ошибке разбора плохо, потому что функция форматирования даты споткнется--->
|
||
</cfcase>
|
||
|
||
<cfcase value="varchar">
|
||
<cfset submittedValue=trim(submittedValue) />
|
||
<cfparam name="ATTRIBUTES.default" type="string" default=""/>
|
||
<cfset ATTRIBUTES.value=submittedValue/>
|
||
|
||
<cfif ATTRIBUTES.maxlength GT 0>
|
||
<cfif len(submittedValue) GT ATTRIBUTES.maxlength>
|
||
<cfset ATTRIBUTES.errorState = "true"/>
|
||
<cfset ATTRIBUTES.errorMessage = "Длина #ATTRIBUTES.displayName# превышает #ATTRIBUTES.maxlength#"/>
|
||
<cfset ATTRIBUTES.value="" />
|
||
<cfexit method="exittag" />
|
||
</cfif>
|
||
</cfif>
|
||
|
||
<cfif ATTRIBUTES.minlength GT 0>
|
||
<cfif len(submittedValue) LT ATTRIBUTES.minlength>
|
||
<cfset ATTRIBUTES.errorState = "true"/>
|
||
<cfset ATTRIBUTES.errorMessage = "Длина #ATTRIBUTES.displayName# меньше #ATTRIBUTES.minlength#"/>
|
||
<cfset ATTRIBUTES.value="" />
|
||
<cfexit method="exittag" />
|
||
</cfif>
|
||
</cfif>
|
||
|
||
<cfif ATTRIBUTES.size GT 0>
|
||
<cfif len(submittedValue) GT ATTRIBUTES.size>
|
||
<!--- truncate input --->
|
||
<cfset ATTRIBUTES.value=left(submittedValue, ATTRIBUTES.size)/>
|
||
<cfset ATTRIBUTES.warningState = "true"/>
|
||
<cfset ATTRIBUTES.warningMessage = "String is truncated (#ATTRIBUTES.displayName#: #len(submittedValue)#->#ATTRIBUTES.size#)"/>
|
||
</cfif>
|
||
</cfif>
|
||
</cfcase>
|
||
|
||
<!---<cfcase value="guid,uuid,uniqueidentifier,idstamp">--->
|
||
|
||
<cfcase value="other">
|
||
<!--- other = old PostgreSQL hack (incompatibility) --->
|
||
<!--- validation skipped --->
|
||
<cfset ATTRIBUTES.value = submittedValue />
|
||
</cfcase>
|
||
|
||
<cfcase value="varbinary">
|
||
<cfset ATTRIBUTES.value=submittedValue/>
|
||
</cfcase>
|
||
|
||
<cfdefaultcase>
|
||
<cfset ATTRIBUTES.value=""/>
|
||
<cfthrow type="UnsupportedParameterType" message="Specified parameter type is unsupported by current version of bean/param" detail='Parameter type "#ATTRIBUTES.type#" is not supported by custom tags bean/param'/>
|
||
</cfdefaultcase>
|
||
|
||
<!---<cfcase value="hex">
|
||
<cfset s=submittedValue/>
|
||
<cfif reFindNoCase("[^0-9A-F]", s)>
|
||
<cfset ATTRIBUTES.errorState = "true"/>
|
||
<cfset ATTRIBUTES.errorMessage = "Invalid HEXADECIMAL number (#ATTRIBUTES.displayName#: #submittedValue#)"/>
|
||
<cfelse>
|
||
<cftry>
|
||
<cfset ATTRIBUTES.value=inputBaseN(s, 16)/>
|
||
<cfcatch type="any">
|
||
<cfset ATTRIBUTES.errorState = "true"/>
|
||
<cfset ATTRIBUTES.errorMessage = "Error converting from HEXADECIMAL format (#ATTRIBUTES.displayName#: #submittedValue#)"/>
|
||
</cfcatch>
|
||
</cftry>
|
||
</cfif>
|
||
</cfcase>--->
|
||
</cfswitch>
|
||
|
||
<!--- regular expression validation --->
|
||
<cfif StructKeyExists(ATTRIBUTES,"regex")>
|
||
<cfif reFind(ATTRIBUTES.regex,submittedValue) EQ 0>
|
||
<cfset ATTRIBUTES.errorState = "true"/>
|
||
<cfset ATTRIBUTES.errorMessage = 'Pattern "#ATTRIBUTES.regex#" does not match "#submittedValue#"'/>
|
||
<cfset ATTRIBUTES.value="" />
|
||
<cfexit method="exittag" />
|
||
</cfif>
|
||
</cfif>
|
||
<!--- custom validation --->
|
||
<cfif StructKeyExists(ATTRIBUTES,"validate")>
|
||
<cfset validationMessage=ATTRIBUTES.validate(submittedValue)/>
|
||
<cfif len(validationMessage)>
|
||
<cfset ATTRIBUTES.errorState = "true"/>
|
||
<cfset ATTRIBUTES.errorMessage = validationMessage/>
|
||
<cfset ATTRIBUTES.value="" />
|
||
<cfexit method="exittag" />
|
||
</cfif>
|
||
</cfif>
|
||
|
||
<cfexit method="EXITTAG" />
|
||
|
||
<!--- это искусственный прием
|
||
<cfif ATTRIBUTES.errorState AND NOT structKeyExists(ATTRIBUTES, "autoincrement") AND NOT structKeyExists(ATTRIBUTES, "key")>
|
||
<cfset getBaseTagData("cf_bean").validationOk="false"/>
|
||
</cfif>
|
||
--->
|
||
<!---
|
||
CF_SQL_BIGINT
|
||
CF_SQL_BIT
|
||
CF_SQL_CHAR
|
||
CF_SQL_BLOB
|
||
CF_SQL_CLOB
|
||
CF_SQL_DATE
|
||
CF_SQL_DECIMAL
|
||
CF_SQL_DOUBLE
|
||
CF_SQL_FLOAT
|
||
CF_SQL_IDSTAMP
|
||
CF_SQL_INTEGER
|
||
CF_SQL_LONGVARCHAR
|
||
CF_SQL_MONEY
|
||
CF_SQL_MONEY4
|
||
CF_SQL_NUMERIC
|
||
CF_SQL_REAL
|
||
CF_SQL_REFCURSOR
|
||
CF_SQL_SMALLINT
|
||
CF_SQL_TIME
|
||
CF_SQL_TIMESTAMP
|
||
CF_SQL_TINYINT
|
||
CF_SQL_VARCHAR
|
||
*** CF_SQL_OTHER - используется с Postgre для GUID и jsonb
|
||
--->
|
||
<!--- при ошибочном вводе, например, некорректном формате, возможно два пути
|
||
- сохранить, подставив дефолт (в том числе нулл). Можно генерировать ворнинг, цепляя его прямо к полю. В этом случае надо требовать корректные дефолты
|
||
- можно ввести раздельные понятия дефолт и инит.
|
||
Разница между дефолтом и инитом. При инициализации новой
|
||
|
||
Как дефолт, так и инит имеют целевой тип (число, дата...)
|
||
Дефолт и инит используются для начального заполнения полей нового объекта. Для строки дефолт-дефолт - пустая, для остальных нулл
|
||
Рассмотреть обязательные и необязательные параметры?
|
||
Можно
|
||
- выгрузить некорректный набор данных для редактирования. При втором варианте нужно все выгружать в стринги? Тогда как будут работать функции форматирования в теле страницы, ожидающе не строку? Скажем, дату? По ближайшей догадке? Пустую строку?
|
||
Попытка вставить нулл в колонку NOT NULL должна пресекаться заранее (до ошибки БД)
|
||
Нулловые строки можно не рассматривать, приводя к пустой строке.
|
||
|
||
Тэг принимает параметр от формы, парсит и отдает в атрибуте value. Если не получает, не отдает, можно это специально указывать флагом. Можно ему лишнего не делегировать.
|
||
Остальное оставить вызывающему тегу?
|
||
--->
|
||
|
||
|