sso looks working
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
<!---
|
||||
bean v0.96
|
||||
msyu@mail.ru
|
||||
2013-08-29 v0.1
|
||||
2013-09-04 v0.2 tested with CF8 and Railo
|
||||
2013-10-14 added meta attribute - can now export each parameter metadata
|
||||
2014-12-26 POST-GET read-pass-save
|
||||
2015-01-19 read-pass-save, добавлен formmarker, убрано определение режима по POST-GET
|
||||
2015-01-19 убран formmarker, упразднен режим init (используется pass)
|
||||
2015-07-16 добавлен passmarker для перерисовки формы с введенными значениями
|
||||
2015-10-02 поддержка составных ключей
|
||||
2015-10-09 v0.9 если задано value параметра, считается, что он hasInput (корректно работает в составе ключа)
|
||||
2015-11-25 добавлен readmarker, mode (принудительная установка режима)
|
||||
2015-11-26 добавлен mode=skip
|
||||
2016-09-01 изменена трактовка processingMode. Теперь понимается как исходная команда, фиксируется до начала действий. Можно использовать для управления
|
||||
2018-09-16 добавлена ограниченная поддержка нецелых первичных ключей (idstamp)
|
||||
child tag: param
|
||||
to save - should send parameter with name from list ATTRIBUTES.savemarker (value doesn't matter) in FORM, e.g. URL.save="trtrtr", while ATTRIBUTES.savemarker="save,saveAndClose"; for multiple forms on a page (and bean tags) different savemarkers should be used
|
||||
2022-01-06 PostgreSQL (MSSQL incompatibility because of scope_identity)
|
||||
2024-04-13 PostgreSQL GUID, autoincrement by RETURNING
|
||||
2024-09-05 added status.recordExists (*** при вставке принудительно устанавливаем в true, веря, что если insert прошел, то запись появилась)
|
||||
--->
|
||||
|
||||
<!---
|
||||
Автогенерируемое поле GUID можно заполнять несколькими способами
|
||||
- как default или init bean/param, но это не очень удобно, потому что может потеряться удобный признак новой записи - инвалидный ключ, и сейчас init-default взаимодействуют некорректно, по крайней мере, неочевидно
|
||||
- генерируемое в БД (например, как default). Для PostgreSQL значение возвращается через Returning, как это делать для других БД, непонятно.
|
||||
- можно генерировать при сохранении в bean, но для этого нужно расширение синтаксиса, зато не зависит от БД (*** TODO)
|
||||
--->
|
||||
|
||||
<cfif thisTag.ExecutionMode is 'end'>
|
||||
<cfparam name="ATTRIBUTES.readmarker" type="string" default="read"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда перечитывания. Имеет приоритет перед savemarker, passmarker. Можно передавать несколько вариантов списком через запятую. --->
|
||||
<cfparam name="ATTRIBUTES.savemarker" type="string" default="save,saveAndClose"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда сохранения. Обычно это имя кнопки. Можно передавать несколько вариантов списком через запятую. --->
|
||||
<cfparam name="ATTRIBUTES.passmarker" type="string" default="pass"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда заполнить форму отправленными данными без перечитывания. В форме такое поле обычно есть. Команда сохранения имеет приоритет перед пробросом. Можно передавать несколько вариантов списком через запятую. --->
|
||||
<cfparam name="ATTRIBUTES.datasource" type="string"/>
|
||||
<cfparam name="ATTRIBUTES.table" type="string"/>
|
||||
<cfparam name="ATTRIBUTES.output" type="string"/><!--- имя переменной (структуры) области CALLER, в которую возвращаются данные. Обычно "d" --->
|
||||
<cfparam name="ATTRIBUTES.info" type="string" default=""/>
|
||||
<cfparam name="ATTRIBUTES.meta" type="string" default=""/>
|
||||
<cfparam name="ATTRIBUTES.status" type="string"/><!--- имя переменной (структуры) области CALLER, в которую возвращается статуc. Обычно "status" --->
|
||||
<cfparam name="ATTRIBUTES.readonly" type="boolean" default="No"/>
|
||||
<cfparam name="ATTRIBUTES.mode" type="string" default=""/> <!--- Принудительный режим read|pass|save|skip. Имеет приоритет перед маркерами --->
|
||||
|
||||
<cfif ATTRIBUTES.mode EQ "skip">
|
||||
<!---<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorState", false) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorMessage", "") />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.processingMode", "skip") />--->
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(false,"","skip",false))/><!--- *** можно было не создавать компонент, он все равно не имеет логики, это map из нескольких полей. С другой стороны, хоть поля декларированы. Не очень удобно, что в отдельном файле --->
|
||||
<cfexit method="exittag"/>
|
||||
</cfif>
|
||||
|
||||
<cfset validationOk="true" />
|
||||
<cfset errorState="false" />
|
||||
<cfset errorMessage="" />
|
||||
<cfset warningState="false" />
|
||||
<cfset warningMessage="" />
|
||||
<!---<cfset keyField=""/>
|
||||
<cfset keyValue=""/>--->
|
||||
<cfset hasAutoIncrement="false"/>
|
||||
<cfset autoIncrementKeyIndex=0/><!--- сгенерированное значение присваивается первому полю, отмеченному как autoincrement --->
|
||||
<cfset recordExists = "false"/>
|
||||
<cfset keyInvalid = "false"/>
|
||||
|
||||
<!--- разбор метаданных - поиск ключа --->
|
||||
<!--- поддерживаются только простые целочисленные ключи --->
|
||||
<cfset keys=arrayNew(1)/>
|
||||
<!--- *** почему где-то копируем поля param в key, а где-то прямо цикл по параметрам --->
|
||||
<!--- коллекция thistag.params формируется вложенными тегами param --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<!---<cfdump var=#param#/>--->
|
||||
<cfif structFind(param,"key")><!--- not just structKeyExists! Not exisis, OR exists, but FALSE! --->
|
||||
<cfset key = structNew()/>
|
||||
<cfset ArrayAppend(keys, key)/>
|
||||
|
||||
<cfset key.name = param.param />
|
||||
<cfset key.field = param.field />
|
||||
<cfset key.type = param.type />
|
||||
<cfset key.value = param.value /><!--- должно уже быть присвоено --->
|
||||
<cfset key.null = param.null />
|
||||
|
||||
<cfif param.errorState OR !param.hasInput>
|
||||
<cfset keyInvalid="true"/>
|
||||
<cfelse>
|
||||
<cfset keyInvalid=!checkDataFormat(param.type, param.value)/>
|
||||
</cfif>
|
||||
|
||||
<cfif !hasAutoIncrement><!--- take only first field with "autoincrement" attribute--->
|
||||
<cfif structFind(param, "autoincrement")>
|
||||
<cfset hasAutoIncrement = true/>
|
||||
<cfset autoIncrementKeyIndex = arrayLen(keys)/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
</cfif>
|
||||
</cfloop>
|
||||
<!---<cfdump var=#keys#/>--->
|
||||
|
||||
<cfif !keyInvalid>
|
||||
<!--- *** дублируется похожий cfqueryparam, перепутал один раз --->
|
||||
<cfquery name="qCheckExistence" datasource="#ATTRIBUTES.datasource#">
|
||||
select count(*) as cnt
|
||||
from #ATTRIBUTES.table#
|
||||
where 1=1
|
||||
<cfloop array=#keys# index="key">
|
||||
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
|
||||
</cfloop>
|
||||
</cfquery>
|
||||
<cfset recordExists = (qCheckExistence.cnt GT 0)/>
|
||||
<!--- <cfdump var=#qCheckExistence#/> --->
|
||||
<cfelse>
|
||||
<cfset recordExists = "false"/>
|
||||
</cfif>
|
||||
|
||||
<cfif listFindNoCase("read,pass,save", ATTRIBUTES.mode)>
|
||||
<cfset processingMode=ATTRIBUTES.mode/>
|
||||
|
||||
<cfelse>
|
||||
|
||||
<!--- ищем команду принудительного перечитывания --->
|
||||
<cfset doRead = false/>
|
||||
<cfloop list=#ATTRIBUTES.readmarker# index="marker">
|
||||
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
|
||||
<cfset doRead = true/>
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
|
||||
<!--- ищем команду сохранения --->
|
||||
<cfset doSave = false/>
|
||||
<cfif !doRead AND !ATTRIBUTES.readonly>
|
||||
<cfloop list=#ATTRIBUTES.savemarker# index="marker">
|
||||
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
|
||||
<cfset doSave = true/>
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<!--- ищем команду проброса --->
|
||||
<cfset doPass = false/>
|
||||
<cfif !doRead AND !doSave>
|
||||
<cfloop list=#ATTRIBUTES.passmarker# index="marker">
|
||||
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
|
||||
<cfset doPass = true/>
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<!--- определяем режим --->
|
||||
<cfif doRead>
|
||||
<cfset processingMode="read"/>
|
||||
<cfelseif doSave>
|
||||
<cfset processingMode="save"/>
|
||||
<cfelse>
|
||||
<cfif doPass OR !recordExists>
|
||||
<cfset processingMode="pass"/>
|
||||
<cfelse>
|
||||
<cfset processingMode="read"/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
</cfif>
|
||||
|
||||
<cfset initialProcessingMode = processingMode/>
|
||||
|
||||
|
||||
<!---debug <cfoutput>processingMode:#processingMode#</cfoutput>--->
|
||||
<!--- ---------------------------------------------------------- --->
|
||||
|
||||
<!---*** Необходимость вставки (против апдейта) определяется по существованию записи. Это небезопасно, поскольку наличие записи с ключом -1 может прекратить создание новых записей. Конечно, можно добавить констрейнт на ключевое поле, но это лишняя зависимость.
|
||||
Можно явно передавать команду вставки, а не заморачиваться с анализом ключа. Просто традиционно мы сигнализировали необходимость вставки, передавая невалидное значение ключа, обычно -1. Для целочисленных ключей можно было бы просто проверять положительность (что всю жизнь отлично работало), но хочется более общего.
|
||||
--->
|
||||
|
||||
<cfif processingMode IS "save"> <!--- validation lookup --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
|
||||
<cfif !param.key>
|
||||
<cfif param.warningState>
|
||||
<cfif NOT warningState><!--- catch the first fault --->
|
||||
<cfset warningMessage=param.warningMessage />
|
||||
</cfif>
|
||||
<cfset warningState="true" />
|
||||
</cfif>
|
||||
|
||||
<cfif param.errorState>
|
||||
<cfif NOT errorState><!--- catch the first fault --->
|
||||
<cfset errorMessage=param.errorMessage />
|
||||
</cfif>
|
||||
<cfset errorState="true" />
|
||||
<cfset validationOk="false"/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<cfif errorState>
|
||||
<!--- clear warning state --->
|
||||
<cfset warningMessage="" />
|
||||
<cfset warningState=false />
|
||||
<!--- finish lookup --->
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
|
||||
</cfloop>
|
||||
|
||||
<cfif validationOk>
|
||||
<!---<cfdump var=#param#/> --->
|
||||
<cftry>
|
||||
<cfif recordExists><!--- update. Наличие keyValue гарантировано (уверен) --->
|
||||
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#" result="qSaveResult">
|
||||
update #ATTRIBUTES.table# set
|
||||
<cfset i=0 />
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<!--- не обновляем ключевые поля --->
|
||||
<cfif NOT (StructFind(param, "key")
|
||||
OR StructFind(param, "autoincrement")
|
||||
OR StructFind(param, "readonly")
|
||||
OR StructFind(param, "skipUpdate")
|
||||
)><cfif i++ GT 0>,</cfif>
|
||||
#param.field#=<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
where 1=1
|
||||
<cfloop array=#keys# index="key">
|
||||
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#"/>
|
||||
</cfloop>
|
||||
</cfquery>
|
||||
<!---<cfdump var=#qSaveResult#/>--->
|
||||
<cfelse><!--- insert --->
|
||||
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#">
|
||||
insert into #ATTRIBUTES.table#
|
||||
(
|
||||
<cfset i=0 />
|
||||
<!--- ключевые поля без автоинкремента попадают в insert! --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfif NOT (StructFind(param, "autoincrement")
|
||||
OR StructFind(param, "readonly")
|
||||
OR StructFind(param, "skipInsert")
|
||||
)><cfif i++ GT 0>,</cfif>
|
||||
#param.field#
|
||||
</cfif>
|
||||
</cfloop>
|
||||
)
|
||||
values
|
||||
(
|
||||
<cfset i=0 />
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfif NOT (StructFind(param, "autoincrement")
|
||||
OR StructFind(param, "readonly")
|
||||
OR StructFind(param, "skipInsert")
|
||||
)><cfif i++ GT 0>,</cfif>
|
||||
<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
)
|
||||
<!--- /*MSSQL syntax*/
|
||||
<cfif hasAutoIncrement>
|
||||
select scope_identity() as id;
|
||||
</cfif>
|
||||
--->
|
||||
<!---*** PostgreSQL incompatible hack follows--->
|
||||
<cfif hasAutoIncrement>
|
||||
returning #thistag.params[autoIncrementKeyIndex].field# as id;
|
||||
</cfif>;
|
||||
</cfquery>
|
||||
<!--- <cfdump var=#qSave#/> --->
|
||||
<cfif hasAutoIncrement>
|
||||
<cfset keys[autoIncrementKeyIndex].value=qSave.id/>
|
||||
</cfif>
|
||||
<!--- здесь можно установить recordExists=true, если твердо верить, что вставка прошла, если нет исключения --->
|
||||
<cfset recordExists=true/>
|
||||
</cfif>
|
||||
<!--- recordExists --->
|
||||
|
||||
|
||||
<cfset processingMode="read"/>
|
||||
|
||||
<cfcatch type="database">
|
||||
<cfset errorState="true" />
|
||||
<cfset errorMessage="Ошибка при записи в базу данных. #cfcatch.message# #cfcatch.detail#" />
|
||||
<cfset processingMode="pass" />
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
<cfelse><!--- (not) validationOk --->
|
||||
<cfset processingMode="pass"/>
|
||||
</cfif><!--- validationOk --->
|
||||
|
||||
</cfif><!--- processingMode EQ "save" --->
|
||||
|
||||
<!--- экспортируем данные --->
|
||||
<cfswitch expression=#processingMode#>
|
||||
<cfcase value="read">
|
||||
<cfquery name="qRead" datasource="#ATTRIBUTES.datasource#">
|
||||
select
|
||||
<cfset i=0 />
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfset i=i+1/>
|
||||
#param.field#<cfif i LT ArrayLen(thistag.params)>,</cfif>
|
||||
</cfloop>
|
||||
from #ATTRIBUTES.table#
|
||||
where 1=1
|
||||
<cfloop array=#keys# index="key">
|
||||
/*#sqlType(key.type)#*/
|
||||
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
|
||||
</cfloop>
|
||||
</cfquery>
|
||||
|
||||
<cfloop array=#thistag.params# index="param"><!--- *** --->
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", structFind(qRead, #param.field#)) />
|
||||
</cfloop>
|
||||
</cfcase>
|
||||
|
||||
|
||||
<cfcase value="pass"><!--- заполнить все поля. Если значения переданы в форме или урле, тег параметра обязан их разобрать и подставить, в противном случае подставить инит-значения. Инит-значение по умолчанию пустая строка, дефолт по умолчанию тоже пустая строка --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<!--- <cfdump var=#param#/>--->
|
||||
<cfif param.hasInput>
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.submittedValue) /><!---*** дефект: 03.12.2015, передаваемая строкой, при обработке dateFormat неправильно парсится --->
|
||||
<cfelse>
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.init) />
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfcase>
|
||||
|
||||
<cfdefaultcase>
|
||||
</cfdefaultcase>
|
||||
|
||||
</cfswitch>
|
||||
|
||||
<cfif len(ATTRIBUTES.info)><!--- экспортируем дополнительные сведения--->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.submittedValue", #param.submittedValue#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorState", #param.errorState#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorMessage", #param.errorMessage#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningState", #param.warningState#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningMessage", #param.warningMessage#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.null", #param.null#) />
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<cfif len(ATTRIBUTES.meta)><!--- экспортируем метаданные параметра - для инвариантности вызовов --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.param", #param.param#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.field", #param.field#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.type", #param.type#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.displayName", #param.displayName#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.default", #param.default#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.size", #param.size#) />
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(errorState,errorMessage,initialProcessingMode,recordExists))/>
|
||||
|
||||
</cfif><!--- thisTag.ExecutionMode is 'end'--->
|
||||
|
||||
<!--- quick and dirty --->
|
||||
<cffunction name="sqlType">
|
||||
<cfargument name="type"/>
|
||||
|
||||
<cfswitch expression=#type#>
|
||||
<cfcase value="guid,uuid,idstamp"><cfreturn "CF_SQL_OTHER"/></cfcase><!--- hack for PostgreSQL uuid --->
|
||||
<cfdefaultcase><cfreturn uCase("CF_SQL_#type#")/></cfdefaultcase>
|
||||
</cfswitch>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="checkDataFormat">
|
||||
<cfargument name="type"/>
|
||||
<cfargument name="value"/>
|
||||
|
||||
<cfswitch expression=#type#>
|
||||
<cfcase value="integer"><cfreturn isValid("integer",value)/></cfcase>
|
||||
<cfcase value="guid,uuid,idstamp"><cfreturn isValid("guid",value)/></cfcase>
|
||||
<cfdefaultcase><cfreturn true/></cfdefaultcase>
|
||||
</cfswitch>
|
||||
</cffunction>
|
||||
|
||||
|
||||
<!--- //quick and dirty
|
||||
function sqlType(type) {
|
||||
switch (type) {
|
||||
case "guid,idstamp": return "CF_SQL_OTHER"; /*hack for PostgreSQL uuid*/ /*break not needed!*/
|
||||
default: return uCase("CF_SQL_#type#");
|
||||
}
|
||||
}
|
||||
|
||||
function checkDataFormat(type,value) {
|
||||
switch (type) {
|
||||
case "integer": return isValid("integer",value); /*break not needed!*/
|
||||
case "guid,uuid,idstamp": return isValid("guid",value); /*break not needed!*/ /*INCORRECT cfscript switch-case does not support lists*/
|
||||
default: return true;
|
||||
}
|
||||
} --->
|
||||
<cfscript>
|
||||
//readonly может транслироваться в pass!
|
||||
function translateMode(readonly) {
|
||||
if (readonly) {
|
||||
return "read";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
</cfscript>
|
||||
@@ -0,0 +1,400 @@
|
||||
<!---
|
||||
bean v0.95
|
||||
msyu@mail.ru
|
||||
2013-08-29 v0.1
|
||||
2013-09-04 v0.2 tested with CF8 and Railo
|
||||
2013-10-14 added meta attribute - can now export each parameter metadata
|
||||
2014-12-26 POST-GET read-pass-save
|
||||
2015-01-19 read-pass-save, добавлен formmarker, убрано определение режима по POST-GET
|
||||
2015-01-19 убран formmarker, упразднен режим init (используется pass)
|
||||
2015-07-16 добавлен passmarker для перерисовки формы с введенными значениями
|
||||
2015-10-02 поддержка составных ключей
|
||||
2015-10-09 v0.9 если задано value параметра, считается, что он hasInput (корректно работает в составе ключа)
|
||||
2015-11-25 добавлен readmarker, mode (принудительная установка режима)
|
||||
2015-11-26 добавлен mode=skip
|
||||
2016-09-01 изменена трактовка processingMode. Теперь понимается как исходная команда, фиксируется до начала действий. Можно использовать для управления
|
||||
2018-09-16 добавлена ограниченная поддержка нецелых первичных ключей (idstamp)
|
||||
child tag: param
|
||||
to save - should send parameter with name from list ATTRIBUTES.savemarker (value doesn't matter) in FORM, e.g. URL.save="trtrtr", while ATTRIBUTES.savemarker="save,saveAndClose"; for multiple forms on a page (and bean tags) different savemarkers should be used
|
||||
2022-01-06 PostgreSQL (MSSQL incompatibility because of scope_identity)
|
||||
2024-04-13 PostgreSQL GUID, autoincrement by RETURNING
|
||||
--->
|
||||
|
||||
<!---
|
||||
Автогенерируемое поле GUID можно заполнять несколькими способами
|
||||
- как default или init bean/param, но это не очень удобно, потому что может потеряться удобный признак новой записи - инвалидный ключ, и сейчас init-default взаимодействуют некорректно, по крайней мере, неочевидно
|
||||
- генерируемое в БД (например, как default). Для PostgreSQL значение возвращается через Returning, как это делать для других БД, непонятно.
|
||||
- можно генерировать при сохранении в bean, но для этого нужно расширение синтаксиса, зато не зависит от БД (*** TODO)
|
||||
--->
|
||||
|
||||
<cfif thisTag.ExecutionMode is 'end'>
|
||||
<cfparam name="ATTRIBUTES.readmarker" type="string" default="read"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда перечитывания. Имеет приоритет перед savemarker, passmarker. Можно передавать несколько вариантов списком через запятую. --->
|
||||
<cfparam name="ATTRIBUTES.savemarker" type="string" default="save,saveAndClose"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда сохранения. Обычно это имя кнопки. Можно передавать несколько вариантов списком через запятую. --->
|
||||
<cfparam name="ATTRIBUTES.passmarker" type="string" default="pass"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда заполнить форму отправленными данными без перечитывания. В форме такое поле обычно есть. Команда сохранения имеет приоритет перед пробросом. Можно передавать несколько вариантов списком через запятую. --->
|
||||
<cfparam name="ATTRIBUTES.datasource" type="string"/>
|
||||
<cfparam name="ATTRIBUTES.table" type="string"/>
|
||||
<cfparam name="ATTRIBUTES.output" type="string"/>
|
||||
<cfparam name="ATTRIBUTES.info" type="string" default=""/>
|
||||
<cfparam name="ATTRIBUTES.meta" type="string" default=""/>
|
||||
<cfparam name="ATTRIBUTES.status" type="string"/>
|
||||
<cfparam name="ATTRIBUTES.readonly" type="boolean" default="No"/>
|
||||
<cfparam name="ATTRIBUTES.mode" type="string" default=""/> <!--- Принудительный режим read|pass|save|skip. Имеет приоритет перед маркерами --->
|
||||
|
||||
<cfif ATTRIBUTES.mode EQ "skip">
|
||||
<!---<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorState", false) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorMessage", "") />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.processingMode", "skip") />--->
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(false,"","skip"))/>
|
||||
<cfexit method="exittag"/>
|
||||
</cfif>
|
||||
|
||||
<cfset validationOk="true" />
|
||||
<cfset errorState="false" />
|
||||
<cfset errorMessage="" />
|
||||
<cfset warningState="false" />
|
||||
<cfset warningMessage="" />
|
||||
<!---<cfset keyField=""/>
|
||||
<cfset keyValue=""/>--->
|
||||
<cfset hasAutoIncrement="false"/>
|
||||
<cfset autoIncrementKeyIndex=0/><!--- сгенерированное значение присваивается первому полю, отмеченному как autoincrement --->
|
||||
<cfset recordExists = "false"/>
|
||||
<cfset keyInvalid = "false"/>
|
||||
|
||||
<!--- разбор метаданных - поиск ключа --->
|
||||
<!--- поддерживаются только простые целочисленные ключи --->
|
||||
<cfset keys=arrayNew(1)/>
|
||||
<!--- *** почему где-то копируем поля param в key, а где-то прямо цикл по параметрам --->
|
||||
<!--- коллекция thistag.params формируется вложенными тегами param --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<!---<cfdump var=#param#/>--->
|
||||
<cfif structFind(param,"key")><!--- not just structKeyExists! Not exisis, OR exists, but FALSE! --->
|
||||
<cfset key = structNew()/>
|
||||
<cfset ArrayAppend(keys, key)/>
|
||||
|
||||
<cfset key.name = param.param />
|
||||
<cfset key.field = param.field />
|
||||
<cfset key.type = param.type />
|
||||
<cfset key.value = param.value /><!--- должно уже быть присвоено --->
|
||||
<cfset key.null = param.null />
|
||||
|
||||
<cfif param.errorState OR !param.hasInput>
|
||||
<cfset keyInvalid="true"/>
|
||||
<cfelse>
|
||||
<cfset keyInvalid=!checkDataFormat(param.type, param.value)/>
|
||||
</cfif>
|
||||
|
||||
<cfif !hasAutoIncrement><!--- take only first field with "autoincrement" attribute--->
|
||||
<cfif structFind(param, "autoincrement")>
|
||||
<cfset hasAutoIncrement = true/>
|
||||
<cfset autoIncrementKeyIndex = arrayLen(keys)/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
</cfif>
|
||||
</cfloop>
|
||||
<!---<cfdump var=#keys#/>--->
|
||||
|
||||
<cfif !keyInvalid>
|
||||
<!--- *** дублируется похожий cfqueryparam, перепутал один раз --->
|
||||
<cfquery name="qCheckExistence" datasource="#ATTRIBUTES.datasource#">
|
||||
select count(*) as cnt
|
||||
from #ATTRIBUTES.table#
|
||||
where 1=1
|
||||
<cfloop array=#keys# index="key">
|
||||
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
|
||||
</cfloop>
|
||||
</cfquery>
|
||||
<cfset recordExists = (qCheckExistence.cnt GT 0)/>
|
||||
<!--- <cfdump var=#qCheckExistence#/> --->
|
||||
<cfelse>
|
||||
<cfset recordExists = "false"/>
|
||||
</cfif>
|
||||
|
||||
<cfif listFindNoCase("read,pass,save", ATTRIBUTES.mode)>
|
||||
<cfset processingMode=ATTRIBUTES.mode/>
|
||||
|
||||
<cfelse>
|
||||
|
||||
<!--- ищем команду принудительного перечитывания --->
|
||||
<cfset doRead = false/>
|
||||
<cfloop list=#ATTRIBUTES.readmarker# index="marker">
|
||||
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
|
||||
<cfset doRead = true/>
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
|
||||
<!--- ищем команду сохранения --->
|
||||
<cfset doSave = false/>
|
||||
<cfif !doRead AND !ATTRIBUTES.readonly>
|
||||
<cfloop list=#ATTRIBUTES.savemarker# index="marker">
|
||||
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
|
||||
<cfset doSave = true/>
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<!--- ищем команду проброса --->
|
||||
<cfset doPass = false/>
|
||||
<cfif !doRead AND !doSave>
|
||||
<cfloop list=#ATTRIBUTES.passmarker# index="marker">
|
||||
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
|
||||
<cfset doPass = true/>
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<!--- определяем режим --->
|
||||
<cfif doRead>
|
||||
<cfset processingMode="read"/>
|
||||
<cfelseif doSave>
|
||||
<cfset processingMode="save"/>
|
||||
<cfelse>
|
||||
<cfif doPass OR !recordExists>
|
||||
<cfset processingMode="pass"/>
|
||||
<cfelse>
|
||||
<cfset processingMode="read"/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
</cfif>
|
||||
|
||||
<cfset initialProcessingMode = processingMode/>
|
||||
|
||||
|
||||
<!---debug <cfoutput>processingMode:#processingMode#</cfoutput>--->
|
||||
<!--- ---------------------------------------------------------- --->
|
||||
|
||||
<!---*** Необходимость вставки (против апдейта) определяется по существованию записи. Это небезопасно, поскольку наличие записи с ключом -1 может прекратить создание новых записей. Конечно, можно добавить констрейнт на ключевое поле, но это лишняя зависимость.
|
||||
Можно явно передавать команду вставки, а не заморачиваться с анализом ключа. Просто традиционно мы сигнализировали необходимость вставки, передавая невалидное значение ключа, обычно -1. Для целочисленных ключей можно было бы просто проверять положительность (что всю жизнь отлично работало), но хочется более общего.
|
||||
--->
|
||||
|
||||
<cfif processingMode IS "save"> <!--- validation lookup --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
|
||||
<cfif !param.key>
|
||||
<cfif param.warningState>
|
||||
<cfif NOT warningState><!--- catch the first fault --->
|
||||
<cfset warningMessage=param.warningMessage />
|
||||
</cfif>
|
||||
<cfset warningState="true" />
|
||||
</cfif>
|
||||
|
||||
<cfif param.errorState>
|
||||
<cfif NOT errorState><!--- catch the first fault --->
|
||||
<cfset errorMessage=param.errorMessage />
|
||||
</cfif>
|
||||
<cfset errorState="true" />
|
||||
<cfset validationOk="false"/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<cfif errorState>
|
||||
<!--- clear warning state --->
|
||||
<cfset warningMessage="" />
|
||||
<cfset warningState=false />
|
||||
<!--- finish lookup --->
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
|
||||
</cfloop>
|
||||
|
||||
<cfif validationOk>
|
||||
<!---<cfdump var=#param#/> --->
|
||||
<cftry>
|
||||
<cfif recordExists><!--- update. Наличие keyValue гарантировано (уверен) --->
|
||||
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#" result="qSaveResult">
|
||||
update #ATTRIBUTES.table# set
|
||||
<cfset i=0 />
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<!--- не обновляем ключевые поля --->
|
||||
<cfif NOT (StructFind(param, "key")
|
||||
OR StructFind(param, "autoincrement")
|
||||
OR StructFind(param, "readonly")
|
||||
OR StructFind(param, "skipUpdate")
|
||||
)><cfif i++ GT 0>,</cfif>
|
||||
#param.field#=<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
where 1=1
|
||||
<cfloop array=#keys# index="key">
|
||||
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#"/>
|
||||
</cfloop>
|
||||
</cfquery>
|
||||
<!---<cfdump var=#qSaveResult#/>--->
|
||||
<cfelse><!--- insert --->
|
||||
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#">
|
||||
insert into #ATTRIBUTES.table#
|
||||
(
|
||||
<cfset i=0 />
|
||||
<!--- ключевые поля без автоинкремента попадают в insert! --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfif NOT (StructFind(param, "autoincrement")
|
||||
OR StructFind(param, "readonly")
|
||||
OR StructFind(param, "skipInsert")
|
||||
)><cfif i++ GT 0>,</cfif>
|
||||
#param.field#
|
||||
</cfif>
|
||||
</cfloop>
|
||||
)
|
||||
values
|
||||
(
|
||||
<cfset i=0 />
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfif NOT (StructFind(param, "autoincrement")
|
||||
OR StructFind(param, "readonly")
|
||||
OR StructFind(param, "skipInsert")
|
||||
)><cfif i++ GT 0>,</cfif>
|
||||
<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
);
|
||||
<!---
|
||||
<cfif hasAutoIncrement>
|
||||
select scope_identity() as id;
|
||||
</cfif>
|
||||
--->
|
||||
<!---
|
||||
*** PostgreSQL incompatible hack
|
||||
--->
|
||||
<cfif hasAutoIncrement>
|
||||
select LASTVAL() as id;
|
||||
</cfif>
|
||||
</cfquery>
|
||||
|
||||
<cfif hasAutoIncrement>
|
||||
<cfset keys[autoIncrementKeyIndex].value=qSave.id/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
<!--- recordExists --->
|
||||
|
||||
|
||||
<cfset processingMode="read"/>
|
||||
|
||||
<cfcatch type="database">
|
||||
<cfset errorState="true" />
|
||||
<cfset errorMessage="Ошибка при записи в базу данных. #cfcatch.message# #cfcatch.detail#" />
|
||||
<cfset processingMode="pass" />
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
<cfelse><!--- (not) validationOk --->
|
||||
<cfset processingMode="pass"/>
|
||||
</cfif><!--- validationOk --->
|
||||
|
||||
</cfif><!--- processingMode EQ "save" --->
|
||||
|
||||
<!--- экспортируем данные --->
|
||||
<cfswitch expression=#processingMode#>
|
||||
<cfcase value="read">
|
||||
<cfquery name="qRead" datasource="#ATTRIBUTES.datasource#">
|
||||
select
|
||||
<cfset i=0 />
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfset i=i+1/>
|
||||
#param.field#<cfif i LT ArrayLen(thistag.params)>,</cfif>
|
||||
</cfloop>
|
||||
from #ATTRIBUTES.table#
|
||||
where 1=1
|
||||
<cfloop array=#keys# index="key">
|
||||
/*#sqlType(key.type)#*/
|
||||
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
|
||||
</cfloop>
|
||||
</cfquery>
|
||||
|
||||
<cfloop array=#thistag.params# index="param"><!--- *** --->
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", structFind(qRead, #param.field#)) />
|
||||
</cfloop>
|
||||
</cfcase>
|
||||
|
||||
|
||||
<cfcase value="pass"><!--- заполнить все поля. Если значения переданы в форме или урле, тег параметра обязан их разобрать и подставить, в противном случае подставить инит-значения. Инит-значение по умолчанию пустая строка, дефолт по умолчанию тоже пустая строка --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<!--- <cfdump var=#param#/>--->
|
||||
<cfif param.hasInput>
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.submittedValue) /><!---*** дефект: 03.12.2015, передаваемая строкой, при обработке dateFormat неправильно парсится --->
|
||||
<cfelse>
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.init) />
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfcase>
|
||||
|
||||
<cfdefaultcase>
|
||||
</cfdefaultcase>
|
||||
|
||||
</cfswitch>
|
||||
|
||||
<cfif len(ATTRIBUTES.info)><!--- экспортируем дополнительные сведения--->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.submittedValue", #param.submittedValue#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorState", #param.errorState#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorMessage", #param.errorMessage#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningState", #param.warningState#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningMessage", #param.warningMessage#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.null", #param.null#) />
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<cfif len(ATTRIBUTES.meta)><!--- экспортируем метаданные параметра - для инвариантности вызовов --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.param", #param.param#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.field", #param.field#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.type", #param.type#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.displayName", #param.displayName#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.default", #param.default#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.size", #param.size#) />
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(errorState,errorMessage,initialProcessingMode))/>
|
||||
|
||||
</cfif><!--- thisTag.ExecutionMode is 'end'--->
|
||||
|
||||
<!--- quick and dirty --->
|
||||
<cffunction name="sqlType">
|
||||
<cfargument name="type"/>
|
||||
|
||||
<cfswitch expression=#type#>
|
||||
<cfcase value="guid,uuid,idstamp"><cfreturn "CF_SQL_OTHER"/></cfcase><!--- hack for PostgreSQL uuid --->
|
||||
<cfdefaultcase><cfreturn uCase("CF_SQL_#type#")/></cfdefaultcase>
|
||||
</cfswitch>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="checkDataFormat">
|
||||
<cfargument name="type"/>
|
||||
<cfargument name="value"/>
|
||||
|
||||
<cfswitch expression=#type#>
|
||||
<cfcase value="integer"><cfreturn isValid("integer",value)/></cfcase>
|
||||
<cfcase value="guid,uuid,idstamp"><cfreturn isValid("guid",value)/></cfcase>
|
||||
<cfdefaultcase><cfreturn true/></cfdefaultcase>
|
||||
</cfswitch>
|
||||
</cffunction>
|
||||
|
||||
|
||||
<!--- //quick and dirty
|
||||
function sqlType(type) {
|
||||
switch (type) {
|
||||
case "guid,idstamp": return "CF_SQL_OTHER"; /*hack for PostgreSQL uuid*/ /*break not needed!*/
|
||||
default: return uCase("CF_SQL_#type#");
|
||||
}
|
||||
}
|
||||
|
||||
function checkDataFormat(type,value) {
|
||||
switch (type) {
|
||||
case "integer": return isValid("integer",value); /*break not needed!*/
|
||||
case "guid,uuid,idstamp": return isValid("guid",value); /*break not needed!*/ /*INCORRECT cfscript switch-case does not support lists*/
|
||||
default: return true;
|
||||
}
|
||||
} --->
|
||||
<cfscript>
|
||||
//readonly может транслироваться в pass!
|
||||
function translateMode(readonly) {
|
||||
if (readonly) {
|
||||
return "read";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
</cfscript>
|
||||
@@ -0,0 +1,398 @@
|
||||
<!---
|
||||
bean v0.95
|
||||
msyu@mail.ru
|
||||
2013-08-29 v0.1
|
||||
2013-09-04 v0.2 tested with CF8 and Railo
|
||||
2013-10-14 added meta attribute - can now export each parameter metadata
|
||||
2014-12-26 POST-GET read-pass-save
|
||||
2015-01-19 read-pass-save, добавлен formmarker, убрано определение режима по POST-GET
|
||||
2015-01-19 убран formmarker, упразднен режим init (используется pass)
|
||||
2015-07-16 добавлен passmarker для перерисовки формы с введенными значениями
|
||||
2015-10-02 поддержка составных ключей
|
||||
2015-10-09 v0.9 если задано value параметра, считается, что он hasInput (корректно работает в составе ключа)
|
||||
2015-11-25 добавлен readmarker, mode (принудительная установка режима)
|
||||
2015-11-26 добавлен mode=skip
|
||||
2016-09-01 изменена трактовка processingMode. Теперь понимается как исходная команда, фиксируется до начала действий. Можно использовать для управления
|
||||
2018-09-16 добавлена ограниченная поддержка нецелых первичных ключей (idstamp)
|
||||
child tag: param
|
||||
to save - should send parameter with name from list ATTRIBUTES.savemarker (value doesn't matter) in FORM, e.g. URL.save="trtrtr", while ATTRIBUTES.savemarker="save,saveAndClose"; for multiple forms on a page (and bean tags) different savemarkers should be used
|
||||
2022-01-06 PostgreSQL (MSSQL incompatibility because of scope_identity)
|
||||
2024-04-13 PostgreSQL GUID, autoincrement by RETURNING
|
||||
--->
|
||||
|
||||
<!---
|
||||
Автогенерируемое поле GUID можно заполнять несколькими способами
|
||||
- как default или init bean/param, но это не очень удобно, потому что может потеряться удобный признак новой записи - инвалидный ключ, и сейчас init-default взаимодействуют некорректно, по крайней мере, неочевидно
|
||||
- генерируемое в БД (например, как default). Для PostgreSQL значение возвращается через Returning, как это делать для других БД, непонятно.
|
||||
- можно генерировать при сохранении в bean, но для этого нужно расширение синтаксиса, зато не зависит от БД (*** TODO)
|
||||
--->
|
||||
|
||||
<cfif thisTag.ExecutionMode is 'end'>
|
||||
<cfparam name="ATTRIBUTES.readmarker" type="string" default="read"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда перечитывания. Имеет приоритет перед savemarker, passmarker. Можно передавать несколько вариантов списком через запятую. --->
|
||||
<cfparam name="ATTRIBUTES.savemarker" type="string" default="save,saveAndClose"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда сохранения. Обычно это имя кнопки. Можно передавать несколько вариантов списком через запятую. --->
|
||||
<cfparam name="ATTRIBUTES.passmarker" type="string" default="pass"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда заполнить форму отправленными данными без перечитывания. В форме такое поле обычно есть. Команда сохранения имеет приоритет перед пробросом. Можно передавать несколько вариантов списком через запятую. --->
|
||||
<cfparam name="ATTRIBUTES.datasource" type="string"/>
|
||||
<cfparam name="ATTRIBUTES.table" type="string"/>
|
||||
<cfparam name="ATTRIBUTES.output" type="string"/>
|
||||
<cfparam name="ATTRIBUTES.info" type="string" default=""/>
|
||||
<cfparam name="ATTRIBUTES.meta" type="string" default=""/>
|
||||
<cfparam name="ATTRIBUTES.status" type="string"/>
|
||||
<cfparam name="ATTRIBUTES.readonly" type="boolean" default="No"/>
|
||||
<cfparam name="ATTRIBUTES.mode" type="string" default=""/> <!--- Принудительный режим read|pass|save|skip. Имеет приоритет перед маркерами --->
|
||||
|
||||
<cfif ATTRIBUTES.mode EQ "skip">
|
||||
<!---<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorState", false) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorMessage", "") />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.processingMode", "skip") />--->
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(false,"","skip"))/>
|
||||
<cfexit method="exittag"/>
|
||||
</cfif>
|
||||
|
||||
<cfset validationOk="true" />
|
||||
<cfset errorState="false" />
|
||||
<cfset errorMessage="" />
|
||||
<cfset warningState="false" />
|
||||
<cfset warningMessage="" />
|
||||
<!---<cfset keyField=""/>
|
||||
<cfset keyValue=""/>--->
|
||||
<cfset hasAutoIncrement="false"/>
|
||||
<cfset autoIncrementKeyIndex=0/><!--- сгенерированное значение присваивается первому полю, отмеченному как autoincrement --->
|
||||
<cfset recordExists = "false"/>
|
||||
<cfset keyInvalid = "false"/>
|
||||
|
||||
<!--- разбор метаданных - поиск ключа --->
|
||||
<!--- поддерживаются только простые целочисленные ключи --->
|
||||
<cfset keys=arrayNew(1)/>
|
||||
<!--- *** почему где-то копируем поля param в key, а где-то прямо цикл по параметрам --->
|
||||
<!--- коллекция thistag.params формируется вложенными тегами param --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<!---<cfdump var=#param#/>--->
|
||||
<cfif structFind(param,"key")><!--- not just structKeyExists! Not exisis, OR exists, but FALSE! --->
|
||||
<cfset key = structNew()/>
|
||||
<cfset ArrayAppend(keys, key)/>
|
||||
|
||||
<cfset key.name = param.param />
|
||||
<cfset key.field = param.field />
|
||||
<cfset key.type = param.type />
|
||||
<cfset key.value = param.value /><!--- должно уже быть присвоено --->
|
||||
<cfset key.null = param.null />
|
||||
|
||||
<cfif param.errorState OR !param.hasInput>
|
||||
<cfset keyInvalid="true"/>
|
||||
<cfelse>
|
||||
<cfset keyInvalid=!checkDataFormat(param.type, param.value)/>
|
||||
</cfif>
|
||||
|
||||
<cfif !hasAutoIncrement><!--- take only first field with "autoincrement" attribute--->
|
||||
<cfif structFind(param, "autoincrement")>
|
||||
<cfset hasAutoIncrement = true/>
|
||||
<cfset autoIncrementKeyIndex = arrayLen(keys)/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
</cfif>
|
||||
</cfloop>
|
||||
<!---<cfdump var=#keys#/>--->
|
||||
|
||||
<cfif !keyInvalid>
|
||||
<!--- *** дублируется похожий cfqueryparam, перепутал один раз --->
|
||||
<cfquery name="qCheckExistence" datasource="#ATTRIBUTES.datasource#">
|
||||
select count(*) as cnt
|
||||
from #ATTRIBUTES.table#
|
||||
where 1=1
|
||||
<cfloop array=#keys# index="key">
|
||||
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
|
||||
</cfloop>
|
||||
</cfquery>
|
||||
<cfset recordExists = (qCheckExistence.cnt GT 0)/>
|
||||
<!--- <cfdump var=#qCheckExistence#/> --->
|
||||
<cfelse>
|
||||
<cfset recordExists = "false"/>
|
||||
</cfif>
|
||||
|
||||
<cfif listFindNoCase("read,pass,save", ATTRIBUTES.mode)>
|
||||
<cfset processingMode=ATTRIBUTES.mode/>
|
||||
|
||||
<cfelse>
|
||||
|
||||
<!--- ищем команду принудительного перечитывания --->
|
||||
<cfset doRead = false/>
|
||||
<cfloop list=#ATTRIBUTES.readmarker# index="marker">
|
||||
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
|
||||
<cfset doRead = true/>
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
|
||||
<!--- ищем команду сохранения --->
|
||||
<cfset doSave = false/>
|
||||
<cfif !doRead AND !ATTRIBUTES.readonly>
|
||||
<cfloop list=#ATTRIBUTES.savemarker# index="marker">
|
||||
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
|
||||
<cfset doSave = true/>
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<!--- ищем команду проброса --->
|
||||
<cfset doPass = false/>
|
||||
<cfif !doRead AND !doSave>
|
||||
<cfloop list=#ATTRIBUTES.passmarker# index="marker">
|
||||
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
|
||||
<cfset doPass = true/>
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<!--- определяем режим --->
|
||||
<cfif doRead>
|
||||
<cfset processingMode="read"/>
|
||||
<cfelseif doSave>
|
||||
<cfset processingMode="save"/>
|
||||
<cfelse>
|
||||
<cfif doPass OR !recordExists>
|
||||
<cfset processingMode="pass"/>
|
||||
<cfelse>
|
||||
<cfset processingMode="read"/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
</cfif>
|
||||
|
||||
<cfset initialProcessingMode = processingMode/>
|
||||
|
||||
|
||||
<!---debug <cfoutput>processingMode:#processingMode#</cfoutput>--->
|
||||
<!--- ---------------------------------------------------------- --->
|
||||
|
||||
<!---*** Необходимость вставки (против апдейта) определяется по существованию записи. Это небезопасно, поскольку наличие записи с ключом -1 может прекратить создание новых записей. Конечно, можно добавить констрейнт на ключевое поле, но это лишняя зависимость.
|
||||
Можно явно передавать команду вставки, а не заморачиваться с анализом ключа. Просто традиционно мы сигнализировали необходимость вставки, передавая невалидное значение ключа, обычно -1. Для целочисленных ключей можно было бы просто проверять положительность (что всю жизнь отлично работало), но хочется более общего.
|
||||
--->
|
||||
|
||||
<cfif processingMode IS "save"> <!--- validation lookup --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
|
||||
<cfif !param.key>
|
||||
<cfif param.warningState>
|
||||
<cfif NOT warningState><!--- catch the first fault --->
|
||||
<cfset warningMessage=param.warningMessage />
|
||||
</cfif>
|
||||
<cfset warningState="true" />
|
||||
</cfif>
|
||||
|
||||
<cfif param.errorState>
|
||||
<cfif NOT errorState><!--- catch the first fault --->
|
||||
<cfset errorMessage=param.errorMessage />
|
||||
</cfif>
|
||||
<cfset errorState="true" />
|
||||
<cfset validationOk="false"/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<cfif errorState>
|
||||
<!--- clear warning state --->
|
||||
<cfset warningMessage="" />
|
||||
<cfset warningState=false />
|
||||
<!--- finish lookup --->
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
|
||||
</cfloop>
|
||||
|
||||
<cfif validationOk>
|
||||
<!---<cfdump var=#param#/> --->
|
||||
<cftry>
|
||||
<cfif recordExists><!--- update. Наличие keyValue гарантировано (уверен) --->
|
||||
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#" result="qSaveResult">
|
||||
update #ATTRIBUTES.table# set
|
||||
<cfset i=0 />
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<!--- не обновляем ключевые поля --->
|
||||
<cfif NOT (StructFind(param, "key")
|
||||
OR StructFind(param, "autoincrement")
|
||||
OR StructFind(param, "readonly")
|
||||
OR StructFind(param, "skipUpdate")
|
||||
)><cfif i++ GT 0>,</cfif>
|
||||
#param.field#=<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
where 1=1
|
||||
<cfloop array=#keys# index="key">
|
||||
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#"/>
|
||||
</cfloop>
|
||||
</cfquery>
|
||||
<!---<cfdump var=#qSaveResult#/>--->
|
||||
<cfelse><!--- insert --->
|
||||
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#">
|
||||
insert into #ATTRIBUTES.table#
|
||||
(
|
||||
<cfset i=0 />
|
||||
<!--- ключевые поля без автоинкремента попадают в insert! --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfif NOT (StructFind(param, "autoincrement")
|
||||
OR StructFind(param, "readonly")
|
||||
OR StructFind(param, "skipInsert")
|
||||
)><cfif i++ GT 0>,</cfif>
|
||||
#param.field#
|
||||
</cfif>
|
||||
</cfloop>
|
||||
)
|
||||
values
|
||||
(
|
||||
<cfset i=0 />
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfif NOT (StructFind(param, "autoincrement")
|
||||
OR StructFind(param, "readonly")
|
||||
OR StructFind(param, "skipInsert")
|
||||
)><cfif i++ GT 0>,</cfif>
|
||||
<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
)
|
||||
<!---
|
||||
<cfif hasAutoIncrement>
|
||||
select scope_identity() as id;
|
||||
</cfif>
|
||||
--->
|
||||
<!---*** PostgreSQL incompatible hack follows--->
|
||||
<cfif hasAutoIncrement>
|
||||
returning #thistag.params[autoIncrementKeyIndex].field# as id;
|
||||
</cfif>;
|
||||
</cfquery>
|
||||
<!--- <cfdump var=#qSave#/> --->
|
||||
<cfif hasAutoIncrement>
|
||||
<cfset keys[autoIncrementKeyIndex].value=qSave.id/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
<!--- recordExists --->
|
||||
|
||||
|
||||
<cfset processingMode="read"/>
|
||||
|
||||
<cfcatch type="database">
|
||||
<cfset errorState="true" />
|
||||
<cfset errorMessage="Ошибка при записи в базу данных. #cfcatch.message# #cfcatch.detail#" />
|
||||
<cfset processingMode="pass" />
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
<cfelse><!--- (not) validationOk --->
|
||||
<cfset processingMode="pass"/>
|
||||
</cfif><!--- validationOk --->
|
||||
|
||||
</cfif><!--- processingMode EQ "save" --->
|
||||
|
||||
<!--- экспортируем данные --->
|
||||
<cfswitch expression=#processingMode#>
|
||||
<cfcase value="read">
|
||||
<cfquery name="qRead" datasource="#ATTRIBUTES.datasource#">
|
||||
select
|
||||
<cfset i=0 />
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfset i=i+1/>
|
||||
#param.field#<cfif i LT ArrayLen(thistag.params)>,</cfif>
|
||||
</cfloop>
|
||||
from #ATTRIBUTES.table#
|
||||
where 1=1
|
||||
<cfloop array=#keys# index="key">
|
||||
/*#sqlType(key.type)#*/
|
||||
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
|
||||
</cfloop>
|
||||
</cfquery>
|
||||
|
||||
<cfloop array=#thistag.params# index="param"><!--- *** --->
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", structFind(qRead, #param.field#)) />
|
||||
</cfloop>
|
||||
</cfcase>
|
||||
|
||||
|
||||
<cfcase value="pass"><!--- заполнить все поля. Если значения переданы в форме или урле, тег параметра обязан их разобрать и подставить, в противном случае подставить инит-значения. Инит-значение по умолчанию пустая строка, дефолт по умолчанию тоже пустая строка --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<!--- <cfdump var=#param#/>--->
|
||||
<cfif param.hasInput>
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.submittedValue) /><!---*** дефект: 03.12.2015, передаваемая строкой, при обработке dateFormat неправильно парсится --->
|
||||
<cfelse>
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.init) />
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfcase>
|
||||
|
||||
<cfdefaultcase>
|
||||
</cfdefaultcase>
|
||||
|
||||
</cfswitch>
|
||||
|
||||
<cfif len(ATTRIBUTES.info)><!--- экспортируем дополнительные сведения--->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.submittedValue", #param.submittedValue#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorState", #param.errorState#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorMessage", #param.errorMessage#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningState", #param.warningState#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningMessage", #param.warningMessage#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.null", #param.null#) />
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<cfif len(ATTRIBUTES.meta)><!--- экспортируем метаданные параметра - для инвариантности вызовов --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.param", #param.param#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.field", #param.field#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.type", #param.type#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.displayName", #param.displayName#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.default", #param.default#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.size", #param.size#) />
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(errorState,errorMessage,initialProcessingMode))/>
|
||||
|
||||
</cfif><!--- thisTag.ExecutionMode is 'end'--->
|
||||
|
||||
<!--- quick and dirty --->
|
||||
<cffunction name="sqlType">
|
||||
<cfargument name="type"/>
|
||||
|
||||
<cfswitch expression=#type#>
|
||||
<cfcase value="guid,uuid,idstamp"><cfreturn "CF_SQL_OTHER"/></cfcase><!--- hack for PostgreSQL uuid --->
|
||||
<cfdefaultcase><cfreturn uCase("CF_SQL_#type#")/></cfdefaultcase>
|
||||
</cfswitch>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="checkDataFormat">
|
||||
<cfargument name="type"/>
|
||||
<cfargument name="value"/>
|
||||
|
||||
<cfswitch expression=#type#>
|
||||
<cfcase value="integer"><cfreturn isValid("integer",value)/></cfcase>
|
||||
<cfcase value="guid,uuid,idstamp"><cfreturn isValid("guid",value)/></cfcase>
|
||||
<cfdefaultcase><cfreturn true/></cfdefaultcase>
|
||||
</cfswitch>
|
||||
</cffunction>
|
||||
|
||||
|
||||
<!--- //quick and dirty
|
||||
function sqlType(type) {
|
||||
switch (type) {
|
||||
case "guid,idstamp": return "CF_SQL_OTHER"; /*hack for PostgreSQL uuid*/ /*break not needed!*/
|
||||
default: return uCase("CF_SQL_#type#");
|
||||
}
|
||||
}
|
||||
|
||||
function checkDataFormat(type,value) {
|
||||
switch (type) {
|
||||
case "integer": return isValid("integer",value); /*break not needed!*/
|
||||
case "guid,uuid,idstamp": return isValid("guid",value); /*break not needed!*/ /*INCORRECT cfscript switch-case does not support lists*/
|
||||
default: return true;
|
||||
}
|
||||
} --->
|
||||
<cfscript>
|
||||
//readonly может транслироваться в pass!
|
||||
function translateMode(readonly) {
|
||||
if (readonly) {
|
||||
return "read";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
</cfscript>
|
||||
@@ -0,0 +1,398 @@
|
||||
<!---
|
||||
bean v0.95
|
||||
msyu@mail.ru
|
||||
2013-08-29 v0.1
|
||||
2013-09-04 v0.2 tested with CF8 and Railo
|
||||
2013-10-14 added meta attribute - can now export each parameter metadata
|
||||
2014-12-26 POST-GET read-pass-save
|
||||
2015-01-19 read-pass-save, добавлен formmarker, убрано определение режима по POST-GET
|
||||
2015-01-19 убран formmarker, упразднен режим init (используется pass)
|
||||
2015-07-16 добавлен passmarker для перерисовки формы с введенными значениями
|
||||
2015-10-02 поддержка составных ключей
|
||||
2015-10-09 v0.9 если задано value параметра, считается, что он hasInput (корректно работает в составе ключа)
|
||||
2015-11-25 добавлен readmarker, mode (принудительная установка режима)
|
||||
2015-11-26 добавлен mode=skip
|
||||
2016-09-01 изменена трактовка processingMode. Теперь понимается как исходная команда, фиксируется до начала действий. Можно использовать для управления
|
||||
2018-09-16 добавлена ограниченная поддержка нецелых первичных ключей (idstamp)
|
||||
child tag: param
|
||||
to save - should send parameter with name from list ATTRIBUTES.savemarker (value doesn't matter) in FORM, e.g. URL.save="trtrtr", while ATTRIBUTES.savemarker="save,saveAndClose"; for multiple forms on a page (and bean tags) different savemarkers should be used
|
||||
2022-01-06 PostgreSQL (MSSQL incompatibility because of scope_identity)
|
||||
2024-04-13 PostgreSQL GUID, autoincrement by RETURNING
|
||||
--->
|
||||
|
||||
<!---
|
||||
Автогенерируемое поле GUID можно заполнять несколькими способами
|
||||
- как default или init bean/param, но это не очень удобно, потому что может потеряться удобный признак новой записи - инвалидный ключ, и сейчас init-default взаимодействуют некорректно, по крайней мере, неочевидно
|
||||
- генерируемое в БД (например, как default). Для PostgreSQL значение возвращается через Returning, как это делать для других БД, непонятно.
|
||||
- можно генерировать при сохранении в bean, но для этого нужно расширение синтаксиса, зато не зависит от БД (*** TODO)
|
||||
--->
|
||||
|
||||
<cfif thisTag.ExecutionMode is 'end'>
|
||||
<cfparam name="ATTRIBUTES.readmarker" type="string" default="read"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда перечитывания. Имеет приоритет перед savemarker, passmarker. Можно передавать несколько вариантов списком через запятую. --->
|
||||
<cfparam name="ATTRIBUTES.savemarker" type="string" default="save,saveAndClose"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда сохранения. Обычно это имя кнопки. Можно передавать несколько вариантов списком через запятую. --->
|
||||
<cfparam name="ATTRIBUTES.passmarker" type="string" default="pass"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда заполнить форму отправленными данными без перечитывания. В форме такое поле обычно есть. Команда сохранения имеет приоритет перед пробросом. Можно передавать несколько вариантов списком через запятую. --->
|
||||
<cfparam name="ATTRIBUTES.datasource" type="string"/>
|
||||
<cfparam name="ATTRIBUTES.table" type="string"/>
|
||||
<cfparam name="ATTRIBUTES.output" type="string"/>
|
||||
<cfparam name="ATTRIBUTES.info" type="string" default=""/>
|
||||
<cfparam name="ATTRIBUTES.meta" type="string" default=""/>
|
||||
<cfparam name="ATTRIBUTES.status" type="string"/>
|
||||
<cfparam name="ATTRIBUTES.readonly" type="boolean" default="No"/>
|
||||
<cfparam name="ATTRIBUTES.mode" type="string" default=""/> <!--- Принудительный режим read|pass|save|skip. Имеет приоритет перед маркерами --->
|
||||
|
||||
<cfif ATTRIBUTES.mode EQ "skip">
|
||||
<!---<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorState", false) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorMessage", "") />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.processingMode", "skip") />--->
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(false,"","skip"))/>
|
||||
<cfexit method="exittag"/>
|
||||
</cfif>
|
||||
|
||||
<cfset validationOk="true" />
|
||||
<cfset errorState="false" />
|
||||
<cfset errorMessage="" />
|
||||
<cfset warningState="false" />
|
||||
<cfset warningMessage="" />
|
||||
<!---<cfset keyField=""/>
|
||||
<cfset keyValue=""/>--->
|
||||
<cfset hasAutoIncrement="false"/>
|
||||
<cfset autoIncrementKeyIndex=0/><!--- сгенерированное значение присваивается первому полю, отмеченному как autoincrement --->
|
||||
<cfset recordExists = "false"/>
|
||||
<cfset keyInvalid = "false"/>
|
||||
|
||||
<!--- разбор метаданных - поиск ключа --->
|
||||
<!--- поддерживаются только простые целочисленные ключи --->
|
||||
<cfset keys=arrayNew(1)/>
|
||||
<!--- *** почему где-то копируем поля param в key, а где-то прямо цикл по параметрам --->
|
||||
<!--- коллекция thistag.params формируется вложенными тегами param --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<!---<cfdump var=#param#/>--->
|
||||
<cfif structFind(param,"key")><!--- not just structKeyExists! Not exisis, OR exists, but FALSE! --->
|
||||
<cfset key = structNew()/>
|
||||
<cfset ArrayAppend(keys, key)/>
|
||||
|
||||
<cfset key.name = param.param />
|
||||
<cfset key.field = param.field />
|
||||
<cfset key.type = param.type />
|
||||
<cfset key.value = param.value /><!--- должно уже быть присвоено --->
|
||||
<cfset key.null = param.null />
|
||||
|
||||
<cfif param.errorState OR !param.hasInput>
|
||||
<cfset keyInvalid="true"/>
|
||||
<cfelse>
|
||||
<cfset keyInvalid=!checkDataFormat(param.type, param.value)/>
|
||||
</cfif>
|
||||
|
||||
<cfif !hasAutoIncrement><!--- take only first field with "autoincrement" attribute--->
|
||||
<cfif structFind(param, "autoincrement")>
|
||||
<cfset hasAutoIncrement = true/>
|
||||
<cfset autoIncrementKeyIndex = arrayLen(keys)/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
</cfif>
|
||||
</cfloop>
|
||||
<!---<cfdump var=#keys#/>--->
|
||||
|
||||
<cfif !keyInvalid>
|
||||
<!--- *** дублируется похожий cfqueryparam, перепутал один раз --->
|
||||
<cfquery name="qCheckExistence" datasource="#ATTRIBUTES.datasource#">
|
||||
select count(*) as cnt
|
||||
from #ATTRIBUTES.table#
|
||||
where 1=1
|
||||
<!--- <cfloop array=#keys# index="key">
|
||||
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
|
||||
</cfloop> --->
|
||||
</cfquery>
|
||||
<cfset recordExists = (qCheckExistence.cnt GT 0)/>
|
||||
<!--- <cfdump var=#qCheckExistence#/> --->
|
||||
<cfelse>
|
||||
<cfset recordExists = "false"/>
|
||||
</cfif>
|
||||
|
||||
<cfif listFindNoCase("read,pass,save", ATTRIBUTES.mode)>
|
||||
<cfset processingMode=ATTRIBUTES.mode/>
|
||||
|
||||
<cfelse>
|
||||
|
||||
<!--- ищем команду принудительного перечитывания --->
|
||||
<cfset doRead = false/>
|
||||
<cfloop list=#ATTRIBUTES.readmarker# index="marker">
|
||||
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
|
||||
<cfset doRead = true/>
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
|
||||
<!--- ищем команду сохранения --->
|
||||
<cfset doSave = false/>
|
||||
<cfif !doRead AND !ATTRIBUTES.readonly>
|
||||
<cfloop list=#ATTRIBUTES.savemarker# index="marker">
|
||||
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
|
||||
<cfset doSave = true/>
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<!--- ищем команду проброса --->
|
||||
<cfset doPass = false/>
|
||||
<cfif !doRead AND !doSave>
|
||||
<cfloop list=#ATTRIBUTES.passmarker# index="marker">
|
||||
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
|
||||
<cfset doPass = true/>
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<!--- определяем режим --->
|
||||
<cfif doRead>
|
||||
<cfset processingMode="read"/>
|
||||
<cfelseif doSave>
|
||||
<cfset processingMode="save"/>
|
||||
<cfelse>
|
||||
<cfif doPass OR !recordExists>
|
||||
<cfset processingMode="pass"/>
|
||||
<cfelse>
|
||||
<cfset processingMode="read"/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
</cfif>
|
||||
|
||||
<cfset initialProcessingMode = processingMode/>
|
||||
|
||||
|
||||
<!---debug <cfoutput>processingMode:#processingMode#</cfoutput>--->
|
||||
<!--- ---------------------------------------------------------- --->
|
||||
|
||||
<!---*** Необходимость вставки (против апдейта) определяется по существованию записи. Это небезопасно, поскольку наличие записи с ключом -1 может прекратить создание новых записей. Конечно, можно добавить констрейнт на ключевое поле, но это лишняя зависимость.
|
||||
Можно явно передавать команду вставки, а не заморачиваться с анализом ключа. Просто традиционно мы сигнализировали необходимость вставки, передавая невалидное значение ключа, обычно -1. Для целочисленных ключей можно было бы просто проверять положительность (что всю жизнь отлично работало), но хочется более общего.
|
||||
--->
|
||||
|
||||
<cfif processingMode IS "save"> <!--- validation lookup --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
|
||||
<cfif !param.key>
|
||||
<cfif param.warningState>
|
||||
<cfif NOT warningState><!--- catch the first fault --->
|
||||
<cfset warningMessage=param.warningMessage />
|
||||
</cfif>
|
||||
<cfset warningState="true" />
|
||||
</cfif>
|
||||
|
||||
<cfif param.errorState>
|
||||
<cfif NOT errorState><!--- catch the first fault --->
|
||||
<cfset errorMessage=param.errorMessage />
|
||||
</cfif>
|
||||
<cfset errorState="true" />
|
||||
<cfset validationOk="false"/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<cfif errorState>
|
||||
<!--- clear warning state --->
|
||||
<cfset warningMessage="" />
|
||||
<cfset warningState=false />
|
||||
<!--- finish lookup --->
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
|
||||
</cfloop>
|
||||
|
||||
<cfif validationOk>
|
||||
<!---<cfdump var=#param#/> --->
|
||||
<cftry>
|
||||
<cfif recordExists><!--- update. Наличие keyValue гарантировано (уверен) --->
|
||||
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#" result="qSaveResult">
|
||||
update #ATTRIBUTES.table# set
|
||||
<cfset i=0 />
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<!--- не обновляем ключевые поля --->
|
||||
<cfif NOT (StructFind(param, "key")
|
||||
OR StructFind(param, "autoincrement")
|
||||
OR StructFind(param, "readonly")
|
||||
OR StructFind(param, "skipUpdate")
|
||||
)><cfif i++ GT 0>,</cfif>
|
||||
#param.field#=<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
where 1=1
|
||||
<cfloop array=#keys# index="key">
|
||||
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#"/>
|
||||
</cfloop>
|
||||
</cfquery>
|
||||
<!---<cfdump var=#qSaveResult#/>--->
|
||||
<cfelse><!--- insert --->
|
||||
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#">
|
||||
insert into #ATTRIBUTES.table#
|
||||
(
|
||||
<cfset i=0 />
|
||||
<!--- ключевые поля без автоинкремента попадают в insert! --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfif NOT (StructFind(param, "autoincrement")
|
||||
OR StructFind(param, "readonly")
|
||||
OR StructFind(param, "skipInsert")
|
||||
)><cfif i++ GT 0>,</cfif>
|
||||
#param.field#
|
||||
</cfif>
|
||||
</cfloop>
|
||||
)
|
||||
values
|
||||
(
|
||||
<cfset i=0 />
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfif NOT (StructFind(param, "autoincrement")
|
||||
OR StructFind(param, "readonly")
|
||||
OR StructFind(param, "skipInsert")
|
||||
)><cfif i++ GT 0>,</cfif>
|
||||
<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
)
|
||||
<!---
|
||||
<cfif hasAutoIncrement>
|
||||
select scope_identity() as id;
|
||||
</cfif>
|
||||
--->
|
||||
<!---*** PostgreSQL incompatible hack follows--->
|
||||
<cfif hasAutoIncrement>
|
||||
returning #thistag.params[autoIncrementKeyIndex].field# as id;
|
||||
</cfif>;
|
||||
</cfquery>
|
||||
<!--- <cfdump var=#qSave#/> --->
|
||||
<cfif hasAutoIncrement>
|
||||
<cfset keys[autoIncrementKeyIndex].value=qSave.id/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
<!--- recordExists --->
|
||||
|
||||
|
||||
<cfset processingMode="read"/>
|
||||
|
||||
<cfcatch type="database">
|
||||
<cfset errorState="true" />
|
||||
<cfset errorMessage="Ошибка при записи в базу данных. #cfcatch.message# #cfcatch.detail#" />
|
||||
<cfset processingMode="pass" />
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
<cfelse><!--- (not) validationOk --->
|
||||
<cfset processingMode="pass"/>
|
||||
</cfif><!--- validationOk --->
|
||||
|
||||
</cfif><!--- processingMode EQ "save" --->
|
||||
|
||||
<!--- экспортируем данные --->
|
||||
<cfswitch expression=#processingMode#>
|
||||
<cfcase value="read">
|
||||
<cfquery name="qRead" datasource="#ATTRIBUTES.datasource#">
|
||||
select
|
||||
<cfset i=0 />
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfset i=i+1/>
|
||||
#param.field#<cfif i LT ArrayLen(thistag.params)>,</cfif>
|
||||
</cfloop>
|
||||
from #ATTRIBUTES.table#
|
||||
where 1=1
|
||||
<cfloop array=#keys# index="key">
|
||||
/*#sqlType(key.type)#*/
|
||||
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
|
||||
</cfloop>
|
||||
</cfquery>
|
||||
|
||||
<cfloop array=#thistag.params# index="param"><!--- *** --->
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", structFind(qRead, #param.field#)) />
|
||||
</cfloop>
|
||||
</cfcase>
|
||||
|
||||
|
||||
<cfcase value="pass"><!--- заполнить все поля. Если значения переданы в форме или урле, тег параметра обязан их разобрать и подставить, в противном случае подставить инит-значения. Инит-значение по умолчанию пустая строка, дефолт по умолчанию тоже пустая строка --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<!--- <cfdump var=#param#/>--->
|
||||
<cfif param.hasInput>
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.submittedValue) /><!---*** дефект: 03.12.2015, передаваемая строкой, при обработке dateFormat неправильно парсится --->
|
||||
<cfelse>
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.init) />
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfcase>
|
||||
|
||||
<cfdefaultcase>
|
||||
</cfdefaultcase>
|
||||
|
||||
</cfswitch>
|
||||
|
||||
<cfif len(ATTRIBUTES.info)><!--- экспортируем дополнительные сведения--->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.submittedValue", #param.submittedValue#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorState", #param.errorState#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorMessage", #param.errorMessage#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningState", #param.warningState#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningMessage", #param.warningMessage#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.null", #param.null#) />
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<cfif len(ATTRIBUTES.meta)><!--- экспортируем метаданные параметра - для инвариантности вызовов --->
|
||||
<cfloop array=#thistag.params# index="param">
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.param", #param.param#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.field", #param.field#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.type", #param.type#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.displayName", #param.displayName#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.default", #param.default#) />
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.size", #param.size#) />
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(errorState,errorMessage,initialProcessingMode))/>
|
||||
|
||||
</cfif><!--- thisTag.ExecutionMode is 'end'--->
|
||||
|
||||
<!--- quick and dirty --->
|
||||
<cffunction name="sqlType">
|
||||
<cfargument name="type"/>
|
||||
|
||||
<cfswitch expression=#type#>
|
||||
<cfcase value="guid,uuid,idstamp"><cfreturn "CF_SQL_OTHER"/></cfcase><!--- hack for PostgreSQL uuid --->
|
||||
<cfdefaultcase><cfreturn uCase("CF_SQL_#type#")/></cfdefaultcase>
|
||||
</cfswitch>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="checkDataFormat">
|
||||
<cfargument name="type"/>
|
||||
<cfargument name="value"/>
|
||||
|
||||
<cfswitch expression=#type#>
|
||||
<cfcase value="integer"><cfreturn isValid("integer",value)/></cfcase>
|
||||
<cfcase value="guid,uuid,idstamp"><cfreturn isValid("guid",value)/></cfcase>
|
||||
<cfdefaultcase><cfreturn true/></cfdefaultcase>
|
||||
</cfswitch>
|
||||
</cffunction>
|
||||
|
||||
|
||||
<!--- //quick and dirty
|
||||
function sqlType(type) {
|
||||
switch (type) {
|
||||
case "guid,idstamp": return "CF_SQL_OTHER"; /*hack for PostgreSQL uuid*/ /*break not needed!*/
|
||||
default: return uCase("CF_SQL_#type#");
|
||||
}
|
||||
}
|
||||
|
||||
function checkDataFormat(type,value) {
|
||||
switch (type) {
|
||||
case "integer": return isValid("integer",value); /*break not needed!*/
|
||||
case "guid,uuid,idstamp": return isValid("guid",value); /*break not needed!*/ /*INCORRECT cfscript switch-case does not support lists*/
|
||||
default: return true;
|
||||
}
|
||||
} --->
|
||||
<cfscript>
|
||||
//readonly может транслироваться в pass!
|
||||
function translateMode(readonly) {
|
||||
if (readonly) {
|
||||
return "read";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
</cfscript>
|
||||
@@ -0,0 +1,24 @@
|
||||
<cfsilent>
|
||||
<!--- 20181209 v0.3 --->
|
||||
<!---v0.3 добавлен атрибут cfSqlType--->
|
||||
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
|
||||
<cfassociate basetag="cf_field_set" datacollection="fieldsArray"/>
|
||||
|
||||
<cfparam name="ATTRIBUTES.expression" default=""/>
|
||||
<cfparam name="ATTRIBUTES.title" default=""/>
|
||||
<cfparam name="ATTRIBUTES.name" default=""/>
|
||||
<cfparam name="ATTRIBUTES.cfSqlType" default="CF_SQL_VARCHAR"/>
|
||||
|
||||
<cfif ATTRIBUTES.expression IS "">
|
||||
<cfset ATTRIBUTES.expression=thisTag.generatedContent/>
|
||||
</cfif>
|
||||
|
||||
<cfif ATTRIBUTES.name IS "">
|
||||
<cfset regex="([[:word:]]+)[[:space:]]*$"/>
|
||||
<cfset matches=REFindNoCase(regex, ATTRIBUTES.expression, 1, true)/>
|
||||
<cfif arrayLen(matches.pos) GT 1>
|
||||
<cfset ATTRIBUTES.name=mid(ATTRIBUTES.expression, matches.pos[2], matches.len[2])/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
</cfif>
|
||||
</cfsilent>
|
||||
@@ -0,0 +1,41 @@
|
||||
<cfsilent>
|
||||
<!--- 20181209 v0.3 --->
|
||||
<!--- Используется в селекте для структурирования списка полей. Выводит вместо себя список через запятую, экспортирует Map со структурами выражение+заголовок, с ключом, соответствующим имени колонки в селекте (ключ вычисляется регулярным выражением) --->
|
||||
<!---v0.3 добавлено поле cfSqlType--->
|
||||
<cfif thisTag.executionMode is "end">
|
||||
|
||||
<cfparam name="ATTRIBUTES.titleMapOut"/>
|
||||
<cfparam name="ATTRIBUTES.lengthOut"/>
|
||||
<cfparam name="ATTRIBUTES.listOut" default=""/>
|
||||
|
||||
<cfparam name="thisTag.fieldsArray" type="array"/><!--- Вложенные теги field отдают сюда свои данные --->
|
||||
<cfset titleMap=structNew()/>
|
||||
<cfset list=""/>
|
||||
<cfset i=0/>
|
||||
<cfloop array=#thisTag.fieldsArray# index="field">
|
||||
<cfset i=i+1/>
|
||||
<cfset list=listAppend(#list#, #field.expression#)/>
|
||||
<cfif len(#field.name#)>
|
||||
<cfset structInsert(#titleMap#, #field.name#, structNew())/>
|
||||
<cfset "titleMap.#field.name#.ordinal"=#i#/>
|
||||
<cfset "titleMap.#field.name#.title"=#field.title#/>
|
||||
<cfset "titleMap.#field.name#.cfSqlType"=#field.cfSqlType#/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
|
||||
<cfif len(ATTRIBUTES.lengthOut)>
|
||||
<cfset "CALLER.#ATTRIBUTES.lengthOut#"=#arrayLen(thisTag.fieldsArray)#/>
|
||||
</cfif>
|
||||
|
||||
<cfif len(ATTRIBUTES.titleMapOut)>
|
||||
<cfset "CALLER.#ATTRIBUTES.titleMapOut#"=#titleMap#/>
|
||||
</cfif>
|
||||
|
||||
<cfif len(ATTRIBUTES.listOut)>
|
||||
<cfset "CALLER.#ATTRIBUTES.listOut#"=preserveSingleQuotes(list)/>
|
||||
<cfelse>
|
||||
<cfset thisTag.generatedContent=preserveSingleQuotes(list)/>
|
||||
</cfif>
|
||||
|
||||
</cfif>
|
||||
</cfsilent>
|
||||
@@ -0,0 +1,305 @@
|
||||
<!---
|
||||
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
|
||||
--->
|
||||
|
||||
<!--- проверка на неподдерживаемые поля --->
|
||||
<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", #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. 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!!!--->
|
||||
|
||||
<!--- 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=""/>
|
||||
|
||||
<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 isDefined("ATTRIBUTES.preprocessor")>
|
||||
<cfset submittedValue=#ATTRIBUTES.preprocessor(submittedValue)#/>
|
||||
</cfif>
|
||||
|
||||
<!--- check required fields --->
|
||||
<cfif StructFind(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>
|
||||
|
||||
<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.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>
|
||||
|
||||
<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. Если не получает, не отдает, можно это специально указывать флагом. Можно ему лишнего не делегировать.
|
||||
Остальное оставить вызывающему тегу?
|
||||
--->
|
||||
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
<cfsilent>
|
||||
<!--- v0.4---><!---9:50 06.10.2020--->
|
||||
<!--- v0.5---><!---2024-12-11 15:10:07--->
|
||||
<!--- DEL.CFM - унифицированный шаблон операции удаления записи --->
|
||||
<cfimport prefix="m" taglib="../../lib"/>
|
||||
<cfimport prefix="m" taglib="../../lib/controls"/>
|
||||
</cfsilent><cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag><m:silent silent="No">
|
||||
|
||||
|
||||
|
||||
<cfparam name="ATTRIBUTES.savemarker" type="string" default="delete"/>
|
||||
<cfparam name="ATTRIBUTES.readonly" type="boolean" default="No"/>
|
||||
|
||||
<cfparam name="ATTRIBUTES.checkQuery" type="string" default="select 0 as chk"/> <!--- запрос проверки целостности.
|
||||
Простая строка (параметр преобразуется до вызова тега).
|
||||
Если можно удалять, должен вернуть 0 в поле chk --->
|
||||
<cfparam name="ATTRIBUTES.displayDependencies" type="boolean" default="Yes"/>
|
||||
<cfparam name="ATTRIBUTES.ignoreDependencies" type="boolean" default="No"/><!--- будем удалять, несмотря на наличие зависимостей Предположительно, мы примем меры в запросе удаления, или расчитываем на каскадное удаление. Имеет смысл, видимо, только в комбинации с displayDependencies=TRUE, иначе зачем описывать зависимости и потом их игнорировать. --->
|
||||
<cfparam name="ATTRIBUTES.entity" type="string"/>
|
||||
<cfparam name="ATTRIBUTES.accessObj" type="string" default="#ATTRIBUTES.entity#"/> <!--- объект доступа, права к которому проверяем --->
|
||||
|
||||
<cfparam name="ATTRIBUTES.deleteQuery" type="string" default=""/> <!--- запрос удаления *** надо бы параметризовать --->
|
||||
<cfparam name="ATTRIBUTES.confirmMessage" type="string"/> <!--- текст для javascript:confirm() --->
|
||||
<cfparam name="ATTRIBUTES.denyMessage" type="string" default="Удаление невозможно, существуют зависимые записи."/> <!---
|
||||
сообщение о невозможности удаления записи (если проверка целостности говорит нельзя) --->
|
||||
|
||||
<cfparam name="ATTRIBUTES.queryString" type="string" default=""/>
|
||||
<cfparam name="ATTRIBUTES.defaultBackUrl" type="string" default="#ATTRIBUTES.entity#_ls.cfm"/>
|
||||
<cfparam name="ATTRIBUTES.output" type="string" default=""/>
|
||||
<cfparam name="ATTRIBUTES.status" type="string" default=""/>
|
||||
|
||||
<cfparam name="thisTag.dependencies" type="array" default=#arrayNew(1)#/>
|
||||
<cfparam name="thisTag.conditions" type="array"/>
|
||||
|
||||
|
||||
<cfset "CALLER.#ATTRIBUTES.status#"=CreateObject("component","status").init(false,"","")/>
|
||||
|
||||
<cfif len(ATTRIBUTES.queryString) EQ 0>
|
||||
<cfset i = 0 />
|
||||
<cfloop array=#thisTag.conditions# index="condition">
|
||||
<cfif i GT 0>
|
||||
<cfset ATTRIBUTES.queryString="#ATTRIBUTES.queryString#&"/>
|
||||
</cfif>
|
||||
<cfset i = i+1/>
|
||||
<cfset ATTRIBUTES.queryString="#ATTRIBUTES.queryString##condition.field#=#condition.value#"/>
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<m:track
|
||||
thisURL="#request.thisPage#?#ATTRIBUTES.queryString#"
|
||||
defaultBackUrl="#ATTRIBUTES.defaultBackUrl#"
|
||||
output="tr"
|
||||
/>
|
||||
|
||||
<cfquery name="qCheck" datasource="#request.DS#">
|
||||
#preserveSingleQuotes(ATTRIBUTES.checkQuery)#
|
||||
</cfquery>
|
||||
|
||||
<cfset sDenyMessage=""/>
|
||||
|
||||
<cfoutput query="qCheck">
|
||||
<cfset sDenyMessage=evaluate("""#ATTRIBUTES.denyMessage#""")/>
|
||||
</cfoutput>
|
||||
|
||||
<!--- быстрая проверка --->
|
||||
<cfset dependentRecordsExist = false/>
|
||||
<cfloop array=#thisTag.dependencies# index="dep">
|
||||
<cfif dep.query.recordCount GT 0>
|
||||
<cfset dependentRecordsExist = true/>
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
|
||||
<!--- do delete --->
|
||||
<m:ac obj="#ATTRIBUTES.accessObj#" act="#request.PERMISSION_WRITE#" output="writePermitted">
|
||||
|
||||
<cfset doSave = false/>
|
||||
<cfif !ATTRIBUTES.readonly>
|
||||
<cfloop list=#ATTRIBUTES.savemarker# index="marker">
|
||||
<cfif structKeyExists(FORM,"#marker#")><!--- удаление можно делать только постом --->
|
||||
<cfset doSave = true/>
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<cfif doSave AND qCheck.chk EQ 0>
|
||||
<cftry> <!--- <cfoutput> <cfset i = 0 />
|
||||
<cfloop list=#ATTRIBUTES.deleteQuery# delimiters="?" index="queryPart">
|
||||
|
||||
<cfif i GT 0>
|
||||
cfqueryparam
|
||||
cfsqltype='#condition.cfsqltype#'
|
||||
value='#condition.value#'
|
||||
null='#condition.null#'/
|
||||
</cfif><cfset i = i + 1/>*#queryPart#* #i#
|
||||
</cfloop>
|
||||
|
||||
</cfoutput> --->
|
||||
|
||||
|
||||
<cfquery name="qDel" datasource="#request.DS#" result="result">
|
||||
|
||||
<cfif len(ATTRIBUTES.deleteQuery)>
|
||||
<!--- запрос удаления должен быть сформулирован так,
|
||||
чтобы условия удаления были в конце --->
|
||||
<cfset i = 0 />
|
||||
<cfloop list=#ATTRIBUTES.deleteQuery# delimiters="?" index="queryPart">
|
||||
<cfif i GT 0>
|
||||
<cfqueryparam
|
||||
cfsqltype='#condition.cfsqltype#'
|
||||
value='#condition.value#'
|
||||
null='#condition.null#'/>
|
||||
</cfif><cfset i = i + 1/>#queryPart#
|
||||
</cfloop>
|
||||
<cfelse>
|
||||
delete from #ATTRIBUTES.entity# where /**/
|
||||
<cfset i = 0 />
|
||||
<cfloop array=#thisTag.conditions# index="condition">
|
||||
<cfif i GT 0> AND </cfif>
|
||||
<cfset i = i + 1/>
|
||||
#condition.expression#=<cfqueryparam
|
||||
cfsqltype='#condition.cfsqltype#'
|
||||
value='#condition.value#'
|
||||
null='#condition.null#'/>
|
||||
</cfloop>
|
||||
</cfif>
|
||||
</cfquery>
|
||||
|
||||
<!--- *** неуклюжий костыль --->
|
||||
<cfset CALLER.saveAndClose = true/><!--- сигнал для закрытия экрана --->
|
||||
|
||||
<cfcatch type="database">
|
||||
<cfset "CALLER.#ATTRIBUTES.status#.errorState"=true/>
|
||||
<cfset "CALLER.#ATTRIBUTES.status#.errorMessage"="Ошибка при обращении к базе данных. #cfcatch.message# #cfcatch.detail# #cfcatch.tagcontext[1].template# line #cfcatch.tagcontext[1].line#"/>
|
||||
<!--- <cfrethrow/> --->
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
</cfif>
|
||||
</m:ac>
|
||||
|
||||
<cfif !writePermitted><!--- перетирания cfcatch не будет --->
|
||||
<cfset "CALLER.#ATTRIBUTES.status#.errorState"=true/>
|
||||
<cfset "CALLER.#ATTRIBUTES.status#.errorMessage"="Ваших полномочий недостаточно для выполнения данной операции."/>
|
||||
</cfif>
|
||||
|
||||
|
||||
<!--- print output --->
|
||||
<cfsavecontent variable="markup">
|
||||
<cfoutput>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<br>
|
||||
<form name="frmDel" action="#request.thisPage#" method="POST">
|
||||
<cfif NOT writePermitted>
|
||||
<!--- ситуация возможна при прямом заходе по URL. Надо высказать юзарю свое неодобрение. --->
|
||||
<!--- <h4 class="text-danger">Ваших полномочий недостаточно для выполнения данной операции.</h4>
|
||||
<br/> --->
|
||||
|
||||
<cfelseif qCheck.chk GT 0>
|
||||
<h4 class="text-danger">#ATTRIBUTES.denyMessage#</h4>
|
||||
<br/>
|
||||
|
||||
<cfelseif dependentRecordsExist>
|
||||
<cfif ATTRIBUTES.displayDependencies>
|
||||
|
||||
<h4 class="text-danger">#ATTRIBUTES.denyMessage#</h4>
|
||||
<br/>
|
||||
<cftry>
|
||||
<cfloop array=#thisTag.dependencies# index="dep">
|
||||
<cfif dep.query.recordCount GT 0>
|
||||
|
||||
<h5>#dep.title#</h5>
|
||||
|
||||
<cfif dep.showDetails>
|
||||
<table class="worktable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="1%" class="c"></th>
|
||||
<cfloop array=#dep.fields# item="fld">
|
||||
<cfif len(fld.title)>
|
||||
<th>#fld.title#</th>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
<th width="1%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<cfoutput query=#dep.query#>
|
||||
<cfset urlQueryString=""/>
|
||||
<cfloop array=#dep.keys# index="keyName">
|
||||
<cfif len(urlQueryString)>
|
||||
<cfset urlQueryString="#urlQueryString#&"/><!---***--->
|
||||
</cfif>
|
||||
<cfset urlQueryString="#urlQueryString##keyName#=#dep.query[keyName][currentRow]#"/>
|
||||
</cfloop>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="#dep.page#?#urlQueryString#&#tr.fwx#" <!---class="far fa-edit"--->><img src="img/edit.gif"/></a>
|
||||
</td>
|
||||
<cfloop array=#dep.fields# item="fld">
|
||||
<cfif len(fld.title)>
|
||||
<td>
|
||||
<cfif isDefined("fld.formatter")>
|
||||
#fld.formatter(dep.query[fld.name][currentRow])#
|
||||
<cfelse>
|
||||
#dep.query[fld.name][currentRow]#
|
||||
</cfif>
|
||||
</td>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
<td>
|
||||
<a href="#dep.delete_page#?#urlQueryString#&#tr.fwx#" <!---class="far fa-trash-alt text-danger"--->><img src="img/del.gif"/></a>
|
||||
</td>
|
||||
</tr>
|
||||
</cfoutput>
|
||||
</table>
|
||||
<cfelse><!---dep.showDetails--->
|
||||
<cfoutput><p>#dep.denyMessage# (#dep.query.recordCount#)</p></cfoutput>
|
||||
</cfif><!---dep.showDetails--->
|
||||
</cfif>
|
||||
</cfloop>
|
||||
<cfcatch type="any">
|
||||
<cfoutput>#cfcatch.message# #cfcatch.detail# #cfcatch.tagcontext[1].template# line #cfcatch.tagcontext[1].line#/></cfoutput><cfrethrow/>
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
<cfelse><!---ATTRIBUTES.displayDependencies--->
|
||||
<cfoutput>#ATTRIBUTES.denyMessage#</cfoutput>
|
||||
</cfif><!---ATTRIBUTES.displayDependencies--->
|
||||
</cfif>
|
||||
|
||||
<cfif NOT dependentRecordsExist OR ATTRIBUTES.ignoreDependencies><!--- qCheck.chk GT 0 | dependentRecordsExist --->
|
||||
<cfif writePermitted>
|
||||
<input type="hidden" name="track" value="#tr.self#">
|
||||
<cfloop array=#thisTag.conditions# index="condition">
|
||||
<!---<input type="hidden" name="#condition.expression#" value="#evaluate('#condition.expression#')#"/>--->
|
||||
<input type="hidden" name="#condition.field#" value="#evaluate('#condition.field#')#"/><!--- по замыслу, берется из скоупа FORM (*** слишком неявно)--->
|
||||
</cfloop>
|
||||
<button type="submit" class="maincontrol btn btn-secondary" name="delete" onclick="if(!confirm('#evaluate("""#ATTRIBUTES.confirmMessage#""")#')) return false;">Удалить</button>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<button type="button" class="maincontrol btn btn-secondary" name="close" onclick="document.location.href='#tr.backURL#'">
|
||||
Закрыть
|
||||
</button>
|
||||
|
||||
<!--- <cfdump var=#dependentRecordsExist#/>
|
||||
<cfdump var=#ATTRIBUTES.ignoreDependencies#/>
|
||||
<cfdump var=#NOT dependentRecordsExist OR ATTRIBUTES.ignoreDependencies#/> --->
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</cfoutput>
|
||||
</cfsavecontent>
|
||||
|
||||
</m:silent><cfif len(ATTRIBUTES.output)><cfset "CALLER.#ATTRIBUTES.output#" = #markup#/><cfelse>
|
||||
<cfoutput>#markup#</cfoutput>
|
||||
</cfif></cfif>
|
||||
<!--- пример монструозной конструкции Postgre для каскадного удаления --->
|
||||
<!--- with
|
||||
inst as (
|
||||
delete from instance
|
||||
where instance_uid='e283e337-0ddc-459c-88b8-57d1963b4a7d'
|
||||
returning instance_uid
|
||||
),
|
||||
st as (
|
||||
delete from instance_state
|
||||
where instance_uid in (select instance_uid from inst)
|
||||
returning instance_state_uid
|
||||
),
|
||||
op as (
|
||||
delete from instance_operation
|
||||
where instance_uid in (select instance_uid from inst)
|
||||
returning instance_operation_uid
|
||||
),
|
||||
par as (
|
||||
delete from instance_operation_param
|
||||
where instance_operation_uid in (select instance_operation_uid from op)
|
||||
returning instance_operation_param_uid
|
||||
),
|
||||
cfs as (
|
||||
delete from instance_operation_cfs_param
|
||||
where instance_operation_uid in (select instance_operation_uid from op)
|
||||
returning instance_operation_cfs_param_uid
|
||||
)
|
||||
select * from inst
|
||||
left outer join st on (inst.instance_uid=st.instance_uid)
|
||||
left outer join op on (inst.instance_uid=op.instance_uid)
|
||||
left outer join par on (op.instance_operation_uid=par.instance_operation_uid)
|
||||
left outer join cfs on (op.instance_operation_uid=cfs.instance_operation_uid)
|
||||
; --->
|
||||
<!--- такой вопрос: точно ли будут задействованы все CTE, если я на них не сошлюсь? На месте оптимизатора я бы проигнорил незадействованные. Наверное, надо все включить в джойн для уверенности? --->
|
||||
@@ -0,0 +1,19 @@
|
||||
<!---v0.2 9:50 06.10.2020
|
||||
---><cfsilent>
|
||||
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
|
||||
<cfassociate basetag="cf_del" datacollection="conditions" />
|
||||
<cfparam name="ATTRIBUTES.field"/><!---имя параметра формы --->
|
||||
<cfparam name="ATTRIBUTES.value"/>
|
||||
<cfparam name="ATTRIBUTES.expression" default=""/><!---левая сторона выражения во where, обычно имя поля--->
|
||||
<cfparam name="ATTRIBUTES.cfsqltype" default="cf_sql_integer"/>
|
||||
<cfparam name="ATTRIBUTES.null" type="boolean" default="false"/>
|
||||
|
||||
<cfif ATTRIBUTES.expression IS "">
|
||||
<cfif len(thisTag.generatedContent)>
|
||||
<cfset ATTRIBUTES.expression=#thisTag.generatedContent#/>
|
||||
<cfelse>
|
||||
<cfset ATTRIBUTES.expression=#ATTRIBUTES.field#/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
</cfif>
|
||||
</cfsilent>
|
||||
@@ -0,0 +1,26 @@
|
||||
<cfsilent>
|
||||
<cfset out=""/>
|
||||
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
|
||||
<cfassociate basetag="cf_dependency" datacollection="fieldsArray" />
|
||||
|
||||
<cfparam name="ATTRIBUTES.expression" default=""/>
|
||||
<cfparam name="ATTRIBUTES.title" default=""/>
|
||||
<cfparam name="ATTRIBUTES.name" default=""/>
|
||||
|
||||
<cfif ATTRIBUTES.expression IS "">
|
||||
<cfset ATTRIBUTES.expression=thisTag.generatedContent/>
|
||||
</cfif>
|
||||
|
||||
<cfif ATTRIBUTES.name IS "">
|
||||
<cfset regex="([[:word:]]+)[[:space:]]*$"/>
|
||||
<cfset matches=REFindNoCase(regex, ATTRIBUTES.expression, 1, true)/>
|
||||
<cfif arrayLen(matches.pos) GT 1>
|
||||
<cfset ATTRIBUTES.name=mid(ATTRIBUTES.expression, matches.pos[2], matches.len[2])/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
<cfif arrayLen(getBaseTagData("cf_dependency",1).thisTag.fieldsArray) GT 0>
|
||||
<cfset thisTag.generatedContent=", #thisTag.generatedContent#"/>
|
||||
</cfif>
|
||||
|
||||
</cfif>
|
||||
</cfsilent>
|
||||
@@ -0,0 +1,9 @@
|
||||
<cfsilent>
|
||||
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
|
||||
<cfassociate basetag="cf_dependency" datacollection="keys" />
|
||||
|
||||
<cfparam name="ATTRIBUTES.field"/>
|
||||
<cfparam name="ATTRIBUTES.cfsqltype default="cf_sql_integer"/>
|
||||
|
||||
</cfif>
|
||||
</cfsilent>
|
||||
@@ -0,0 +1,56 @@
|
||||
<cfsilent>
|
||||
<cfif thisTag.executionMode IS "end">
|
||||
<cfassociate basetag="cf_del" datacollection="dependencies" />
|
||||
|
||||
<!--- following are tag attribute placeholders. Coorresponding data are used by parent tags!--->
|
||||
<cfparam name="ATTRIBUTES.entity" default=""/>
|
||||
<cfparam name="ATTRIBUTES.title" default=""/>
|
||||
<cfparam name="ATTRIBUTES.page" default="#ATTRIBUTES.entity#.cfm"/>
|
||||
<cfparam name="ATTRIBUTES.delete_page" default="#ATTRIBUTES.entity#_del.cfm"/>
|
||||
<cfparam name="ATTRIBUTES.showDetails" type="boolean" default="Yes"/>
|
||||
<cfparam name="ATTRIBUTES.denyMessage" type="string" default="Необходимо удалить соответствующие записи"/><!--- displayed instead of details when ATTRIBUTES.showDetails IS "No"--->
|
||||
<!---<cfparam name="ATTRIBUTES.ondelete" default="restrict"/> implement 1-level Cascade? --->
|
||||
|
||||
<cfparam name="thisTag.fields" type="array"/>
|
||||
<cfparam name="thisTag.from" type="array"/>
|
||||
<cfparam name="thisTag.conditions" type="array"/>
|
||||
<cfparam name="thisTag.order_by" type="array"/>
|
||||
|
||||
<cfset ATTRIBUTES.fields=thisTag.fields/>
|
||||
<cfset ATTRIBUTES.keys=arrayNew(1)/>
|
||||
|
||||
<cfset i=0/>
|
||||
<cfloop array=#thisTag.fields# index="field">
|
||||
<cfif len(field.name)>
|
||||
<cfif structKeyExists(field, "key")>
|
||||
<cfset arrayAppend(ATTRIBUTES.keys, #field.name#)/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
|
||||
<cfquery name="qDependency" datasource="#request.DS#">
|
||||
select
|
||||
<cfset i = 0 />
|
||||
<cfloop array=#thisTag.fields# index="field">
|
||||
<cfif i GT 0>, </cfif><cfset i = i+1/>#field.expression#
|
||||
</cfloop>
|
||||
from
|
||||
<cfloop array=#thisTag.from# index="from">
|
||||
#from.expression#
|
||||
</cfloop>
|
||||
where
|
||||
<cfset i = 0 />
|
||||
<cfloop array=#thisTag.conditions# index="condition">
|
||||
<cfif i GT 0> AND </cfif><cfset i = i+1/>
|
||||
#condition.expression#=<cfqueryparam cfsqltype='#condition.cfsqltype#' value='#condition.value#' null='#condition.null#'/>
|
||||
</cfloop>
|
||||
order by
|
||||
<cfset i = 0 />
|
||||
<cfloop array=#thisTag.order_by# index="order">
|
||||
<cfif i GT 0>, </cfif><cfset i = i+1/>#order.expression#
|
||||
</cfloop>
|
||||
</cfquery>
|
||||
<cfset ATTRIBUTES.query=#qDependency#/>
|
||||
</cfif>
|
||||
<cfset thisTag.generatedContent=""/>
|
||||
</cfsilent>
|
||||
@@ -0,0 +1,9 @@
|
||||
<cfsilent>
|
||||
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
|
||||
<cfassociate basetag="cf_dependency" datacollection="conditions" />
|
||||
<cfparam name="ATTRIBUTES.expression" default=#thisTag.generatedContent#/>
|
||||
<cfparam name="ATTRIBUTES.value"/>
|
||||
<cfparam name="ATTRIBUTES.cfsqltype" default="cf_sql_integer"/>
|
||||
<cfparam name="ATTRIBUTES.null" type="boolean" default="No"/>
|
||||
</cfif>
|
||||
</cfsilent>
|
||||
@@ -0,0 +1,22 @@
|
||||
<cfsilent></cfsilent>
|
||||
<cfset out=""/>
|
||||
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
|
||||
<cfassociate basetag="cf_dependency" datacollection="fields" />
|
||||
|
||||
<cfparam name="ATTRIBUTES.expression" default=#thisTag.generatedContent#/>
|
||||
<cfparam name="ATTRIBUTES.title" default=""/>
|
||||
<cfparam name="ATTRIBUTES.name" default=""/>
|
||||
|
||||
<cfif ATTRIBUTES.name IS "">
|
||||
<cfset regex="([[:word:]]+)[[:space:]]*$"/>
|
||||
<cfset matches=REFindNoCase(regex, ATTRIBUTES.expression, 1, true)/>
|
||||
<cfif arrayLen(matches.pos) GT 1>
|
||||
<cfset ATTRIBUTES.name=mid(ATTRIBUTES.expression, matches.pos[2], matches.len[2])/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<cfif arrayLen(getBaseTagData("cf_dependency",1).thisTag.fields) GT 1>
|
||||
<cfset thisTag.generatedContent=", #thisTag.generatedContent#"/>
|
||||
</cfif>
|
||||
|
||||
</cfif>
|
||||
@@ -0,0 +1,6 @@
|
||||
<cfsilent>
|
||||
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
|
||||
<cfassociate basetag="cf_dependency" datacollection="from" />
|
||||
<cfparam name="ATTRIBUTES.expression" default=#thisTag.generatedContent# />
|
||||
</cfif>
|
||||
</cfsilent>
|
||||
@@ -0,0 +1,6 @@
|
||||
<cfsilent>
|
||||
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
|
||||
<cfassociate basetag="cf_dependency" datacollection="order_by" />
|
||||
<cfparam name="ATTRIBUTES.expression" default=#thisTag.generatedContent# />
|
||||
</cfif>
|
||||
</cfsilent>
|
||||
@@ -0,0 +1,66 @@
|
||||
<cfsilent>
|
||||
<!--- 20181209 v0.3 --->
|
||||
<!---v0.3 добавлен атрибут cfSqlType--->
|
||||
<!---v0.4 добавлен атрибут container for json--->
|
||||
<!---v0.5 добавлен атрибут type и трансляция в CF_SQL_--->
|
||||
<!--- 20210326 v0.8 formatter --->
|
||||
|
||||
<cffunction name="passThrough"
|
||||
returntype="any"
|
||||
output="false"
|
||||
hint="just return argument">
|
||||
<cfargument name="x" type="ANY" required="true"/>
|
||||
<cfreturn #ARGUMENTS.x#/>
|
||||
</cffunction>
|
||||
|
||||
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
|
||||
<cfassociate basetag="cf_field_set" datacollection="fieldsArray"/>
|
||||
|
||||
<cfparam name="ATTRIBUTES.expression" default=""/>
|
||||
<cfparam name="ATTRIBUTES.title" default=""/>
|
||||
<cfparam name="ATTRIBUTES.name" default=""/>
|
||||
<cfparam name="ATTRIBUTES.type" default="string"/>
|
||||
<cfparam name="ATTRIBUTES.cfSqlType" default="#getCfSqlType(ATTRIBUTES.type)#"/>
|
||||
<cfparam name="ATTRIBUTES.container" default=""/>
|
||||
<cfparam name="ATTRIBUTES.formatter" default=#passThrough#/>
|
||||
|
||||
<cfif ATTRIBUTES.expression IS "">
|
||||
<cfset ATTRIBUTES.expression=thisTag.generatedContent/>
|
||||
</cfif>
|
||||
|
||||
<cfif ATTRIBUTES.name IS "">
|
||||
<cfset regex="([[:word:]]+)[[:space:]]*$"/>
|
||||
<cfset matches=REFindNoCase(regex, ATTRIBUTES.expression, 1, true)/>
|
||||
<cfif arrayLen(matches.pos) GT 1>
|
||||
<cfset ATTRIBUTES.name=mid(ATTRIBUTES.expression, matches.pos[2], matches.len[2])/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<cffunction name="getCfSqlType">
|
||||
<!---Translate CF type understandable by isValid to CF_SQL_*--->
|
||||
<cfargument name="type"/>
|
||||
|
||||
<cfswitch expression=#ARGUMENTS.type#>
|
||||
<cfcase value="string">
|
||||
<cfreturn "CF_SQL_VARCHAR"/>
|
||||
</cfcase>
|
||||
<cfcase value="integer">
|
||||
<cfreturn "CF_SQL_INTEGER"/>
|
||||
</cfcase>
|
||||
<cfcase value="boolean">
|
||||
<cfreturn "CF_SQL_BIT"/>
|
||||
</cfcase>
|
||||
<cfcase value="numeric">
|
||||
<cfreturn "CF_SQL_NUMERIC"/>
|
||||
</cfcase>
|
||||
<cfcase value="date,time">
|
||||
<cfreturn "CF_SQL_TIMESTAMP"/>
|
||||
</cfcase>
|
||||
<cfdefaultcase>
|
||||
<cfthrow type="custom" message="Unsupported type" detail="Type #ARGUMENTS.type# not supported"/>
|
||||
</cfdefaultcase>
|
||||
</cfswitch>
|
||||
|
||||
</cffunction>
|
||||
</cfsilent>
|
||||
@@ -0,0 +1,68 @@
|
||||
<cfsilent>
|
||||
<!--- 20181209 v0.3 добавлено поле cfSqlType--->
|
||||
<!--- 20201029 v0.5 все атрибуты необязательные--->
|
||||
<!--- 20201114 v0.6 добавлен атрибут "container" for json--->
|
||||
<!--- 20201201 v0.7 исправлен listOut--->
|
||||
<!--- 20210326 v0.8 formatter --->
|
||||
<!--- 20210326 v0.9 listOutKeepContent --->
|
||||
<!--- Используется в селекте для структурирования списка полей. Выводит вместо себя список через запятую, экспортирует Map со структурами выражение+заголовок, с ключом, соответствующим имени колонки в селекте (ключ вычисляется регулярным выражением) --->
|
||||
|
||||
<cfif thisTag.executionMode is "end">
|
||||
|
||||
<cfparam name="ATTRIBUTES.titleMapOut" default=""/>
|
||||
<cfparam name="ATTRIBUTES.lengthOut" default=""/>
|
||||
<cfparam name="ATTRIBUTES.listOut" default=""/><!---возвращаемый список полей через запятую для селекта. Если не задан (чаще всего так), этот список возвращается в виде контента тега --->
|
||||
<cfparam name="ATTRIBUTES.listOutKeepContent" default=false/><!--- указывает listOut не очищать контент --->
|
||||
<cfparam name="ATTRIBUTES.fieldsToInclude" default=""/><!--- список нужных полей, если не пустой - выбрасывать все поля, кроме тех, что в списке --->
|
||||
|
||||
<cfparam name="thisTag.fieldsArray" type="array"/><!--- Вложенные теги field отдают сюда свои данные --->
|
||||
|
||||
<cfset titleMap=structNew("linked")/><!---railo syntax--->
|
||||
<cfset list=""/>
|
||||
<cfset i=0/>
|
||||
<cfloop array=#thisTag.fieldsArray# index="field">
|
||||
<cfif len(#field.name#) EQ 0
|
||||
OR listLen(ATTRIBUTES.fieldsToInclude) EQ 0
|
||||
OR listFind(ATTRIBUTES.fieldsToInclude,field.name)>
|
||||
<!--- пытаемся сохранить прежнюю логику - поля без имени не выбрасываются, но не попадают в titleMap --->
|
||||
<cfset i=i+1/>
|
||||
<cfset list=listAppend(#list#, #field.expression#)/>
|
||||
|
||||
<cfif len(#field.name#)>
|
||||
<cfset structInsert(#titleMap#, #field.name#, structNew())/>
|
||||
<cfset "titleMap.#field.name#.ordinal"=#i#/>
|
||||
<cfset "titleMap.#field.name#.title"=#field.title#/>
|
||||
<cfset "titleMap.#field.name#.type"=#field.type#/>
|
||||
<cfset "titleMap.#field.name#.cfSqlType"=#field.cfSqlType#/>
|
||||
<cfset "titleMap.#field.name#.container"=#field.container#/>
|
||||
<cfset "titleMap.#field.name#.formatter"=#field.formatter#/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
|
||||
<cfif len(ATTRIBUTES.lengthOut)>
|
||||
<cfset "CALLER.#ATTRIBUTES.lengthOut#"= i/>
|
||||
</cfif>
|
||||
|
||||
<cfif len(ATTRIBUTES.titleMapOut)>
|
||||
<cfset "CALLER.#ATTRIBUTES.titleMapOut#"=#titleMap#/>
|
||||
</cfif>
|
||||
|
||||
<!--- <cfif len(ATTRIBUTES.listOut)>
|
||||
<cfset "CALLER.#ATTRIBUTES.listOut#"=preserveSingleQuotes(list)/>
|
||||
<cfset thisTag.generatedContent=""/>
|
||||
</cfif> --->
|
||||
|
||||
<cfif len(ATTRIBUTES.listOut)><!--- *** некрасиво --->
|
||||
<cfset "CALLER.#ATTRIBUTES.listOut#"=list/>
|
||||
<cfif ATTRIBUTES.listOutKeepContent>
|
||||
<cfset thisTag.generatedContent=preserveSingleQuotes(list)/>
|
||||
<cfelse>
|
||||
<cfset thisTag.generatedContent=""/>
|
||||
</cfif>
|
||||
<cfelse>
|
||||
<cfset thisTag.generatedContent=preserveSingleQuotes(list)/>
|
||||
</cfif>
|
||||
|
||||
</cfif>
|
||||
</cfsilent>
|
||||
@@ -0,0 +1,354 @@
|
||||
<!---
|
||||
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. Если не получает, не отдает, можно это специально указывать флагом. Можно ему лишнего не делегировать.
|
||||
Остальное оставить вызывающему тегу?
|
||||
--->
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<cfcomponent displayname="Bean Status" output="false">
|
||||
<!--- v 0.03 2024-09-05 12:36:40 added recordExists --->
|
||||
|
||||
<cfproperty name="errorState" type="boolean"/>
|
||||
<cfproperty name="errorMessage" type="string"/>
|
||||
<cfproperty name="processingMode" type="string"/>
|
||||
<cfproperty name="recordExists" type="boolean"/>
|
||||
|
||||
<cffunction name="init">
|
||||
<cfset this.errorState=false/>
|
||||
<cfset this.errorMessage=""/>
|
||||
<cfset this.processingMode=""/>
|
||||
<cfset this.recordExists=false/>
|
||||
|
||||
<cfreturn this>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="init">
|
||||
<cfargument name="errorState" type="boolean"/>
|
||||
<cfargument name="errorMessage" type="string"/>
|
||||
<cfargument name="processingMode" type="string"/>
|
||||
|
||||
<cfset this.errorState=arguments.errorState/>
|
||||
<cfset this.errorMessage=arguments.errorMessage/>
|
||||
<cfset this.processingMode=arguments.processingMode/>
|
||||
|
||||
<cfreturn this>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="init">
|
||||
<cfargument name="errorState" type="boolean"/>
|
||||
<cfargument name="errorMessage" type="string"/>
|
||||
<cfargument name="processingMode" type="string"/>
|
||||
<cfargument name="recordExists" type="boolean"/>
|
||||
|
||||
<cfset this.errorState=arguments.errorState/>
|
||||
<cfset this.errorMessage=arguments.errorMessage/>
|
||||
<cfset this.processingMode=arguments.processingMode/>
|
||||
<cfset this.recordExists=arguments.recordExists/>
|
||||
|
||||
<cfreturn this>
|
||||
</cffunction>
|
||||
</cfcomponent>
|
||||
@@ -0,0 +1,59 @@
|
||||
<cfif thisTag.ExecutionMode is 'end'>
|
||||
<cfparam name="ATTRIBUTES.output" type="string">
|
||||
<cfparam name="ATTRIBUTES.outputType" type="string" default="map">
|
||||
<cfparam name="thisTag.params" type="array"/>
|
||||
|
||||
|
||||
<cffunction name="paramTypeToColumnType">
|
||||
<cfargument name="paramType" type="string"/>
|
||||
|
||||
<cfswitch expression=#ARGUMENTS.paramType#>
|
||||
<cfcase value="integer">
|
||||
<cfreturn "integer"/>
|
||||
</cfcase>
|
||||
<cfcase value="numeric">
|
||||
<cfreturn "decimal"/><!---*** --->
|
||||
</cfcase>
|
||||
<cfcase value="datetime">
|
||||
<cfreturn "timestamp"/>
|
||||
</cfcase>
|
||||
<cfcase value="string">
|
||||
<cfreturn "varchar"/>
|
||||
</cfcase>
|
||||
<cfdefaultcase>
|
||||
<cfreturn "varchar"/>
|
||||
</cfdefaultcase>
|
||||
</cfswitch>
|
||||
</cffunction>
|
||||
|
||||
|
||||
|
||||
<cfswitch expression=#ATTRIBUTES.outputType#>
|
||||
|
||||
<cfcase value="map">
|
||||
<cfloop from=1 to=#arrayLen(thisTag.params)# index="i">
|
||||
<cfset "CALLER.#ATTRIBUTES.output#.#thisTag.params[i].name#"=thisTag.params[i].value/>
|
||||
</cfloop>
|
||||
</cfcase>
|
||||
|
||||
<cfcase value="query">
|
||||
<cfset columnList=""/>
|
||||
<cfset columnTypeList=""/>
|
||||
<cfloop from=1 to=#arrayLen(thisTag.params)# index="i">
|
||||
<cfset columnList=listAppend(columnList, thisTag.params[i].name)/>
|
||||
<cfset columnTypeList=listAppend(columnTypeList, paramTypeToColumnType(thisTag.params[i].type))/>
|
||||
</cfloop>
|
||||
<cfset qOut=queryNew(columnList,columnTypeList)/>
|
||||
<cfset queryAddRow(qOut)/>
|
||||
<cfloop from=1 to=#arrayLen(thisTag.params)# index="i">
|
||||
<cfset querySetCell(qOut, thisTag.params[i].name, thisTag.params[i].value)/>
|
||||
</cfloop>
|
||||
<cfset "CALLER.#ATTRIBUTES.output#"=#qOut#/>
|
||||
</cfcase>
|
||||
|
||||
<cfdefaultcase>
|
||||
<cfthrow type="custom" message="Unsupported output type '#ATTRIBUTES.outputType#'" detail=""/>
|
||||
</cfdefaultcase>
|
||||
|
||||
</cfswitch>
|
||||
</cfif>
|
||||
@@ -0,0 +1,69 @@
|
||||
<cfsilent></cfsilent>
|
||||
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
|
||||
|
||||
<cfassociate basetag="cf_submit_collection" datacollection="params" />
|
||||
|
||||
<cfparam name="ATTRIBUTES.name"/>
|
||||
<cfparam name="ATTRIBUTES.type" default="string"/>
|
||||
<cfparam name="ATTRIBUTES.default" default=""/><!--- не валидируется, что позволяет использовать, например, пустую строку для индикации нулла в числовой колонке --->
|
||||
|
||||
<!--- output params --->
|
||||
<cfparam name="ATTRIBUTES.submittedValue" type="string" default=""/>
|
||||
<cfparam name="ATTRIBUTES.value" type="any" default=""/>
|
||||
|
||||
|
||||
<cfif structKeyExists(CALLER.VARIABLES, "#ATTRIBUTES.name#")>
|
||||
<cfset ATTRIBUTES.submittedValue=CALLER.VARIABLES["#ATTRIBUTES.name#"]/>
|
||||
<cfelseif structKeyExists(FORM, "#ATTRIBUTES.name#")>
|
||||
<cfset ATTRIBUTES.submittedValue=FORM["#ATTRIBUTES.name#"]/>
|
||||
<cfelseif structKeyExists(URL, "#ATTRIBUTES.name#")>
|
||||
<cfset ATTRIBUTES.submittedValue=URL["#ATTRIBUTES.name#"]/>
|
||||
<cfelse>
|
||||
<cfset ATTRIBUTES.submittedValue=#ATTRIBUTES.default#/>
|
||||
</cfif>
|
||||
|
||||
<cfif structKeyExists(ATTRIBUTES,"preprocessor")>
|
||||
<cfset preprocessedValue=#ATTRIBUTES.preprocessor(ATTRIBUTES.submittedValue)#/>
|
||||
<cfelse>
|
||||
<cfset preprocessedValue=#ATTRIBUTES.submittedValue#/>
|
||||
</cfif>
|
||||
|
||||
|
||||
<cfswitch expression=#ATTRIBUTES.type#>
|
||||
<cfcase value="integer">
|
||||
<cfif isValid('integer', preprocessedValue)>
|
||||
<cfset ATTRIBUTES.value=preprocessedValue/>
|
||||
<cfelseif isValid('number', preprocessedValue)>
|
||||
<cfset ATTRIBUTES.value=round(preprocessedValue)/>
|
||||
<cfelse>
|
||||
<cfset ATTRIBUTES.value=#ATTRIBUTES.default#/>
|
||||
</cfif>
|
||||
</cfcase>
|
||||
|
||||
<cfcase value="numeric">
|
||||
<cfif isValid('numeric', preprocessedValue)>
|
||||
<cfset ATTRIBUTES.value=preprocessedValue/>
|
||||
<cfelse>
|
||||
<cfset ATTRIBUTES.value=#ATTRIBUTES.default#/>
|
||||
</cfif>
|
||||
</cfcase>
|
||||
|
||||
<cfcase value="datetime">
|
||||
<cfparam name="ATTRIBUTES.locale" default="ru"/>
|
||||
<cfparam name="ATTRIBUTES.format" default=""/>
|
||||
|
||||
<cftry>
|
||||
<cfset ATTRIBUTES.value=LSParseDateTime(preprocessedValue, #ATTRIBUTES.locale#, #ATTRIBUTES.format#)/>
|
||||
<cfcatch type="ANY">
|
||||
<cfset ATTRIBUTES.value=#ATTRIBUTES.default#/>
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
</cfcase>
|
||||
|
||||
<cfcase value="string">
|
||||
<cfset ATTRIBUTES.value=preprocessedValue/>
|
||||
</cfcase>
|
||||
|
||||
</cfswitch>
|
||||
|
||||
</cfif>
|
||||
Reference in New Issue
Block a user