This commit is contained in:
msyu
2025-09-18 19:37:04 +03:00
parent 89ac26325d
commit d42df73e4e
21 changed files with 1354 additions and 63 deletions
+19 -16
View File
@@ -26,6 +26,7 @@
<cfset this.defaultdatasource = this.datasource/>
<cfset request.DS = "#this.datasource#">
<cfset request.language="">
<!--- кажется, нужно инициализировать датасорцы в псевдоконструкторе - onRequest не получается --->
<!--- <cfset this.datasources["#this.datasource#"]=getDS("#this.datasource#","ds_#this.datasource#")/> --->
@@ -127,20 +128,20 @@
Вообще не следует ничего блокировать на время сетевого взаимодействия
Нижеследующая, на первый взгляд, странная конструкция ровно для этого придумана
--->
<cfoutput>onRequest:check for cert started #(getTickCount()-request.startTickCount)#</cfoutput>
<!--- <cfoutput>onRequest:check for cert started #(getTickCount()-request.startTickCount)#</cfoutput> --->
<cfset var idpCertificateExists = false/>
<cflock scope="application" type="readonly" timeout=1>
<cfset request.APP_NAME=this.Name/>
<cfset idpCertificateExists = structKeyExists(application,"idpCertificate")/>
</cflock>
<cfif NOT idpCertificateExists>
Obtain IDP Certificate (onRequest)
<!--- Obtain IDP Certificate (onRequest) --->
<cfset var idpCertificate = getIdpCertificate(this.idpCertUrl) />
<cflock scope="application" type="exclusive" timeout=1>
<cfset application.idpCertificate = idpCertificate/>
</cflock>
</cfif>
<cfoutput>onRequest:check for cert finished #(getTickCount()-request.startTickCount)#</cfoutput>
<!--- <cfoutput>onRequest:check for cert finished #(getTickCount()-request.startTickCount)#</cfoutput> --->
<cfset this.redirect_uri = "https://#CGI.SERVER_NAME#/#CGI.SCRIPT_NAME#" />
@@ -152,24 +153,26 @@
<!--- SSO --->
<cflock scope="session" type="exclusive" timeout="3">
<!--- --->
<!---
<cfdump var=#session#/>
<cfoutput>#(getTickCount()-request.startTickCount)#</cfoutput>
<cfoutput>#(getTickCount()-request.startTickCount)#</cfoutput>--->
<!---
Это первая проба, с сессией
Когда протухнет сессия, мы можем пойти к IDP и повторно аутентифицироваться. Поскольку у нас сохраняются куки IDP, его сессия, возможно, будет жива. Если мы обновляли сессию IDP в другом приложении SSO, по всей видимости, она будет продолжаться. Если мы видим, что сессия IDP состарилась и хочет протухнуть, мы можем ее освежить и получить новый access token (который мы пока не используем - мы пользуемся ответом IDP) Наверное, правильно обходиться для этих целей вообще без сессии *** --->
<!---
сделать логаут
сделать отображение ошибки IDP?
+сделать получение сертификата
сделать рефреш
проверить sso
? сделать рефреш
? сделать логаут
? сделать отображение ошибки IDP
+ сделать получение сертификата
--->
<cfif structKeyExists(session, "auth")>
<cfset request.auth = structCopy(session.auth)/>
<!--- тут нужно сделать проверку и рефреш токена --->
сессия есть
<!--- сессия есть --->
<cfelse>
сессии нет
<!--- сессии нет --->
<cfset session.auth_state=createGUID().toString()/>
<cfscript>
@@ -183,9 +186,9 @@
может быть, KeyCloak так защищается от циклических редиректов
Не очень понятно, как предотвратить повторный запрос (можно, конечно, использовать сессию, но странно)
*/
writeOutput(getTickCount()-request.startTickCount);
//writeOutput(getTickCount()-request.startTickCount);
if(structKeyExists(url, "code")) { //обрабатываем редирект от IDP *** наличие поля code в URL введет нас в заблуждение, но эvar = data, label =то только без валидной сессии
dump(var = url.code, label = "URL.code");
//dump(var = url.code, label = "URL.code");
// Request access token from idp with the
// authorization code that we got via the URL
//aFormFields = [];
@@ -196,13 +199,13 @@
resp = idp.makeAccessTokenRequest(url.code/*, aFormFields*/);
writeOutput("idp.makeAccessTokenRequest Done");
writeOutput(getTickCount()-request.startTickCount);
dump(resp);
//dump(resp);
data = deserializeJson(resp.content);
//writedump(data);
// Print full response from idp
dump(var = data, label = "makeAccessTokenRequest - Response from idp");
//dump(var = data, label = "makeAccessTokenRequest - Response from idp");
echo('<a href="./index.cfm?refresh=#data.refresh_token#">Refresh token #data.refresh_token#</a>');
//echo('<a href="./index.cfm?refresh=#data.refresh_token#">Refresh token #data.refresh_token#</a>');
session.auth.expires_in = data.expires_in;
session.auth.refresh_expires_in = data.refresh_expires_in;
+3 -1
View File
@@ -49,10 +49,12 @@ function timeFmt(date, sFormat = 'HH:MM'){
}
request.timeFmt = timeFmt;
//////////////////////////////////////////////////////
function usrAuthenticated() {
return request.usr_id GT 0 AND NOT (request.usr_id EQ request.ANONYMOUS_USR_ID);
return structKeyExists(request, "auth");
}
request.usrAuthenticated = usrAuthenticated;
/////////////////////////////////////////////////////
function filterOn(filter) {
return (isStruct(filter) AND NOT structIsEmpty(filter));
-42
View File
@@ -10,49 +10,7 @@
<ul id="mainmenu">
<cfif request.usrAuthenticated()></cfif>
<li class="menu-title">Реестры</li>
<c:menu_item acl="" page="deal_ls.cfm" label="Сделки"/>
<c:menu_item acl="" page="contract_ls.cfm" label="Договоры"/>
<c:menu_item acl="" page="additional_agreement_ls.cfm" label="Доп. соглашения"/>
<!---
<c:menu_item acl="" page="specification_ls.cfm" label="Спецификации"/>
<c:menu_item acl="" page="agreement_ls.cfm" label="Сделки"/>
<c:menu_item acl="" page="specification_item_ls.cfm" label="Экземпляры услуг"/>--->
<li class="menu-title">Отчеты</li>
<c:menu_item acl="" page="payg.cfm" label="PAYG"/>
<!--- <c:menu_item acl="" page="detail_daily_rpt.cfm" label="Детализация по дням"/>
<c:menu_item acl="" page="income_daily_rpt.cfm" label="Выручка по дням"/>
<c:menu_item acl="" page="income_monthly_rpt.cfm" label="Выручка по месяцам"/> --->
<li class="menu-title">Справочники</li>
<!--- <c:menu_item acl="" page="svc_ls.cfm" label="Сервисы"/> --->
<c:menu_item acl="" page="contragent_ls.cfm" label="Контрагенты"/>
<!--- <c:menu_item acl="" page="measure_ls.cfm" label="Единицы измерения"/>
<c:menu_item acl="" page="usr_ls.cfm" label="Пользователи"/>
--->
<!--- <li class="menu-title">Пользователь</li>
<cfif request.usrAuthenticated()>
<cfquery name="qUsr" datasource="#request.DS#">
select shortname/*, login, firstname, middlename, lastname*/
from usr
where usr_id=<cfqueryparam cfsqltype="CF_SQL_INTEGER" value=#request.usr_id#/>
</cfquery>
<li class="menu-item-noaction">#qUsr.shortname#</li> <!---#request.usr_id#--->
<c:menu_item acl="" page="password.cfm" queryString="#ATTRIBUTES.fwx#" label="Смена пароля"/>
<c:menu_item acl="" page="logout.cfm" queryString="target_page=#URLEncodedFormat(ATTRIBUTES.thisUrl)#" label="Выход"/>
<cfelse>
<c:menu_item acl="" page="saml/login.cfm" queryString="target_page=#URLEncodedFormat(ATTRIBUTES.thisUrl)#" label="Вход через WebSSO"/>
<c:menu_item acl="" page="login.cfm" queryString="#ATTRIBUTES.fwx#" label="Вход"/>
<c:menu_item acl="" page="logout.cfm" queryString="target_page=#URLEncodedFormat(ATTRIBUTES.thisUrl)#" label="Выход"/>
</cfif> --->
</ul>
</cfoutput>
<cfexit method="exittag"/>
+1
View File
@@ -5,3 +5,4 @@
По истечении установленного времени (скажем, половины срока жизни токена) идем к IDP за рефрешем
Сертификат открытого ключа можно каждый раз получать от IDP.
Запрос к IDP идет только для освежения сессии, а не на каждый запрос. Логаут на IDP не проверяется при каждом запросе.
<cflocation url="payg.cfm"/>
+33
View File
@@ -0,0 +1,33 @@
<cfsilent>
<!---Замещает любую переменную (*** небезопасно) тега layout:page --->
<!---Тег layout:page проглатывает собственный контент, и вместо него после закрытия выводит разметку на основании атрибутов (при открытии тега он формирует переменные - фрагменты разметки - по шаблону) и вложенных тегов layout:attribute (которые по закрытии тега на самом деле перезаписывают переменные в layout:page).
Вложенные теги удобны, чтобы передавать читабельную разметку, а не строку в кавычках.--->
<cfparam name="ATTRIBUTES.name"/><!---имя переменной layout:page, чаще всего title, controls--->
<!--- ATTRIBUTES.value --->
<cfparam name="ATTRIBUTES.append" default=""/><!---before,after--->
<cfif thisTag.executionMode IS "end">
<cfassociate basetag="cf_page"/>
<cfset layoutTemplate=getBaseTagData('cf_page',1)/>
<cfif structKeyExists(ATTRIBUTES, "value")>
<cfset value=ATTRIBUTES.value/>
<cfelse>
<cfset value=thisTag.generatedContent/>
</cfif>
<!---ниже в комментариях предполагается, что мы меняем фрагмент разметки, например, controls, но на самом деле это может быть любая переменная тега layout:page--->
<cfif structKeyExists(layoutTemplate,"#ATTRIBUTES.name#") AND lcase(ATTRIBUTES.append) EQ "before">
<!---дописать в начало фрагмента разметки с именем name, сгенерированного layout:page, например, controls--->
<cfset "layoutTemplate.#ATTRIBUTES.name#" = value & layoutTemplate["#ATTRIBUTES.name#"]/>
<cfelseif structKeyExists(layoutTemplate,"#ATTRIBUTES.name#") AND lcase(ATTRIBUTES.append) EQ "after">
<!---дописать в конец фрагмента разметки с именем name,, сгенерированного layout:page, например, controls--->
<cfset "layoutTemplate.#ATTRIBUTES.name#" = layoutTemplate["#ATTRIBUTES.name#"] & value/>
<cfelse>
<!---заменить фрагмент разметки с именем name, сгенерированный layout:page--->
<cfset "layoutTemplate.#ATTRIBUTES.name#"= value/>
</cfif>
<cfset thisTag.generatedContent=""/>
</cfif>
</cfsilent>
+43
View File
@@ -0,0 +1,43 @@
<cfsilent>
<cfimport prefix="m" taglib="../lib"/>
<cfimport prefix="c" taglib="../lib/controls"/>
<!---*** идея включить титул в метаданные была не совсем пустой, но когда титул определяется в селекте, это не слишком удобно --->
<cfassociate basetag="cf_grid_head" datacollection="headersArray"/>
<cfset envelope=getBaseTagData('cf_grid_head',1)/>
<cfparam name="ATTRIBUTES.title" type="string" default="#envelope.ATTRIBUTES.titleMap[ATTRIBUTES.name].title#"/>
<cfparam name="ATTRIBUTES.name" type="string"/><!--- Заголовок указвается на том поле, по которому будет сортировка, а не на том, которе отображается --->
<cfparam name="ATTRIBUTES.arrows" type="boolean" default="Yes"/>
<cfset attributeList="name,arrows,title"/>
<!--- В данном случае реализован подход: дополнительные атрибуты передаются вниз.
Другой вариант - создавать атрибут extra, в который писать суффикс QueryString.
Первый подход, использованный здесь, позволяет иметь доступ к дополнительным атрибутам, но непонятно, зачем - ведь их имена все равно неизвестны.
Очевидный минус этого подхода - возможный конфликт имен с именами существующих атрибутов --->
<cfset extraAttributes=structNew()/>
<!--- дополнительные атрибуты родительского тега--->
<cfif StructKeyExists(envelope, "extraAttributes") AND isStruct(envelope.extraAttributes)>
<cfloop collection=#envelope.extraAttributes# item="attr">
<cfif NOT listFindNoCase(attributeList, attr)><!--- проверка относительно локальных атрибутов, чтобы их не перетереть--->
<cfset val="#structFind(envelope.extraAttributes, attr)#"/>
<cfif IsSimpleValue(val)>
<cfset structInsert(extraAttributes, attr, val)/>
</cfif>
</cfif>
</cfloop>
</cfif>
<cfloop collection=#ATTRIBUTES# item="attr">
<cfif NOT listFindNoCase(attributeList, attr)>
<cfset val="#structFind(ATTRIBUTES, attr)#"/>
<cfif IsSimpleValue(val)>
<cfset structInsert(extraAttributes, attr, val, "Yes")/><!--- локальные атрибуты перетрут родительские --->
</cfif>
</cfif>
</cfloop>
</cfsilent><cfif thisTag.executionMode is "end"><cfoutput>#ATTRIBUTES.title#
<cfif ATTRIBUTES.arrows><c:order_arrows sortArray=#envelope.ATTRIBUTES.sortArray# n=#envelope.ATTRIBUTES.titleMap[ATTRIBUTES.name].ordinal# attributeCollection=#extraAttributes#/></cfif></cfoutput></cfif>
+10
View File
@@ -0,0 +1,10 @@
<!--- It is strongly recommended to keep this tag closed. Unclosed tag generates output into the variable ATTRIBUTES.output only, which may be confusing --->
<cfif thisTag.executionMode IS "end" OR NOT thisTag.hasEndTag>
<cfparam name="ATTRIBUTES.backUrl" type="string" default="."/>
<cfparam name="ATTRIBUTES.enable" type="boolean" default="No"/>
<cfoutput>
<button type="submit" name="save" class="maincontrol"<cfif NOT ATTRIBUTES.enable> disabled</cfif>>#request.i18("Сохранить","Save")#</button>
<button type="submit" name="saveAndClose" class="maincontrol"<cfif NOT ATTRIBUTES.enable> disabled</cfif>>#request.i18("Сохранить и закрыть","Save and Close")#</button>
<button type="button" name="close" class="maincontrol" onClick="document.location.href='#ATTRIBUTES.backUrl#'">#request.i18("Закрыть","Close")#</button>
</cfoutput>
</cfif>
+10
View File
@@ -0,0 +1,10 @@
<cfsilent>
<cfparam name="ATTRIBUTES.title" default=""/>
<!---
<cfset var content=thisTag.generagedContent/>
--->
</cfsilent><!---
---><cfif thisTag.executionMode is "start"><div class="tr">
<div class="th"><cfoutput>#ATTRIBUTES.title#</cfoutput></div><div class="td">
<cfelseif thisTag.executionMode is "end"></div></div></cfif>
+8
View File
@@ -0,0 +1,8 @@
<!--- It is strongly recommended to keep this tag closed. Unclosed tag generates output into the variable ATTRIBUTES.output only, which may be confusing --->
<cfif thisTag.executionMode IS "end" OR NOT thisTag.hasEndTag>
<cfparam name="ATTRIBUTES.backUrl" type="string" default=""/>
<cfoutput>
<button type="submit" name="saveAndClose" class="maincontrol">#request.i18("Применить","Apply")#</button>
<button type="submit" name="resetAndClose" class="maincontrol">#request.i18("Снять","Reset")#</button>
<button type="button" name="close" onClick="document.location.href='#ATTRIBUTES.backUrl#'" class="maincontrol">#request.i18("Закрыть","Close")#</button></cfoutput>
</cfif>
+24
View File
@@ -0,0 +1,24 @@
<!--- It is strongly recommended to keep this tag closed. Unclosed tag generates output into the variable ATTRIBUTES.output only, which may be confusing --->
<!---v0.2 10:07 02.01.2019 - skipped url params--->
<cfif thisTag.executionMode IS "end" OR NOT thisTag.hasEndTag>
<cfparam name="ATTRIBUTES.filterUrl" type="string" default=""/>
<cfparam name="ATTRIBUTES.filter" type="struct" default=#structNew()#/>
<cfparam name="ATTRIBUTES.fwx" type="string" default=""/>
<cfset qString="#ATTRIBUTES.fwx#"/>
<!---Чтобы передавать параметры в URL (и не пользоваться сессией), надо правильно форматировать параметры всех типов--->
<!--- <cfloop collection=#ATTRIBUTES.filter# item="item">
<cftry>
<cfset fltr=structFind(ATTRIBUTES.filter,item)/>
<cfset qString=listAppend(qString, "#item#=#fltr.val#", "&")/>
<cfcatch type="ANY"></cfcatch>
</cftry>
</cfloop>--->
<cfset filterOn=request.filterOn(ATTRIBUTES.filter)/>
<!--- actually, any non-empty string can be assigned to reset_filter --->
<cfoutput><a href="#ATTRIBUTES.filterUrl#?#qString#"><cfif filterOn><b>#request.i18("Фильтр...","Filter")#</b><cfelse>#request.i18("Фильтр","Filter")#</cfif></b></a>
<cfif filterOn><a href="#request.thisPage#?reset_filter=yes">(#request.i18("снять","reset")#)</a></cfif></cfoutput>
</cfif>
+18
View File
@@ -0,0 +1,18 @@
<cfsilent>
<cfparam name="ATTRIBUTES.titleMap" default=#structNew()#/>
<cfparam name="ATTRIBUTES.sortArray" default=#arrayNew(1)#/>
<cfset attributeList="titleMap,sortArray"/>
<cfset extraAttributes=structNew()/>
<cfloop collection=#ATTRIBUTES# item="attr">
<cfif NOT listFindNoCase(attributeList, attr)>
<cfset val="#structFind(ATTRIBUTES, attr)#"/>
<cfif IsSimpleValue(val)>
<cfset structInsert(extraAttributes, attr, val)/>
</cfif>
</cfif>
</cfloop>
</cfsilent><!---
---><cfif thisTag.executionMode is "start"><tr><cfelseif thisTag.executionMode is "end"></tr></cfif>
+82
View File
@@ -0,0 +1,82 @@
<cfsilent>
<cfimport prefix="m" taglib="../lib"/>
<cfimport prefix="c" taglib="../lib/controls"/>
<cfparam name="ATTRIBUTES.useSummary" default="yes"/>
<cfparam name="ATTRIBUTES.addtionalUrlParams" type="string" default=""/>
<cfparam name="ATTRIBUTES.self" default=""/>
<cfparam name="ATTRIBUTES.recordCount" default="0"/>
<cfparam name="ATTRIBUTES.totalCount" default="0"/>
<cfparam name="ATTRIBUTES.recordsPerPage" default="500"/>
<cfparam name="ATTRIBUTES.footerOut" default=""/>
<cfparam name="ATTRIBUTES.excelLink" default="No"/>
<cfparam name="ATTRIBUTES.jsonLink" default="No"/>
<cfparam name="ATTRIBUTES.buttonNew" default="No"/>
<cfparam name="ATTRIBUTES.buttonNewTitle" default="Создать"/>
<cfparam name="ATTRIBUTES.urlNew" default=""/>
<cfset UrlSuffix=""/>
<cfif len(ATTRIBUTES.addtionalUrlParams) GT 0>
<cfset UrlSuffix="&#ATTRIBUTES.addtionalUrlParams#"/>
</cfif>
<cfif len(ATTRIBUTES.self) GT 0>
<cfset UrlSuffix="&track=#ATTRIBUTES.self#"/>
</cfif>
<c:paginator
thisPage=#request.thisPage#
recordCount=#ATTRIBUTES.recordCount#
recordsPerPage=#ATTRIBUTES.recordsPerPage#
self=#ATTRIBUTES.self#
addtionalUrlParams=#ATTRIBUTES.addtionalUrlParams#
output="paginator"/>
</cfsilent>
<cfoutput>
<cfif ATTRIBUTES.useSummary>
<div class="table wide" style="margin:3px 0;">
<div class="td" style="height:2em; line-height:2em; padding-left:0; vertical-align:middle;">
<cfif ATTRIBUTES.buttonNew>
<button type="button" class="maincontrol" onclick="document.location.href='#ATTRIBUTES.urlNew#'" style="margin:.0 1.5em 0 0;">
<a href="#ATTRIBUTES.urlNew#">#ATTRIBUTES.buttonNewTitle#</a>
</button>
<cfelse>
<div style="display:inline-block; width:1em;"/>&nbsp;</div>
</cfif>
<cfif (ATTRIBUTES.recordCount GE 0)>
Выбрано <b>#ATTRIBUTES.recordCount#</b>
<cfif (ATTRIBUTES.totalCount GE 0)>
из <b>#ATTRIBUTES.totalCount#</b>
</cfif>
</cfif>
<cfif ATTRIBUTES.excelLink>
<a href="#request.thisPage#?output_xls#UrlSuffix#" title="экспорт в Excel" style="margin-left:.5em; height:100%;" target="_blank"><img src="img/xls.gif" style="vertical-align:text-bottom;"/></a>
</cfif>
<cfif ATTRIBUTES.jsonLink>
<a href="#request.thisPage#?output_json#UrlSuffix#" title="экспорт в json" style="margin-left:.5em; height:100%;" target="_blank"><img src="img/json.svg" style="vertical-align:text-bottom;" width="13" height="13"/></a>
</cfif>
</div>
<div class="td r">
#paginator.links#
</div>
</div>
</cfif>
</cfoutput>
<cfif len(ATTRIBUTES.footerOut)>
<cfsavecontent variable="CALLER.#ATTRIBUTES.footerOut#">
<cfoutput>
<div class="wide r" style="margin:0.3em">
#paginator.links#
</div>
</cfoutput>
</cfsavecontent>
</cfif>
<cfexit method="exittag"/>
+99
View File
@@ -0,0 +1,99 @@
<cfsilent>
<cfparam name="ATTRIBUTES.qRead" type="query"/>
<cfparam name="ATTRIBUTES.convertSnakeToCamel" type="boolean" default=true/>
<!---<cfdump var=#qRead#/>
<cfdump var=#ATTRIBUTES.titleMap#/>
<cfabort/>--->
<cffunction name="snake2camel"
returntype="string"
output="false"
hint="convert snake style name to camel style name">
<cfargument name="snake" type="string" required="true" />
<cfreturn #reReplace(ARGUMENTS.snake,"_([a-z])","\u\1","ALL")#/>
</cffunction>
<cffunction name="camel2snake"
returntype="string"
output="false"
hint="convert camel style name to snake style name">
<cfargument name="camel" type="string" required="true" />
<cfreturn #reReplace(ARGUMENTS.camel,"([A-Z])","_\l\1","ALL")#/>
</cffunction>
<!--- <cffunction name="escape4json"
returntype="string"
output="false"
hint="escape string for json format">
<cfargument name="s" type="string" required="true" />
<cfreturn #reReplace(ARGUMENTS.camel,"([A-Z])","_\l\1","ALL")#/>
</cffunction> --->
<!--- <cffunction name="query2array">
<cfargument name="qry" type="query" required="true"/><!--- column names should not contain commas --->
<cfargument name="convertSnakeToCamel" type="boolean" default=true/><cfsilent>
</cffunction> --->
<!--- <cffunction name="query2json"
output="true"
hint="print query in json format as array of structures">
<cfargument name="qry" type="query" required="true"/><!--- column names should not contain commas --->
<cfargument name="convertSnakeToCamel" type="boolean" default=true/><cfsilent>
<cfset var columnsIn=#qry.columnList()#/>
<cfset var columnsOut=""/>
<cfif arguments.convertSnakeToCamel>
<cfloop list=#columnsIn# item="col">
<cfset columnsOut=listAppend(columnsOut,snake2camel(col))/>
</cfloop>
<cfelse>
<cfset columnsOut=#columnsIn#/>
</cfif>
</cfsilent>[<cfoutput query=#qry#>{<cfloop index="i" from="1" to=#listLen(columnsOut)#>"#listGetAt(#columnsOut#,i)#":"#qry[listGetAt(#columnsIn#,i)]#"<cfif #i# LT #listLen(columnsOut)#>,</cfif></cfloop>}<cfif #qry.currentRow()# LT #qry.recordCount()#>,</cfif></cfoutput>]
</cffunction> --->
<cffunction name="query2json"
output="false"
hint="print query in json format as array of structures">
<cfargument name="qry" type="query" required="true"/><!--- column names should not contain commas --->
<cfargument name="convertSnakeToCamel" type="boolean" default=true/>
<cfset var columnsIn=#arguments.qry.columnList()#/>
<cfset var columnsOut=""/>
<cfif arguments.convertSnakeToCamel>
<cfloop list=#columnsIn# item="col">
<cfset columnsOut=listAppend(columnsOut,snake2camel(col))/>
</cfloop>
<cfset var outArray=arrayNew(1)/>
//copy fields to structure, renaming fields, this preserves data type
<cfloop query=#arguments.qry#>
<cfset var structRow=structNew()/>
<cfloop index="i" from="1" to=#listLen(columnsOut)#>
<cfset structInsert(structRow,"#listGetAt(#columnsOut#,i)#",arguments.qry["#listGetAt(#columnsIn#,i)#"])/>
</cfloop>
<cfset arrayAppend(outArray,structRow)/>
</cfloop>
<cfreturn serializeJSON(outArray)/>
<cfelse>
<cfreturn serializeJSON(arguments.qry,"struct")/>
</cfif>
</cffunction>
<!--- <cfset request.camel2snake = camel2snake/> --->
<!---<cfsavecontent variable="strXmlData">--->
<!--- <cfif ATTRIBUTES.snake2camel>
<cfloop collection=#ATTRIBUTES.qRead.columnList# item="col">
<cfset queryRenameColumn(ATTRIBUTES.qRead,col,snake2camel(col))/>
<cfset ATTRIBUTES.qREAD.
</cfloop>
</cfif> --->
<cfset msStartAt=getTickCount()/>
</cfsilent><cfcontent
type="application/json"
/><cfoutput>#query2json(ATTRIBUTES.qRead,ATTRIBUTES.convertSnakeToCamel)#</cfoutput><!---
,<cfoutput>"duration":"#(getTickCount()- msStartAt)#"</cfoutput>} --->
<cfexit method="exittag"/>
+9
View File
@@ -0,0 +1,9 @@
<cfif thisTag.executionMode IS "end" OR NOT thisTag.hasEndTag>
<cfoutput>
&nbsp;
&nbsp;
&nbsp;
<a href="?language=ru"<cfif request.language EQ 'ru'> style="font-weight:bold;"</cfif>>Ru</a>
<a href="?language=en"<cfif request.language EQ 'en'> style="font-weight:bold;"</cfif>>En</a>
</cfoutput>
</cfif>
+240
View File
@@ -0,0 +1,240 @@
<cfsilent><!--- *** http://www.bennadel.com/blog/457-a-case-against-coldfusion-custom-tag-page-wrappers.htm--->
<cfimport prefix="layout" taglib="../layout"/>
<cfimport prefix="c" taglib="../lib/controls"/>
<cfparam name="ATTRIBUTES.section" type="string"/>
<cfparam name="ATTRIBUTES.closeForm" type="boolean" default="No"/>
<cfparam name="title" default=""/><!---заголовок страницы, отображаемый в теле например, <h1>--->
<cfparam name="pageTitle" default="PAYG"/><!---заголовок окна браузера, в теге <title>--->
<cfparam name="controls" default=""/>
</cfsilent><cfsilent>
<!---Тег layout:page проглатывает собственный контент, и вместо него после закрытия выводит разметку на основании атрибутов (при открытии тега он формирует переменные - фрагменты разметки - по шаблону) и вложенных тегов layout:attribute (которые по закрытии тега на самом деле перезаписывают переменные в layout:page).
Вложенные теги удобны, чтобы передавать читабельную разметку, а не строку в кавычках.--->
<cfif thisTag.executionMode IS "start">
<!--- Формируется значение переменной controls--->
<!--- Но оно может быть переопределено вызовом layout:attribute в теле тега <layout:page>--->
<cfif #ATTRIBUTES.section# EQ "header">
<cfparam name="ATTRIBUTES.pageInfo" type="struct"/><!--- инициализируется вызывающей страницей --->
<cfsavecontent variable="controls">
<cfoutput>
<cfswitch expression=#ATTRIBUTES.pageInfo.getType()#>
<cfcase value="ls">
<layout:filter_link filterUrl="#ATTRIBUTES.pageInfo.entity#_filter.cfm" filter=#ATTRIBUTES.pageInfo.settings.filter# fwx="#ATTRIBUTES.pageInfo.track.fwx#" />
</cfcase>
<cfcase value="detail">
<layout:detail_buttons backUrl="#ATTRIBUTES.pageInfo.track.backUrl#" enable="#ATTRIBUTES.pageInfo.writePermitted()#"/>
<!---<cfset ATTRIBUTES.form="1"/>--->
</cfcase>
<cfcase value="filter">
<layout:filter_buttons backUrl="#ATTRIBUTES.pageInfo.track.backUrl#"/>
<!---<cfset ATTRIBUTES.form="1"/>--->
</cfcase><!--- *** not implemented
<cfcase value="del">
<layout:del_buttons backUrl="#ATTRIBUTES.pageInfo.track.backUrl#"/>
<cfset ATTRIBUTES.form="1"/>
</cfcase>--->
<cfdefaultcase>
<!---<cfthrow message="Unsupported page type" detail="Supported page types are ls, detail, filter, del"/>--->
</cfdefaultcase>
</cfswitch>
<!---<layout:language_switch/>--->
</cfoutput>
</cfsavecontent>
</cfif>
</cfif>
<!--- Переменную (фрагмент разметки) title по традиции заполняем в вызывающей странице через layout:attribute--->
</cfsilent><!---
<!---*******************************************--->
---><cfif thisTag.executionMode IS "end"><!---
---><cfsilent>
<!---никакая разметка из тела даннного тега не выводится,
вместо этого по закрытии тега им генерируется собственный контент--->
<!---внутри тега на странице определяются его параметры, некорректно названные атрибутами--->
<cfset thisTag.generatedContent=""/><!---
---></cfsilent><!---
---><cfswitch expression=#ATTRIBUTES.section#><!---
---><cfcase value="header"><cfsilent>
</cfsilent><!--- ----------------------------------------------------------------------------
---------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------
---><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en" xml:lang="en" xmlns= "http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/><!--- this should be the first line in head --->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<script type="text/javascript">
var timerStart = Date.now();
</script>
<!---<cfquery name="qUsr" datasource="#request.DS#">
select login, firstname, middlename, lastname from worker where id='#request.usr_id#'
</cfquery>--->
<title><cfoutput>#pageTitle#</cfoutput></title>
<link rel="shortcut icon" href="img/favicon.ico"/>
<!--- <link rel="stylesheet" href="style/fontawesome-all.css"/> --->
<link rel="stylesheet" type="text/css" href="style/web.css"/>
<!--- override layout header style --->
<!--- <cfswitch expression=#request.STAND#>
<cfcase value="DEV">
<style>.title, .main-controls {background-color:#aaf}</style>
</cfcase>
<cfcase value="TEST">
<style>.title, .main-controls {background-color:#9e9}</style>
</cfcase>
<cfcase value="PROD">
<style>.title, .main-controls {background-color:#faa}</style>
</cfcase>
<cfdefaultcase>
<style>.title, .main-controls {background-color:#aaa}</style><!--- без изменений --->
</cfdefaultcase>
</cfswitch> --->
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="js/jquery.searchabledropdown.js"></script>
<script>
function resetListsById(id){
for (var i = 0, j = arguments.length; i < j; i++) {
try {
document.getElementById(arguments[i]).selectedIndex=-1;
} catch (e) {}
}
}
window.onload=function(){document.body.style.cursor='default';}
</script>
</head>
<body style="cursor:wait;"><a name="up"></a> <!--- <cfdump var=#ATTRIBUTES.pageInfo.writePermitted()#/> --->
<div class="page">
<div class="page-main">
<div class="menu noprint" style="margin:0; padding:0;/*border:1px solid red;*/">
<div class="fixed-menu" style="width:145px; height:100%; padding:0 0 0 0; --border:1px solid blue;" />
<a name="up"></a>
<!---<a id="lime_ref" href="http://root" style="display:inline-block; margin: 0; padding: 0 6px; height: 30px; width: 48px; margin: 5px 0 0 0;" title="Home"><img src="img/alarm-arrow-up-icon.png" style="margin: 0; padding: 0; height: 30px; margin: 5px 0 0 0;"/></a>
--->
<!--- <div style="width:20px; height:30px; margin: 5px; background: url('img/logo0.png') 0 0 no-repeat; background-size:contain; display:inline-block;"></div>--->
<div style="width:138px; height:30px; margin: 5px 0 0 5px; background: url('img/logo0.png') 0 0 no-repeat; background-size:contain; display:inline-block; -border:1px solid red;">
<p class="b r" style="color:white; margin:0 10px 10px 10px; font-size:130%;">
Отчет PAYG
</p>
</div>
<p style="color:lightgray; margin:10px 12px; text-align:right;">
<cfoutput>v.#request.APP_VERSION#</cfoutput>
</p>
<style>
.menu-items-container::-webkit-scrollbar {width: 10px; margin:10px;}
}
.menu-items-container::-webkit-scrollbar-track {
background-color: lightgray;
}
.menu-items-container::-webkit-scrollbar-thumb {
--background-color: rgb(97,127,136);/*lightgray*/;
--border-radius: 2px;
--border: 1px solid white;
}
</style>
<div class="menu-items-container" style="width:100%; height:100%; overflow:auto; scrollbar-width:thin; --border:1px solid green;">
<cfmodule template="../inc/menu.cfm" fwx="#ATTRIBUTES.pageInfo.track.fwx#" thisUrl="#ATTRIBUTES.pageInfo.track.thisUrl#"/>
<br/>
<br/>
<br/>
</div>
</div>
</div>
<!-- content -->
<div class="content">
<cfif isInstanceOf(ATTRIBUTES.pageInfo, "form_page_info")>
<cfoutput>
<form name="#ATTRIBUTES.pageInfo.formName#" action="#ATTRIBUTES.pageInfo.formAction#" method="#ATTRIBUTES.pageInfo.formMethod#" <!---style="height:100%;"---><cfif len(ATTRIBUTES.pageInfo.formEncType)> enctype="#ATTRIBUTES.pageInfo.formEncType#"</cfif>><!--- height:100% is required to make inner elements fill the container vertically ---><!--- but if we close the form in the middle of the page, layout is broken --->
</cfoutput>
</cfif>
<div class="header">
<div class="control-bar">
<!--- <div style="display:table-cell; width:23px; background: #ccc/*rgb(117,147,156)*/; border: 1px solid #bbb;"> --->
<!--- <div style="display:table-cell; width:23px; background: #0078c9; border: 1px solid #000;"> --->
<!--- <div style="display:table-cell; width:23px; background: #07b; border: 1px solid #000;"></div> --->
<!--- <div style="display:table-cell; width:3px;"></div> --->
<div class="title">
<cfoutput>#title#</cfoutput>
</div>
<div class="main-controls">
<cfoutput>#controls#</cfoutput>
</div>
</div><!--- control-bar --->
</div>
<div class="layout_vspacer"></div>
<cfif len(ATTRIBUTES.pageInfo.status.errorState)>
<span style="color:red"><cfoutput>#ATTRIBUTES.pageInfo.status.errorMessage#</cfoutput></span>
<cfexit method="exittag"/>
</cfif>
<!-- content -->
</cfcase><cfcase value="extension"><!--- -------------------------------------------------------------------------------------------- --->
<cfif ATTRIBUTES.closeForm></form></cfif>
</cfcase><cfcase value="footer"><!--- -------------------------------------------------------------------------------------------- --->
<!--/content -->
<cfif ATTRIBUTES.closeForm></form></cfif>
</div>
<!--/content -->
</div><!--- page main --->
<div class="page-footer noprint">
<div style="display:table-cell; padding:.5em; background-color:#555;">
<cftry>
<cfset pageTimeMs=GetTickCount()-request.startTickCount/>
<cfoutput><span title="page generation time">#pageTimeMs# ms</span></cfoutput>
<cfcatch type="any">request.startTickCount undefined</cfcatch>
</cftry>
<span id="load-time" title="page loading time"></span>
<script type="text/javascript">
window.onload=function() {
document.body.style.cursor='default';
document.getElementById("load-time").innerHTML = (Date.now()-timerStart) + " ms";
};
<!---alert(document.documentMode);// IE troubles --->
</script>
</div>
<div style="display:table-cell; padding: 3px;">
<div class="footer">
<cftry>
<cflock scope="session" type="readonly" timeout="3">
<cfif len(session.authentication_source)>
<cfoutput>authentication:#session.authentication_source#</cfoutput>&nbsp;
</cfif>
</cflock><cfcatch type="ANY"></cfcatch></cftry>
<a href="#up" class="up">вверх</a>
</div>
</div>
</div>
</div><!--- page --->
<!--- fixed-menu class is defined at the bottom of page, so in case of unhandled error menu is not fixed --->
<style>
div.fixed-menu { position:fixed;}
</style>
</body>
</html><!---
---></cfcase><cfdefaultcase><cfthrow message="Unsupported 'section' attribute value" detail="Supported section values are 'header','extension','footer'"></cfdefaultcase></cfswitch><!---
---></cfif>
+19
View File
@@ -0,0 +1,19 @@
<cfparam name="ATTRIBUTES.query" type="query"/>
<cfif thisTag.executionMode IS "end" OR NOT thisTag.hasEndTag>
<table class="worktable">
<tr>
<cfoutput>
<cfloop list=#queryColumnList(ATTRIBUTES.query)# item="col">
<th>#col#</th>
</cfloop>
</cfoutput>
</tr>
<cfoutput query=#ATTRIBUTES.query#>
<tr>
<cfloop list=#queryColumnList(ATTRIBUTES.query)# item="col">
<td>#ATTRIBUTES.query[col]#</td>
</cfloop>
</tr>
</cfoutput>
</table>
</cfif>
+55
View File
@@ -0,0 +1,55 @@
<!---https://www.bennadel.com/blog/461-creating-microsoft-excel-documents-with-coldfusion-and-xml.htm--->
<!---*** locale-specific--->
<cfparam name="ATTRIBUTES.qRead" type="query"/>
<cfparam name="ATTRIBUTES.titleMap" type="struct"/>
<cfparam name="ATTRIBUTES.sheetTitle" type="string" default="Sheet1"/>
<cfparam name="ATTRIBUTES.filename" type="string" default="export.xls"/>
<cfset fieldArray=structSort(ATTRIBUTES.titleMap, "numeric", "ASC", "ordinal")/>
<cfsavecontent variable="strXmlData">
<cfoutput>
<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:x='urn:schemas-microsoft-com:office:excel' xmlns='http://www.w3.org/TR/REC-html40'><meta http-equiv="content-type" content="application/vnd.ms-excel; charset=UTF-8"><meta http-equiv="content-type" content="application/vnd.ms-excel; charset=UTF-8"><head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>#ATTRIBUTES.sheetTitle#</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--></head><body><table><!---
<cfloop index="i" from="1" to=#structCount(ATTRIBUTES.titleMap)#>
<Column ss:Index="#i#"/>
</cfloop>
--->
<tr>
<cfloop from=1 to=#arrayLen(fieldArray)# index="i">
<cfset title=structFind(ATTRIBUTES.titleMap, fieldArray[i]).title/>
<th><cfif len(title)>#title#<cfelse>#fieldArray[i]#</cfif></th>
</cfloop>
</tr>
<cfloop query=#ATTRIBUTES.qRead#>
<tr>
<cfloop from=1 to=#arrayLen(fieldArray)# index="i">
<cfset data=#ATTRIBUTES.qRead[fieldArray[i]]#/>
<cftry>
<cfif isDate(data)><td>#dateFormat(data,'YYYY-MM-DD')#<cfif hour(data) GT 0 OR minute(data) EQ 0> #timeFormat(data,'HH:mm')#</cfif></td><cfelseif isNumeric(data)><td>#Replace(data, '.', ',')#</td><cfelse><td>#data#</td></cfif>
<cfcatch type="Any"><td>#data#</td></cfcatch>
</cftry>
</cfloop>
</tr>
</cfloop>
</table></body></html>
</cfoutput>
</cfsavecontent>
<cfheader
name="content-disposition"
value="attachment; filename=#ATTRIBUTES.filename#"
/>
<!---
When streaming the Excel XML data, trim the data and
replace all the inter-tag white space. No need to stream
any more content than we have to.
--->
<cfcontent
type="application/msexcel"
variable="#ToBinary( ToBase64( strXmlData.Trim().ReplaceAll( '>\s+', '>' ).ReplaceAll( '\s+<', '<' ) ) )#"
/><cfabort/>
<cfexit method="exittag"/>
+49
View File
@@ -0,0 +1,49 @@
<!---https://www.bennadel.com/blog/461-creating-microsoft-excel-documents-with-coldfusion-and-xml.htm--->
<cfparam name="ATTRIBUTES.qRead" type="query"/>
<cfparam name="ATTRIBUTES.titleMap" type="struct"/>
<cfparam name="ATTRIBUTES.sheetTitle" type="string" default="Sheet1"/>
<cfparam name="ATTRIBUTES.filename" type="string" default="export.xlsx"/>
<cfsavecontent variable="strXmlData">
<cfoutput>
<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:x='urn:schemas-microsoft-com:office:excel' xmlns='http://www.w3.org/TR/REC-html40'>
<meta http-equiv="content-type" content="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet">
<head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>xlsWorksheetName</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--></head>
<body>
<table>
<tr>
<cfloop struct=#ATTRIBUTES.titleMap# item="field">
<th>#structFind(ATTRIBUTES.titleMap,field).title#</th>
</cfloop>
</tr>
<cfloop query=#ATTRIBUTES.qRead#>
<tr>
<cfloop struct=#ATTRIBUTES.titleMap# item="field">
<td>
#ATTRIBUTES.qRead[field]#
</td>
</cfloop>
</tr>
</cfloop>
</table>
</body>
</html>
</cfoutput>
</cfsavecontent>
<cfheader
name="content-disposition"
value="attachment; filename=#ATTRIBUTES.filename#"
/>
<!---
When streaming the Excel XML data, trim the data and
replace all the inter-tag white space. No need to stream
any more content than we have to.
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
--->
<cfcontent
type="application/vnd.ms-excel"
variable="#ToBinary( ToBase64( strXmlData.Trim().ReplaceAll( '>\s+', '>' ).ReplaceAll( '\s+<', '<' ) ) )#"
/>
<cfexit method="exittag"/>
+239
View File
@@ -0,0 +1,239 @@
<cfsilent>
<!---https://www.bennadel.com/blog/461-creating-microsoft-excel-documents-with-coldfusion-and-xml.htm--->
<!---порядок колонок, как в селекте, не управляется никак
Для определения порядка колонок используется поле ATTRIBUTES.titleMap.ordinal, возможно, не самый надежный способ
Можно попробовать использовать порядок колонок в массиве query --->
<cfparam name="ATTRIBUTES.qRead" type="query"/>
<cfparam name="ATTRIBUTES.titleMap" type="struct"/><!---see ../lib/data/field_set.cfm--->
<cfparam name="ATTRIBUTES.sheetTitle" type="string" default="Sheet1"/>
<cfparam name="ATTRIBUTES.filename" type="string" default="export.xlsx"/>
<cfset AVG_SYMBOL_WIDTH=6.5/>
<cfset MAX_COL_LENGTH=30/>
<cfset qRead=ATTRIBUTES.qRead/>
<!---<cfdump var=#qRead#/>
<cfdump var=#ATTRIBUTES.titleMap#/>
<cfabort/>
--->
<!---<cfsavecontent variable="strXmlData">--->
</cfsilent><cfheader
name="content-disposition"
value="attachment; filename=#ATTRIBUTES.filename#"
/><cfcontent
type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
/><cfoutput><?xml version="1.0"?>
<?mso-application progid="Excel.Sheet"?>
<!---
This is the Workbook root element. This element
stores characteristics and properties of the
workbook, such as the namespaces used in
SpreadsheetML.
--->
<Workbook
xmlns="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:html="http://www.w3.org/TR/REC-html40">
<!---
The first child element of the WorkBook element
is DocumentProperties. Office documents store
metadata related to the document&#8212;for example,
the author name, company, creation date, and
more in the DocumentProperties element.
--->
<DocumentProperties
xmlns="urn:schemas-microsoft-com:office:office">
<Author>xls-generator</Author>
<Company></Company>
</DocumentProperties>
<!---
The Styles node represents information related
to individual styles that can be used to format
components of the workbook.
--->
<Styles>
<!--- Basic format used by all cells. --->
<Style ss:ID="Default" ss:Name="Normal">
<Alignment ss:Vertical="Top"/>
<Borders/>
<Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11"/>
<Interior/>
<NumberFormat/>
<Protection/>
</Style>
<Style ss:ID="Th">
<Alignment ss:Horizontal="Center" ss:Vertical="Top"/>
<Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Bold="1"/>
</Style>
<Style ss:ID="Int">
<NumberFormat ss:Format="0" />
</Style>
<Style ss:ID="DateTime">
<NumberFormat ss:Format="dd/mm/yyyy\ hh:mm;@"/>
</Style>
<!---
<Style ss:ID="ShortDate">
<NumberFormat ss:Format="[ENG][$-409]d\-mmm\-yyyy;@" />
</Style>
<Style ss:ID="YesNo">
<NumberFormat ss:Format="Yes/No" />
</Style>
--->
</Styles>
<!---
This defines the first worksheeet and it's name.
We are only using one worksheet in this example,
but you could add more Worksheet nodes after
this one for multiple tabs. The "Name" attribute
here is the name that shows up in the tab.
--->
<Worksheet ss:Name="#ATTRIBUTES.sheetTitle#">
<Table
<!---
We need a column for each column of the
query. This attribute is required to be
correct. If the value here does NOT
match the data in Excel file, the
document will not render properly.
--->
ss:ExpandedColumnCount="#ListLen(qRead.ColumnList)#"
<!---
We need a row for every query record
plus one for the header row. Again, if
this value does not match what is in the
document, the excel file will not
render properly.
--->
ss:ExpandedRowCount="#(qRead.RecordCount + 1)#"
x:FullColumns="1"
x:FullRows="1">
<!---
Here, we can define general properties
regarding each column in the data output.
<Column ss:Index="1" ss:Width="30" />
<Column ss:Index="2" ss:Width="100" />
<Column ss:Index="3" ss:Width="42" />
<Column ss:Index="4" ss:Width="84" />
<Column ss:Index="5" ss:Width="66" />
<Column ss:Index="6" ss:Width="70" />--->
<cfsilent>
<cfset columns=arrayNew(1)/>
<cfset i=1/>
<cfloop struct=#ATTRIBUTES.titleMap# item="field">
<cfset col=structNew()/>
<cfset fieldSpec=structFind(ATTRIBUTES.titleMap, field)/>
<cfset col.name=field/>
<cfset col.title=fieldSpec.title/>
<cfif len(col.title) EQ 0>
<cfset col.title=col.name/>
</cfif>
<cfset col.titleLength=len(col.title)/>
<cfquery name="qLen" dbtype="query">
select coalesce(max(length(#field#)),0) as maxlen from qRead
</cfquery>
<cfset col.length=max(col.titleLength, qLen.maxLen)/>
<cfset col.cfSqlType="CF_SQL_VARCHAR"/>
<cfif structKeyExists(fieldSpec,"cfSqlType")>
<cfset col.cfSqlType=#fieldSpec.cfSqlType#/>
</cfif>
<cfswitch expression=#col.cfSqlType#><!---should use ## around variable name--->
<cfcase value="CF_SQL_BIGINT,CF_SQL_INTEGER,CF_SQL_SMALLINT,CF_SQL_TINYINT,CF_SQL_BIT,CF_SQL_NUMERIC,CF_SQL_DECIMAL,CF_SQL_REAL,CF_SQL_FLOAT,CF_SQL_DOUBLE">
<cfset col.ssType="Number"/>
</cfcase>
<cfcase value="CF_SQL_TIMESTAMP,CF_SQL_DATE,CF_SQL_TIME,CF_SQL_DATETIME">
<cfset col.ssType="DateTime"/>
</cfcase>
<cfdefaultcase>
<cfset col.ssType="String"/>
</cfdefaultcase>
</cfswitch>
<cfset columns[fieldSpec.ordinal]=col/><!---new col created at each iteration, so we have valid references--->
<cfset i=i+1/>
</cfloop>
</cfsilent>
<cfloop from=1 to=#arrayLen(columns)# index="i">
<cfif columns[i].ssType EQ "DateTime"><!--- timestamp string representation is too long, --->
<Column ss:Index="#i#" ss:Width="#round(min(columns[i].titleLength, MAX_COL_LENGTH)*AVG_SYMBOL_WIDTH)#" ss:AutoFitWidth="1"/>
<cfelse>
<Column ss:Index="#i#" ss:Width="#round(min(columns[i].length, MAX_COL_LENGTH)*AVG_SYMBOL_WIDTH)#" ss:AutoFitWidth="1"/>
</cfif>
</cfloop>
<!---
This is our header row. All cells in the
header row will be of type string.
--->
<Row>
<cfloop from=1 to=#arrayLen(columns)# index="i">
<Cell ss:StyleID="Th">
<Data ss:Type="String">#columns[i].title#</Data>
</Cell>
</cfloop>
</Row>
<cfflush/>
<cfloop query=#qRead#><!---
---><Row><!---
---><cfloop from=1 to=#ArrayLen(columns)# index="i"><!---
---><cfset field=columns[i].name/><!---
---><Cell<cfif columns[i].ssType EQ "DateTime"> ss:StyleID="DateTime"</cfif>><!---
---><cfif len(qRead[field])><Data ss:Type="#columns[i].ssType#"><!---no extra whitespace allowed here---><!---
---><cfswitch expression=#columns[i].ssType#><!---
---><cfcase value="DateTime"><!---
--->#dateFormat(qRead[field],"YYYY-MM-DD")#T#timeFormat(qRead[field],"HH:mm:ss.l")#<!---
---></cfcase><!---
---><cfcase value="Number"><!---
--->#qRead[field]#<!---
---></cfcase><!---
---><cfdefaultcase><!---
---><![CDATA[#request.clean4CDATA(qRead[field])#]]><!---
---></cfdefaultcase><!---
---></cfswitch><!---
---></Data></cfif><!---
---></Cell><!---
---></cfloop><!---
---></Row><!---
---></cfloop><!---
---></Table>
</Worksheet>
</Workbook>
</cfoutput>
<!---</cfsavecontent>--->
<!---
Define the way in which the browser should interpret
the content that we are about to stream.
--->
<!---<cfheader
name="content-disposition"
value="attachment; filename=#ATTRIBUTES.filename#"
/>--->
<!---
When streaming the Excel XML data, trim the data and
replace all the inter-tag white space. No need to stream
any more content than we have to..ReplaceAll( '>\s+', '>' ).ReplaceAll( '\s+<', '<' )
--->
<!---<cfcontent
type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
variable="#ToBinary( ToBase64( strXmlData.Trim()))#"
/>---><!---<cfabort/>--->
<cfexit method="exittag"/>
+1 -4
View File
@@ -183,10 +183,7 @@
<cffunction name="saveSettings" access="private">
<cfargument name=settings>
<cfif !isNumeric(request.usr_id)
OR !(request.usr_id GT 0)
OR request.usr_id EQ request.ANONYMOUS_USR_ID
OR request.usr_id EQ request.UNDEFINED_USR_ID>
<cfif NOT request.usrAuthenticated()>
<cfreturn />
</cfif>
+392
View File
@@ -0,0 +1,392 @@
<cfsilent>
<cfimport prefix="m" taglib="lib"/>
<cfimport prefix="c" taglib="lib/controls"/>
<cfimport prefix="d" taglib="lib/data"/>
<cfimport prefix="layout" taglib="layout"/>
</cfsilent>
<m:prepare_ls entity="payg" accessObject="" pageInfoOut="pageInfo" trackOut="tr"/>
<cfparam name="dt_finish" type="date" default=#createDateTime(year(Now()),month(Now()),1,0,0,0)#/>
<cfparam name="dt_start" type="date" default=#dateAdd('m',-1,dt_finish)#/>
<cfset hours=dateDiff('h',dt_start,dt_finish)/>
<cfset tenant_wz_index=val(mid(request.auth.WZ,3,5))/><!--- cut off WZ --->
<!--- Для начала выкатываем просто кусок каталога, не фильтруя по спеке (которой иногда нет) --->
<cfquery name="qSpec" datasource="rpt">
select * from (
values
('Объектное хранилище S3 CEPH', 'Дисковое пространство: Объем хранения','paas.s3.ceph.vol-m','ГБайт',1.80,0)
,('Объектное хранилище S3 CEPH','Исходящий трафик','paas.s3.ceph.trf-m','ГБайт',1.50,0)
,('Объектное хранилище S3 CEPH','Запросы GET, HEAD, OPTIONS','paas.s3.ceph.get-m','10тыс.шт.',0.35,0)
,('Объектное хранилище S3 CEPH','Запросы PUT, POST, PATCH','paas.s3.ceph.put-m','10тыс.шт.',4.50,0)
) as
s (svc, component, code, unit, price, discount)
order by code
</cfquery>
<!--- Нужно еще реализовать в модели данных политики бесплатного минимума --->
<cfquery name="qComputing" datasource="rpt">
select 'WZ'||to_char(tenant.wzcode,'FM00000') as wz, vdc.name as vdc_name,
case
when vdc.name like '%-v1cl1%' then 'iaas.ngc.i29'
when vdc.name like '%-v1cl2%' then 'iaas.ngc.i31'
when vdc.name like '%-v1cl4%' then 'iaas.ngc.a28'
when vdc.name like '%-v1cl6%' then 'iaas.ngc.i28'
else '['||vdc.name||']' end as code,
sum(cpu_used/cpu_speed)/4. as core_h,
sum(mem_used)/1024/4. as gb_h
from ngcloud_ru.capacity_resource
join ngcloud_ru.vdc on capacity_resource.vdc_id=vdc.id
join ngcloud_ru.tenant on tenant.id=vdc.tenant_id
where tenant.wzcode = <cfqueryparam cfsqltype="cf_sql_integer" value=#tenant_wz_index#/>
AND capacity_resource.timestamp >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dt_start#/>
AND capacity_resource.timestamp < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dt_finish#/>
group by tenant.wzcode, vdc.name
order by tenant.wzcode, vdc.name;
</cfquery>
<cfif isDefined("DEBUG")>
<cfdump var=#qComputing#/>
<cfflush/>
</cfif>
<cfquery name="qComputingAge" datasource="rpt">
select max(capacity_resource.timestamp) as dt_load from ngcloud_ru.capacity_resource
</cfquery>
<!--- переделать storage_profile_types с расшифровкой ssd, sata, можно прямо до каталожного кода --->
<cfquery name="qStorage" datasource="rpt">
select 'WZ'||to_char(tenant.wzcode,'FM00000') as wz, vdc.name as vdc_name,
case
WHEN vdc.name LIKE '%-v1cl1%' THEN 'iaas.ngc.i29'
WHEN vdc.name LIKE '%-v1cl2%' THEN 'iaas.ngc.i31'
WHEN vdc.name LIKE '%-v1cl4%' THEN 'iaas.ngc.a28'
WHEN vdc.name LIKE '%-v1cl6%' THEN 'iaas.ngc.i28'
ELSE '['||vdc.name||']' END
|| CASE
WHEN storage_profile_types.name ILIKE '%-SSD%' THEN '.ssd-m'
WHEN storage_profile_types.name ILIKE '%-SAS' THEN '.sas-m'
WHEN storage_profile_types.name ILIKE '%-SATA' THEN '.sata-m'
ELSE '.['|| storage_profile_types.name ||']' END
as code,
storage_profile_types.name, -- *** это не тип, а профайл
sum("limit")/1024./4 as gb_h_limit, -- 4 потому, что сбор раз в 15 минут
sum(used)/1024./4 as gb_h_used
from ngcloud_ru.capacity_storage
join ngcloud_ru.vdc on capacity_storage.vdc_id=vdc.id
join ngcloud_ru.tenant on tenant.id=vdc.tenant_id
join ngcloud_ru.storage_profile_types on capacity_storage.storage_profile_types_id=storage_profile_types.id
where tenant.wzcode = <cfqueryparam cfsqltype="cf_sql_integer" value=#tenant_wz_index#/>
AND capacity_storage.timestamp >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dt_start#/>
AND capacity_storage.timestamp < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dt_finish#/>
group by tenant.wzcode, vdc.name, storage_profile_types.name
order by tenant.wzcode, vdc.name, storage_profile_types.name;
</cfquery>
<cfquery name="qStorageAge" datasource="rpt">
select max(capacity_storage.timestamp) as dt_load from ngcloud_ru.capacity_storage
</cfquery>
<cfif isDefined("DEBUG")>
<cfdump var=#qStorage#/>
<cfflush/>
</cfif>
<!---
"placement" "size_gb" "get_10k" "put_10k" "bytes_sent_gb"
HOT_FREE_LIMIT 1 10 1 100
--->
<!--- надо дисконтировать не ГБ-часы а ГБ-месяцы --->
<cfquery name="qS3Vol" datasource="rpt">
select /* *** ниже дублирование кода агрегации - round ceil etc, обратить внимание при правке */
(sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>) as vol_B
,ceil(sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>/1024/1024/1024) as vol_GB
,greatest(ceil(sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>
/1024/1024/1024)-
case when bucket_stat.placement_id = 1 then 1 else 0 end, 0) as vol_GB_free_tier_subtracted /*считается не в точности так, как в функции*/
,case
when bucket_stat.placement_id = 1 then 'paas.s3.ceph'
when bucket_stat.placement_id = 2 then 'paas.s3.cphc'
else '['|| TO_CHAR(bucket_stat.placement_id,'FM9') ||']' end as code
--,bucket_stat.placement_id
,'WZ'||lpad(bucket_stat.owner, 5, '0') as wz -- *** здесь не число, а строка, поэтому другая формула обратной сборки WZ
from s3billing.bucket_stat
join s3billing.placement ON s3billing.placement.id=bucket_stat.placement_id
join s3billing.bucket_info ON bucket_info.id=bucket_stat.bucket_id
where bucket_stat.placement_id in (1,2)
AND bucket_stat.owner = <cfqueryparam cfsqltype="cf_sql_varchar" value=#tenant_wz_index#/>
AND bucket_stat.timestamp_addition >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dt_start#/>
AND bucket_stat.timestamp_addition < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dt_finish#/>
group by bucket_stat.owner, bucket_stat.placement_id
order by bucket_stat.owner, bucket_stat.placement_id;
</cfquery>
<cfif isDefined("DEBUG")>
<cfdump var=#qS3Vol#/>
<cfflush/>
</cfif>
<cfquery name="qS3VolAge" datasource="rpt">
select max(timestamp_addition) as dt_load from s3billing.bucket_stat
</cfquery>
<cfquery name="qS3OpsTrf" datasource="rpt">
select
sum(
CASE WHEN
(category.name LIKE 'get%'
OR category.name LIKE 'head%'
OR category.name LIKE 'options%'
) THEN ops ELSE 0 END
) as get
,ceil(sum(
CASE WHEN
(category.name LIKE 'get%'
OR category.name LIKE 'head%'
OR category.name LIKE 'options%'
) THEN ops ELSE 0 END
)/10000) as get_10k
,greatest(ceil(sum(
CASE WHEN
(category.name LIKE 'get%'
OR category.name LIKE 'head%'
OR category.name LIKE 'options%'
) THEN ops ELSE 0 END
)/10000) - CASE WHEN bucket_info.placement_id = 1 THEN 10 ELSE 0 END, 0
) as get_10k_free_tier_subtracted
,sum(
CASE WHEN
(category.name LIKE 'put%'
OR category.name LIKE 'post%'
OR category.name LIKE 'patch%'
OR category.name LIKE 'list%'
) THEN ops ELSE 0 END
) as put
,ceil(sum(
CASE WHEN
(category.name LIKE 'put%'
OR category.name LIKE 'post%'
OR category.name LIKE 'patch%'
OR category.name LIKE 'list%'
) THEN ops ELSE 0 END
)/10000) as put_10k
,greatest(ceil(sum(
CASE WHEN
(category.name LIKE 'put%'
OR category.name LIKE 'post%'
OR category.name LIKE 'patch%'
OR category.name LIKE 'list%'
) THEN ops ELSE 0 END
)/10000) - CASE WHEN bucket_info.placement_id = 1 THEN 1 ELSE 0 END, 0
) as put_10k_free_tier_subtracted
,(sum(bytes_sent)) as bytes_sent
,ceil(sum(bytes_sent)/1024/1024/1024) as bytes_sent_GB
,greatest(ceil(sum(bytes_sent)/1024/1024/1024) - CASE WHEN bucket_info.placement_id = 1 THEN 100 ELSE 0 END, 0
) as bytes_sent_GB_free_tier_subtracted
,case
when bucket_info.placement_id = 1 then 'paas.s3.ceph'
when bucket_info.placement_id = 2 then 'paas.s3.cphc'
else '['|| TO_CHAR(bucket_info.placement_id,'FM9') ||']' end as code --*** зачем делать там ключ bigint
,bucket_info.placement_id
--category.name, bucket_info.name
,'WZ'||lpad(user_info.name, 5, '0') as wz
from s3billing.usage_bucket_by_user
join s3billing.user_info on usage_bucket_by_user.user_id=user_info.id
join s3billing.category on usage_bucket_by_user.category_id=category.id
join s3billing.bucket_info on usage_bucket_by_user.bucketid=bucket_info.id
join s3billing.placement on bucket_info.placement_id=placement.id
where 1=1
AND user_info.name = <cfqueryparam cfsqltype="cf_sql_varchar" value=#tenant_wz_index#/>
AND usage_bucket_by_user.time >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dt_start#/>
AND usage_bucket_by_user.time < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dt_finish#/>
group by user_info.name, bucket_info.placement_id
order by user_info.name, bucket_info.placement_id;
</cfquery>
<cfif isDefined("DEBUG")>
<cfdump var=#qS3OpsTrf#/>
<cfflush/>
</cfif>
<cfquery name="qS3OpsTrfAge" datasource="rpt">
select max(time) as dt_load from s3billing.usage_bucket_by_user
</cfquery>
<cfquery name="qUnifiedMetric" dbType="query">
select wz, code || '.vcpu-m' as code, core_h as raw_metric, core_h as metric, core_h as chargeable_metric from qComputing
union all
select wz, code || '.ram-m' as code, gb_h as raw_metric, gb_h as metric, gb_h as chargeable_metric from qComputing
union all
select wz, code, gb_h_used as raw_metric, gb_h_used as metric, gb_h_used as chargeable_metric from qStorage
union all
select wz, code || '.vol-m', vol_b as raw_metric, vol_gb as metric, vol_GB_free_tier_subtracted as chargeable_metric from qS3Vol
union all
select wz, code || '.get-m', get as raw_metric, get_10k as metric, get_10k_free_tier_subtracted as chargeable_metric from qS3OpsTrf
union all
select wz, code || '.put-m', put as raw_metric, put_10k as metric, put_10k_free_tier_subtracted as chargeable_metric from qS3OpsTrf
union all
select wz, code || '.trf-m', bytes_sent as raw_metric, bytes_sent_gb as metric, bytes_sent_gb_free_tier_subtracted as chargeable_metric from qS3OpsTrf
</cfquery>
<cfif isDefined("DEBUG")>
<cfdump var=#qUnifiedMetric#/>
<cfflush/>
</cfif>
<cfquery name="qCharge" dbType="query">
SELECT
<d:field_set titleMapOut="titleMap" lengthOut="fieldCount">
<d:field title="Артикул">s.code</d:field>
<d:field title="Услуга">s.svc</d:field>
<d:field title="Компонент">s.component</d:field>
<d:field title="Кол-во (метрика)" cfSqlType="CF_SQL_NUMERIC">m.chargeable_metric</d:field>
<d:field title="Ед.изм.">s.unit</d:field>
<d:field title="Цена &##8381; с НДС" cfSqlType="CF_SQL_NUMERIC">s.price*(100-s.discount)/100 as discounted_price</d:field>
<d:field title="Стоимость &##8381; с НДС" cfSqlType="CF_SQL_NUMERIC">s.price*(100-s.discount)/100*m.chargeable_metric as charge</d:field>
<!--- <d:field title="Дата НОУ" cfSqlType="CF_SQL_TIMESTAMP">s.dt_from</d:field> --->
<!--- <d:field title="Дата оконч." cfSqlType="CF_SQL_TIMESTAMP">s.dt_to</d:field>
<d:field title="Ключ строки">s.line_key</d:field> --->
<d:field title="GPL с НДС" cfSqlType="CF_SQL_NUMERIC">s.price</d:field>
<d:field title="Скидка%" cfSqlType="CF_SQL_NUMERIC">s.discount</d:field>
<d:field title="Сырая метрика" cfSqlType="CF_SQL_NUMERIC">m.raw_metric</d:field>
<d:field title="Приведен&shy;ная метрика" cfSqlType="CF_SQL_NUMERIC">m.metric</d:field>
<!--- <d:field title="Клиентс. назв.">s.user_description</d:field> --->
</d:field_set>
FROM qSpec as s, qUnifiedMetric as m
WHERE m.wz=<cfqueryparam cfsqltype="cf_sql_varchar" value=#request.auth.wz#/> AND s.code=m.code
order by s.code
</cfquery>
<!--- looks like QoQ does not support join syntax, at least of Lucee 5.4--->
<cfif isDefined("output_xls")>
<layout:xml qRead=#qCharge# titleMap=#titleMap# filename="#pageInfo.entity#.xml"/>
<cfabort/>
</cfif><cfif isDefined("output_json")>
<layout:json qRead=#qCharge# titleMap=#titleMap# filename="#pageInfo.entity#.json"/>
<cfabort/>
</cfif><!---
---><layout:page section="header" pageInfo=#pageInfo#>
<layout:attribute name="title">
<cfoutput><b>Отчет по PAYG</b></cfoutput>
</layout:attribute>
<layout:attribute name="controls">
<!---skip filter link, filter is not implemented--->
<!---<layout:language_switch/>--->
</layout:attribute>
</layout:page>
<!--- Внимание! округление вверх при расчете метрик get, put S3. Расчетные суммы могут содержать погрешности округления, поэтому окончательный расчет стоимости (количество * цену) должен выполняться в бухгалтерской программе.
<b>Если у клиента больше 1 действующего допника с услугами PAYG, то данные в отчете могут быть замножены, в этом случае считать вручную</b> --->
<cfoutput>
<form method="post" action="">
Период с <input type="text" name="dt_start" value="#dateFormat(dt_start,'YYYY-MM-DD')#"/>
по <input type="text" name="dt_finish" value="#dateFormat(dt_finish,'YYYY-MM-DD')#"/>
<input type="submit" style="cursor:pointer;"/>
<!--- <input type="submit" name="DEBUG" value="DEBUG" style="cursor:pointer;"/> --->
</form>
Часов в периоде: <b>#hours#</b> Строк в отчете: <b>#qCharge.recordCount#</b><br/>
Актуальность данных:
VCD Computing: <b>#dateFormat(qComputingAge.dt_load,'YYYY-MM-DD')# #timeFormat(qComputingAge.dt_load,'HH:MM:SS')#</b>
VCD Storage: <b>#dateFormat(qStorageAge.dt_load,'YYYY-MM-DD')# #timeFormat(qStorageAge.dt_load,'HH:MM:SS')#</b>
S3 хранение: <b>#dateFormat(qS3VolAge.dt_load,'YYYY-MM-DD')# #timeFormat(qS3VolAge.dt_load,'HH:MM:SS')#</b>
S3 операции и трафик: <b>#dateFormat(qS3OpsTrfAge.dt_load,'YYYY-MM-DD')# #timeFormat(qS3OpsTrfAge.dt_load,'HH:MM:SS')#</b>
<br/>
<a href="#request.thisPage#?output_xls" title="экспорт в Excel" style="margin-left:.5em; height:100%;" target="_blank"><img src="img/xls.gif" style="vertical-align:text-bottom;"/></a>
<a href="#request.thisPage#?output_json" title="экспорт в json" style="margin-left:.5em; height:100%;" target="_blank"><img src="img/json.svg" style="vertical-align:text-bottom;" width="13" height="13"/></a>
</cfoutput>
<table class="worktable">
<thead>
<td width="1%"></td>
<!--- <th width="3%">WZ</th> --->
<th width="7%">Артикул</th>
<th width="7%">Кол-во (метрика)</th>
<th width="3%">Ед.изм.</th>
<th width="3%">Цена &#8381; со скид., с НДС</th>
<th width="3%">Стоимость &#8381; с НДС</th>
<td width="3%">&nbsp;&nbsp;&nbsp;</td>
<td width="10%">Услуга</td>
<td width="10%">Компонент</td>
<!--- <td width="10%">Клиентс. назв.</td> --->
<td width="3%">GPL с НДС</td>
<td width="3%">Скидка%</td>
<!--- <td width="6%">Дата НОУ</td>
<td width="6%">Дата оконч.</td>
<td width="3%">Ключ строки</td> --->
<!--- <td width="3%">Сырая метрика</td> --->
<td width="3%">Оплачиваемый объем</td>
<!--- <td width="5%">Договор</td>
<td width="10%">Допник</td>
<td width="10%">Сделка</td> --->
</thead>
<cfoutput query="qCharge" group="svc">
<cfset var acc=0/>
<tr>
<td bgcolor="##ccc"></td>
<td bgcolor="##ccc" colspan="5" class="b"> #svc#</td>
<td></td>
<td bgcolor="##ccc" colspan="99"></td>
</tr>
<cfoutput>
<cfset acc=acc+charge/>
<tr>
<td>#currentRow#</a></td>
<td>#code#</td>
<td class="r" style="font-size:120%; padding:0 1em;">#chargeable_metric#</td>
<td class="c" style="font-size:90%;">#ed_izm#</td>
<td class="r">#discounted_price#</td>
<td class="r">#charge#</td>
<td></td>
<td>#svc#</td>
<td>#component#</td>
<!--- <td>#user_description#</td> --->
<td class="r">#price#</td>
<td class="r">#discount#</td>
<td class="r">#metric#</td>
</tr>
</cfoutput>
<tr>
<td bgcolor="##eee" style="border-bottom:2px solid gray;"></td>
<td bgcolor="##eee" colspan="4" class="r" style="border-bottom:2px solid gray;"><b>Итого</b>#svc#:</td>
<td bgcolor="##eee" colspan="1" class="r b" style="border-bottom:2px solid gray;">#NumFmt(acc,2)#</td>
<td colspan="14" style="border-bottom:2px solid gray;"></td>
</tr >
</cfoutput>
</table>
<!--- <cfdump var=#qCharge#/> --->
<layout:page section="footer"/>
<!--- select * from s3billing.billing_per_user_fix_pl('2025-05-01 00:00:00','2025-06-01 00:00:00','1395'); --->