Initial commit
This commit is contained in:
commit
da0bfe21ec
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
__pycache__
|
||||
~$*
|
||||
|
||||
*.spec
|
||||
*.log
|
||||
inputs
|
||||
input.xlsx
|
||||
output.csv
|
||||
chromedriver.exe
|
||||
Users.xlsx
|
||||
|
||||
webapi/\!Контент.xlsm
|
||||
webapi/bin/
|
||||
webapi/build/
|
||||
webapi/*.csv
|
||||
webapi/exporter.ini
|
||||
webapi/exporter.exe
|
||||
webapi/*.xlsm
|
||||
webapi/docx/*.docx
|
||||
test/
|
||||
venv
|
39
VBAMake.txt
Normal file
39
VBAMake.txt
Normal file
|
@ -0,0 +1,39 @@
|
|||
# == Properties Section ==
|
||||
# configuration properties
|
||||
# use .ini format to define properties
|
||||
# mandatory properties: name, artifact_home, source_home
|
||||
|
||||
id = BRE
|
||||
name = БРЭ
|
||||
description = Интерфейс взаимодействия с Большой Российской Энциклопедией
|
||||
artifact_home = BRE
|
||||
source_home = BRE
|
||||
install_home = D:\DEV\!WORK\output\BRE
|
||||
|
||||
%%
|
||||
# === Build section ===
|
||||
# Available commands:
|
||||
# build LOCAL_MANIFEST
|
||||
# copy LOCAL_SOURCE -> [LOCAL_ARTIFACT]
|
||||
# save_as LOCAL_ARTIFACT -> LOCAL_ARTIFACT
|
||||
# run LOCAL_SOURCE.bat
|
||||
|
||||
run webapi\pyinstaller_run.bat
|
||||
build script\databaseManifest.txt
|
||||
copy webapi\bin\exporter.exe -> exporter.exe
|
||||
|
||||
copy webapi\configs -> distr\configs
|
||||
copy webapi\bin\exporter.exe -> distr\exporter.exe
|
||||
copy webapi\bin\menu.exe -> distr\menu.exe
|
||||
copy script\run_menu.bat -> distr\run_menu.bat
|
||||
save_as !Контент.xlsm -> distr\!Контент.xlsm
|
||||
|
||||
%%
|
||||
# === Install section ==
|
||||
# Available commands:
|
||||
# install LOCAL_ARTIFACT -> [INSTALL_PATH]
|
||||
# add_template LOCAL_ARTIFACT -> [LOCAL_TEMPLATE]
|
||||
# run LOCAL_ARTIFACT.bat <- [PARAMETERS]
|
||||
# run APPLICATION <- [PARAMETERS]
|
||||
|
||||
# install !Контент.xlsm
|
32
script/LocalDevSetup.ps1
Normal file
32
script/LocalDevSetup.ps1
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Create venv and install dependencies + imports
|
||||
|
||||
$webapi = Resolve-Path -Path "$PSScriptRoot\..\webapi"
|
||||
$envPath = "$webapi\venv"
|
||||
$python = "$envPath\Scripts\python.exe"
|
||||
|
||||
function LocalDevelopmentSetup() {
|
||||
Set-Location $webapi
|
||||
|
||||
ClearPrevious
|
||||
CreateEnv
|
||||
InstallPips
|
||||
}
|
||||
|
||||
function ClearPrevious() {
|
||||
if (Test-Path -Path $envPath) {
|
||||
Write-Host "Removing previous env: $envPath`n" -ForegroundColor DarkGreen
|
||||
Remove-Item $envPath -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
function CreateEnv() {
|
||||
Write-Host "Creating python env: $envPath`n" -ForegroundColor DarkGreen
|
||||
& 'python' -m venv $envPath
|
||||
}
|
||||
|
||||
function InstallPips() {
|
||||
& $python -m pip install --upgrade pip
|
||||
& $python -m pip install -r requirements_dev.txt
|
||||
}
|
||||
|
||||
LocalDevelopmentSetup
|
75
script/databaseManifest.txt
Normal file
75
script/databaseManifest.txt
Normal file
|
@ -0,0 +1,75 @@
|
|||
# == Properties Section ==
|
||||
# configuration properties
|
||||
# use .ini format to define properties
|
||||
# mandatory properties: name, artifact
|
||||
|
||||
name = !Контент.xlsm
|
||||
artifact = !Контент.xlsm
|
||||
|
||||
%%
|
||||
# === Imports Section ===
|
||||
# Hierarchy of folders and files
|
||||
# Use Tabulator to mark next level in hierarchy
|
||||
# All folders are nested into SharedHome path
|
||||
|
||||
api
|
||||
API_XLWrapper.cls
|
||||
API_UserInteraction.cls
|
||||
|
||||
utility
|
||||
ex_MSHook.bas
|
||||
ex_VBA.bas
|
||||
ex_DataPreparation.bas
|
||||
|
||||
excel
|
||||
ex_Excel.bas
|
||||
|
||||
ui
|
||||
CSE_ProgressBar.frm
|
||||
CSE_ListSelector.frm
|
||||
|
||||
dev
|
||||
DevTester.bas
|
||||
|
||||
%%
|
||||
# === Source Code Section ==
|
||||
# Hierarchy of folders and files
|
||||
# Use Tabulator to mark next level in hierarchy
|
||||
# All folders are nested into SourceHome path
|
||||
|
||||
src
|
||||
DevHelper.bas
|
||||
Declarations.bas
|
||||
Main.bas
|
||||
MainImpl.bas
|
||||
z_UIRibbon.bas
|
||||
z_UIMessages.bas
|
||||
|
||||
DB_Content.cls
|
||||
DB_Workers.cls
|
||||
InfoConfig.cls
|
||||
IteratorAttribute.cls
|
||||
IteratorContent.cls
|
||||
IteratorCSVTasks.cls
|
||||
IteratorCSVContent.cls
|
||||
|
||||
%%
|
||||
# ===== UI Section =======
|
||||
# Pairs of path to UI elements, use " -> " delimiter
|
||||
# First component is a path relative to SourceHome\ui folders
|
||||
# Second component is internal path inside project file
|
||||
|
||||
.rels -> _rels\.rels
|
||||
customUI.xml -> customUI\customUI.xml
|
||||
|
||||
%%
|
||||
# === References Section ===
|
||||
# List dependencies in one of the formats
|
||||
# global : GLOBAL_NAME
|
||||
# guid : {REGISTERED_GUID}
|
||||
# file : PATH_TO_LIBRARY
|
||||
|
||||
global : Scripting
|
||||
global : MSForms
|
||||
global : ADODB
|
||||
global : IWshRuntimeLibrary
|
5
script/run_menu.bat
Normal file
5
script/run_menu.bat
Normal file
|
@ -0,0 +1,5 @@
|
|||
@echo off
|
||||
|
||||
menu.exe
|
||||
|
||||
pause
|
BIN
skeleton/!Контент.xlsm
Normal file
BIN
skeleton/!Контент.xlsm
Normal file
Binary file not shown.
126
src/DB_Content.cls
Normal file
126
src/DB_Content.cls
Normal file
|
@ -0,0 +1,126 @@
|
|||
VERSION 1.0 CLASS
|
||||
BEGIN
|
||||
MultiUse = -1 'True
|
||||
END
|
||||
Attribute VB_Name = "DB_Content"
|
||||
Attribute VB_GlobalNameSpace = False
|
||||
Attribute VB_Creatable = False
|
||||
Attribute VB_PredeclaredId = False
|
||||
Attribute VB_Exposed = False
|
||||
Option Explicit
|
||||
|
||||
Private data_ As Excel.Worksheet
|
||||
Private attributes_ As Excel.Worksheet
|
||||
|
||||
Public Function Init(iData As Excel.Worksheet, iAttributes As Excel.Worksheet)
|
||||
Set data_ = iData
|
||||
Set attributes_ = iAttributes
|
||||
End Function
|
||||
|
||||
Public Function Attributes() As IteratorAttribute
|
||||
Set Attributes = New IteratorAttribute
|
||||
Call Attributes.Init(attributes_)
|
||||
End Function
|
||||
|
||||
Public Function IBegin() As IteratorContent
|
||||
Set IBegin = New IteratorContent
|
||||
Call IBegin.Init(data_)
|
||||
End Function
|
||||
|
||||
Public Function ILast() As IteratorContent
|
||||
Set ILast = New IteratorContent
|
||||
Call ILast.Init(data_)
|
||||
Call ILast.GoLast
|
||||
End Function
|
||||
|
||||
Public Function INew() As IteratorContent
|
||||
Set INew = New IteratorContent
|
||||
Call INew.Init(data_)
|
||||
Call INew.GoLast
|
||||
Call INew.Increment
|
||||
End Function
|
||||
|
||||
Public Property Get Count() As Long
|
||||
Count = ILast.row_ - IBegin.row_ + 1
|
||||
End Property
|
||||
|
||||
Public Function FindTaskID(sID$) As IteratorContent
|
||||
Dim iResult As IteratorContent: Set iResult = IBegin
|
||||
If iResult.FindTaskID(sID) Then _
|
||||
Set FindTaskID = iResult
|
||||
End Function
|
||||
|
||||
Public Function FindContentName(sName$, sType$) As IteratorContent
|
||||
Dim iResult As IteratorContent: Set iResult = IBegin
|
||||
If iResult.FindContentName(sName, sType) Then _
|
||||
Set FindContentName = iResult
|
||||
End Function
|
||||
|
||||
Public Function EnsureDataVisible()
|
||||
Call XLShowAllData(data_, bKeepColumns:=True)
|
||||
Call XLShowAllData(attributes_)
|
||||
End Function
|
||||
|
||||
Public Function ImportCSVTasks(iInput As IteratorCSVTasks)
|
||||
Call iInput.GoFirst
|
||||
Dim iOutput As IteratorContent
|
||||
Do While Not iInput.IsDone
|
||||
Set iOutput = FindPlaceFor(iInput.TaskID, iInput.ContentName, iInput.TaskType)
|
||||
If Not iInput.IsCanceled Or iOutput.TaskType <> "" Then
|
||||
Call iOutput.SyncCSVTasks(iInput)
|
||||
End If
|
||||
If CSE_ProgressBar.Visible Then _
|
||||
Call CSE_ProgressBar.IncrementA
|
||||
Call iInput.Increment
|
||||
Loop
|
||||
End Function
|
||||
|
||||
Public Function ImportCSVContent(iInput As IteratorCSVContent)
|
||||
Call iInput.GoFirst
|
||||
Dim iOutput As IteratorContent
|
||||
Do While Not iInput.IsDone
|
||||
Set iOutput = FindPlaceFor(iInput.TaskID)
|
||||
Call iOutput.SyncCSVContent(iInput)
|
||||
If CSE_ProgressBar.Visible Then _
|
||||
Call CSE_ProgressBar.IncrementA
|
||||
Call iInput.Increment
|
||||
Loop
|
||||
End Function
|
||||
|
||||
Public Function ImportDB(iData As DB_Content)
|
||||
Dim iInput As IteratorContent: Set iInput = iData.IBegin()
|
||||
Dim iOutput As IteratorContent
|
||||
Do While Not iInput.IsDone
|
||||
Set iOutput = FindPlaceFor(iInput.TaskID, iInput.ContentName, iInput.TaskType)
|
||||
If Not iInput.IsCanceled Or iOutput.TaskType <> "" Then _
|
||||
Call iOutput.SyncContent(iInput)
|
||||
If CSE_ProgressBar.Visible Then _
|
||||
Call CSE_ProgressBar.IncrementA
|
||||
Call iInput.Increment
|
||||
Loop
|
||||
Call ImportAttributes(iData.Attributes)
|
||||
End Function
|
||||
|
||||
' =======
|
||||
Private Function FindPlaceFor(sID$, Optional sName$ = "", Optional sType$ = "") As IteratorContent
|
||||
Dim iWhere As IteratorContent: Set iWhere = IBegin
|
||||
If Not iWhere.FindTaskID(sID) Then _
|
||||
If Not iWhere.FindContentName(sName, sType) Or iWhere.TaskID <> "" Then _
|
||||
Call iWhere.GoEmpty
|
||||
Set FindPlaceFor = iWhere
|
||||
End Function
|
||||
|
||||
Private Function ImportAttributes(iInput As IteratorAttribute)
|
||||
Dim iOutput As IteratorAttribute: Set iOutput = Attributes
|
||||
Call iOutput.GoEmpty
|
||||
Do While Not iInput.IsDone
|
||||
Call iOutput.SyncWith(iInput)
|
||||
Call iOutput.Increment
|
||||
Call iInput.Increment
|
||||
Loop
|
||||
Call DeleteAttributeDuplicates
|
||||
End Function
|
||||
|
||||
Private Function DeleteAttributeDuplicates()
|
||||
|
||||
End Function
|
28
src/DB_Workers.cls
Normal file
28
src/DB_Workers.cls
Normal file
|
@ -0,0 +1,28 @@
|
|||
VERSION 1.0 CLASS
|
||||
BEGIN
|
||||
MultiUse = -1 'True
|
||||
END
|
||||
Attribute VB_Name = "DB_Workers"
|
||||
Attribute VB_GlobalNameSpace = False
|
||||
Attribute VB_Creatable = False
|
||||
Attribute VB_PredeclaredId = False
|
||||
Attribute VB_Exposed = False
|
||||
Option Explicit
|
||||
|
||||
Private data_ As Excel.ListObject
|
||||
|
||||
Public Function Init(oTable As Excel.ListObject)
|
||||
Set data_ = oTable
|
||||
End Function
|
||||
|
||||
Public Function ExpandShortname(sShort$) As String
|
||||
Dim dataRng As Excel.Range: Set dataRng = data_.Range
|
||||
Dim nRow&
|
||||
For nRow = 2 To dataRng.Rows.Count Step 1
|
||||
If dataRng.Cells(nRow, 2) = sShort Then
|
||||
ExpandShortname = dataRng.Cells(nRow, 1)
|
||||
Exit Function
|
||||
End If
|
||||
Next nRow
|
||||
ExpandShortname = sShort
|
||||
End Function
|
82
src/Declarations.bas
Normal file
82
src/Declarations.bas
Normal file
|
@ -0,0 +1,82 @@
|
|||
Attribute VB_Name = "Declarations"
|
||||
Option Private Module
|
||||
Option Explicit
|
||||
|
||||
Public Const DATA_FIRST_ROW = 2
|
||||
|
||||
Public Const NO_CONTENT_PLACEHOLDER = "Íåò Êîíòåíòà"
|
||||
Public Const NO_MEDIA_PLACEHOLDER = "Íåò Ìåäèà"
|
||||
|
||||
Public Const STATUS_CANCELED = "Îòìåíåíà"
|
||||
Public Const BOOL_TEXT_YES = "Äà"
|
||||
Public Const BOOL_TEXT_NO = "Íåò"
|
||||
|
||||
Public Const SHEET_CONTENT = "Êîíòåíò"
|
||||
Public Const SHEET_OPTIONS = "Ñïðàâî÷íèêè"
|
||||
Public Const SHEET_ATTRIBUTES = "Ïðèçíàêè"
|
||||
Public Const SHEET_CONFIG = "Êîíôèã"
|
||||
|
||||
Public Const TABLE_WORKERS = "t_Person"
|
||||
Public Const TABLE_MARKERS = "t_Markers"
|
||||
Public Const TABLE_TAGS = "t_Tags"
|
||||
|
||||
Public Const URL_PREFIX_TASK = "https://rk.greatbook.ru/tasks/"
|
||||
Public Const URL_PREFIX_CONTENT = "https://rk.greatbook.ru/widgets?link=task&id="
|
||||
|
||||
Public Const EXPORTER_CONFIG_FILE = "exporter.ini"
|
||||
Public Const EXPORTER_EXECUTABLE = "exporter.exe"
|
||||
|
||||
Public Const PREFIX_IMMUTABLE = "Íåèçìåííûå "
|
||||
Public Const SUFFIX_IMMUTABLE = " (áèáëèîãðàôèÿ+êîððåêòóðà+òðàíñêðèïöèÿ)"
|
||||
|
||||
Public Enum TUpdateStatus
|
||||
T_UPD_UNDEF = 0
|
||||
[_First] = 1
|
||||
|
||||
T_UPD_COMPLETE = 1
|
||||
T_UPD_IGNORE = 2
|
||||
T_UPD_AUTO = 3
|
||||
T_UPD_ONCE = 4
|
||||
T_UPD_ALWAYS = 5
|
||||
|
||||
[_Last] = 5
|
||||
End Enum
|
||||
|
||||
Public Function InferContentFromTask(sTask$) As String
|
||||
Dim sContent$: sContent = sTask
|
||||
If sContent Like PREFIX_IMMUTABLE & "*" Then _
|
||||
sContent = VBA.Right(sContent, VBA.Len(sContent) - VBA.Len(PREFIX_IMMUTABLE))
|
||||
If sContent Like "*" & SUFFIX_IMMUTABLE Then _
|
||||
sContent = VBA.Left(sContent, VBA.Len(sContent) - VBA.Len(SUFFIX_IMMUTABLE))
|
||||
InferContentFromTask = sContent
|
||||
End Function
|
||||
|
||||
Public Function UpdateStatusFromText(sText$) As TUpdateStatus
|
||||
If sText = "çàâåðøåí" Then
|
||||
UpdateStatusFromText = T_UPD_COMPLETE
|
||||
ElseIf sText = "íèêîãäà" Then
|
||||
UpdateStatusFromText = T_UPD_IGNORE
|
||||
ElseIf sText = "àâòî" Then
|
||||
UpdateStatusFromText = T_UPD_AUTO
|
||||
ElseIf sText = "åäèíîæäû" Then
|
||||
UpdateStatusFromText = T_UPD_ONCE
|
||||
ElseIf sText = "âñåãäà" Then
|
||||
UpdateStatusFromText = T_UPD_ALWAYS
|
||||
Else
|
||||
UpdateStatusFromText = T_UPD_UNDEF
|
||||
End If
|
||||
End Function
|
||||
|
||||
Public Function UpdateStatusToText(iStatus As TUpdateStatus) As String
|
||||
Select Case iStatus
|
||||
Case T_UPD_UNDEF: UpdateStatusToText = ""
|
||||
Case T_UPD_COMPLETE: UpdateStatusToText = "çàâåðøåí"
|
||||
Case T_UPD_IGNORE: UpdateStatusToText = "íèêîãäà"
|
||||
Case T_UPD_AUTO: UpdateStatusToText = "àâòî"
|
||||
Case T_UPD_ONCE: UpdateStatusToText = "åäèíîæäû"
|
||||
Case T_UPD_ALWAYS: UpdateStatusToText = "âñåãäà"
|
||||
Case Else: UpdateStatusToText = ""
|
||||
End Select
|
||||
End Function
|
||||
|
||||
|
20
src/DevHelper.bas
Normal file
20
src/DevHelper.bas
Normal file
|
@ -0,0 +1,20 @@
|
|||
Attribute VB_Name = "DevHelper"
|
||||
Option Explicit
|
||||
|
||||
Public Function Dev_PrepareSkeleton()
|
||||
Call ClearData
|
||||
End Function
|
||||
|
||||
Public Sub Dev_ManualRunTest()
|
||||
Dim sSuite$: sSuite = "s_Database"
|
||||
Dim sTest$: sTest = "t_RenameLawFile"
|
||||
Dim sMsg$: sMsg = Dev_RunTestDebug(sSuite, sTest)
|
||||
Debug.Print sMsg
|
||||
Call MsgBox(sMsg)
|
||||
End Sub
|
||||
|
||||
Public Function Dev_GetTestSuite(sName$) As Object
|
||||
Select Case sName
|
||||
' Case "s_ActiveStateExporter": Set Dev_GetTestSuite = New s_ActiveStateExporter
|
||||
End Select
|
||||
End Function
|
163
src/InfoConfig.cls
Normal file
163
src/InfoConfig.cls
Normal file
|
@ -0,0 +1,163 @@
|
|||
VERSION 1.0 CLASS
|
||||
BEGIN
|
||||
MultiUse = -1 'True
|
||||
END
|
||||
Attribute VB_Name = "InfoConfig"
|
||||
Attribute VB_GlobalNameSpace = False
|
||||
Attribute VB_Creatable = False
|
||||
Attribute VB_PredeclaredId = False
|
||||
Attribute VB_Exposed = False
|
||||
Option Explicit
|
||||
|
||||
Private Const S_CONFIG_KEY = 1
|
||||
Private Const S_CONFIG_VALUE = 2
|
||||
Private Const S_CONFIG_COMMENT = 3
|
||||
|
||||
Private Enum AppdataRows
|
||||
[_First] = 2
|
||||
|
||||
R_APP_FILTER_TASK = 2
|
||||
R_APP_FILTER_DEPARTMENT = 3
|
||||
R_APP_FILTER_STATUS = 4
|
||||
R_APP_FILTER_RESPONSIBLE = 5
|
||||
R_APP_FILTER_SUPERVISOR = 6
|
||||
R_APP_FILTER_EXECUTOR = 7
|
||||
R_APP_FILTER_OBSERVER = 8
|
||||
|
||||
R_APP_FILTER_CREATED_BEGIN = 9
|
||||
R_APP_FILTER_CREATED_END = 10
|
||||
R_APP_FILTER_TARGET_BEGIN = 11
|
||||
R_APP_FILTER_TARGET_END = 12
|
||||
|
||||
R_APP_OUTPUT = 13
|
||||
R_APP_SCAN_TASKS = 14
|
||||
R_APP_SCAN_CONTENT = 15
|
||||
R_APP_OUTPUT_CONTENT = 16
|
||||
R_APP_ACCESS_TOKEN = 17
|
||||
|
||||
[_Last] = 17
|
||||
End Enum
|
||||
|
||||
Private Enum UserRows
|
||||
[_First] = 19
|
||||
|
||||
R_USER_NAME = 19
|
||||
R_USER_LOGIN = 20
|
||||
R_USER_PASSWORD = 21
|
||||
|
||||
[_Last] = 21
|
||||
End Enum
|
||||
|
||||
Private Enum OptionsRows
|
||||
[_First] = 23
|
||||
|
||||
R_OPT_DEBUG = 23
|
||||
R_OPT_TESTRUN = 24
|
||||
R_OPT_TIMEOUT = 25
|
||||
|
||||
[_Last] = 25
|
||||
End Enum
|
||||
|
||||
Private data_ As Excel.Worksheet
|
||||
Private content_ As DB_Content
|
||||
|
||||
Public Function Init(iData As Excel.Worksheet, iContent As DB_Content)
|
||||
Set data_ = iData
|
||||
Set content_ = iContent
|
||||
End Function
|
||||
|
||||
Public Function SetScanContent(bScanContent As Boolean)
|
||||
data_.Cells(R_APP_SCAN_CONTENT, S_CONFIG_VALUE) = IIf(bScanContent, "true", "false")
|
||||
End Function
|
||||
|
||||
Public Function SetScanTasks(bScanContent As Boolean)
|
||||
data_.Cells(R_APP_SCAN_TASKS, S_CONFIG_VALUE) = IIf(bScanContent, "true", "false")
|
||||
End Function
|
||||
|
||||
Public Function ConfigFilePath() As String
|
||||
ConfigFilePath = data_.Parent.Path & "\" & EXPORTER_CONFIG_FILE
|
||||
End Function
|
||||
|
||||
Public Function OutputFileTasks() As String
|
||||
OutputFileTasks = data_.Parent.Path & "\" & data_.Cells(R_APP_OUTPUT, S_CONFIG_VALUE)
|
||||
End Function
|
||||
|
||||
Public Function OutputFileContent() As String
|
||||
OutputFileContent = data_.Parent.Path & "\" & data_.Cells(R_APP_OUTPUT_CONTENT, S_CONFIG_VALUE)
|
||||
End Function
|
||||
|
||||
Public Function ScanContent() As Boolean
|
||||
ScanContent = data_.Cells(R_APP_SCAN_CONTENT, S_CONFIG_VALUE)
|
||||
End Function
|
||||
|
||||
Public Function ScanTasks() As Boolean
|
||||
ScanTasks = data_.Cells(R_APP_SCAN_TASKS, S_CONFIG_VALUE)
|
||||
End Function
|
||||
|
||||
Public Function CreateConfigFile()
|
||||
Dim nRow&
|
||||
Dim sValue$, sKey$
|
||||
Dim iOut As New ADODB.Stream: iOut.Charset = "utf-8"
|
||||
Call iOut.Open
|
||||
|
||||
Call iOut.WriteText("[AppData]", adWriteLine)
|
||||
For nRow = AppdataRows.[_First] To AppdataRows.[_Last] Step 1
|
||||
sKey = data_.Cells(nRow, S_CONFIG_KEY)
|
||||
sValue = data_.Cells(nRow, S_CONFIG_VALUE)
|
||||
Call iOut.WriteText(Fmt("{1}={2}", sKey, sValue), adWriteLine)
|
||||
Next nRow
|
||||
Call PrepareLists(iOut)
|
||||
|
||||
Call iOut.WriteText("[Options]", adWriteLine)
|
||||
For nRow = OptionsRows.[_First] To OptionsRows.[_Last] Step 1
|
||||
sKey = data_.Cells(nRow, S_CONFIG_KEY)
|
||||
sValue = data_.Cells(nRow, S_CONFIG_VALUE)
|
||||
Call iOut.WriteText(Fmt("{1}={2}", sKey, sValue), adWriteLine)
|
||||
Next nRow
|
||||
|
||||
Call iOut.WriteText("[UserData]", adWriteLine)
|
||||
For nRow = UserRows.[_First] To UserRows.[_Last] Step 1
|
||||
sKey = data_.Cells(nRow, S_CONFIG_KEY)
|
||||
sValue = data_.Cells(nRow, S_CONFIG_VALUE)
|
||||
Call iOut.WriteText(Fmt("{1}={2}", sKey, sValue), adWriteLine)
|
||||
Next nRow
|
||||
|
||||
Dim iOutNoBOM As New ADODB.Stream
|
||||
iOutNoBOM.Type = adTypeBinary
|
||||
Call iOutNoBOM.Open
|
||||
|
||||
iOut.Position = 3
|
||||
Call iOut.CopyTo(iOutNoBOM)
|
||||
|
||||
Call iOutNoBOM.SaveToFile(ConfigFilePath, adSaveCreateOverWrite)
|
||||
Call iOutNoBOM.Close
|
||||
|
||||
Call iOut.Close
|
||||
End Function
|
||||
|
||||
Public Function DeleteConfigFile()
|
||||
Call Kill(ConfigFilePath)
|
||||
End Function
|
||||
|
||||
' ========
|
||||
Private Function PrepareLists(iOut As ADODB.Stream) As String
|
||||
Dim iExclude$: iExclude = ""
|
||||
Dim iInclude$: iInclude = ""
|
||||
Dim iContent As IteratorContent: Set iContent = content_.IBegin
|
||||
Dim iStatus As TUpdateStatus
|
||||
Do While Not iContent.IsDone
|
||||
Call iContent.RecalculateStatus
|
||||
If iContent.IsIgnored Then
|
||||
If iExclude <> "" Then iExclude = iExclude & ";"
|
||||
iExclude = iExclude & iContent.TaskID
|
||||
End If
|
||||
If iContent.NeedsUpdate Then
|
||||
If iInclude <> "" Then iInclude = iInclude & ";"
|
||||
iInclude = iInclude & iContent.TaskID
|
||||
End If
|
||||
Call iContent.Increment
|
||||
Loop
|
||||
|
||||
Call iOut.WriteText(Fmt("{1}={2}", "ExcludeID", iExclude), adWriteLine)
|
||||
Call iOut.WriteText(Fmt("{1}={2}", "IncludeID", iInclude), adWriteLine)
|
||||
End Function
|
99
src/IteratorAttribute.cls
Normal file
99
src/IteratorAttribute.cls
Normal file
|
@ -0,0 +1,99 @@
|
|||
VERSION 1.0 CLASS
|
||||
BEGIN
|
||||
MultiUse = -1 'True
|
||||
END
|
||||
Attribute VB_Name = "IteratorAttribute"
|
||||
Attribute VB_GlobalNameSpace = False
|
||||
Attribute VB_Creatable = False
|
||||
Attribute VB_PredeclaredId = False
|
||||
Attribute VB_Exposed = False
|
||||
Option Explicit
|
||||
|
||||
' _A_ Attrtibutes structure
|
||||
Private Enum AttributesStruct
|
||||
[_First] = 1
|
||||
|
||||
S_A_CONTENT_NAME = 1
|
||||
S_A_ATTRIBUTE = 2
|
||||
S_A_VALUE = 3
|
||||
S_A_INPUT_METHOD = 4
|
||||
|
||||
[_Last] = 4
|
||||
End Enum
|
||||
|
||||
Public row_ As Long
|
||||
|
||||
Private data_ As Excel.Worksheet
|
||||
|
||||
Public Sub Init(target As Excel.Worksheet, Optional tRow& = DATA_FIRST_ROW)
|
||||
Set data_ = target
|
||||
row_ = tRow
|
||||
End Sub
|
||||
|
||||
Public Function Increment(Optional inc& = 1)
|
||||
If row_ + inc > 0 Then _
|
||||
row_ = row_ + inc
|
||||
End Function
|
||||
|
||||
Public Function GoFirst()
|
||||
row_ = DATA_FIRST_ROW
|
||||
End Function
|
||||
|
||||
Public Function GoLast()
|
||||
row_ = GetLastRow
|
||||
End Function
|
||||
|
||||
Public Function GoEmpty()
|
||||
Call GoLast
|
||||
Call Increment
|
||||
End Function
|
||||
|
||||
Public Function IsDone() As Boolean
|
||||
IsDone = row_ > GetLastRow
|
||||
End Function
|
||||
|
||||
Public Function SyncWith(iInput As IteratorAttribute)
|
||||
ContentName = iInput.ContentName
|
||||
Attr = iInput.Attr
|
||||
Value = iInput.Value
|
||||
InputMethod = iInput.InputMethod
|
||||
End Function
|
||||
|
||||
' ======== Property Get =========
|
||||
Public Property Get ContentName() As String
|
||||
ContentName = data_.Cells(row_, S_A_CONTENT_NAME)
|
||||
End Property
|
||||
|
||||
Public Property Get Attr() As String
|
||||
Attr = data_.Cells(row_, S_A_ATTRIBUTE)
|
||||
End Property
|
||||
|
||||
Public Property Get Value() As String
|
||||
Value = data_.Cells(row_, S_A_VALUE)
|
||||
End Property
|
||||
|
||||
Public Property Get InputMethod() As String
|
||||
InputMethod = data_.Cells(row_, S_A_INPUT_METHOD)
|
||||
End Property
|
||||
|
||||
' ==== Property Let ====
|
||||
Public Property Let ContentName(newVal$)
|
||||
data_.Cells(row_, S_A_CONTENT_NAME) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let Attr(newVal$)
|
||||
data_.Cells(row_, S_A_ATTRIBUTE) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let Value(newVal$)
|
||||
data_.Cells(row_, S_A_VALUE) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let InputMethod(newVal$)
|
||||
data_.Cells(row_, S_A_INPUT_METHOD) = newVal
|
||||
End Property
|
||||
|
||||
' =======
|
||||
Private Function GetLastRow() As Long
|
||||
GetLastRow = data_.Cells(data_.Rows.Count, 1).End(xlUp).Row
|
||||
End Function
|
134
src/IteratorCSVContent.cls
Normal file
134
src/IteratorCSVContent.cls
Normal file
|
@ -0,0 +1,134 @@
|
|||
VERSION 1.0 CLASS
|
||||
BEGIN
|
||||
MultiUse = -1 'True
|
||||
END
|
||||
Attribute VB_Name = "IteratorCSVContent"
|
||||
Attribute VB_GlobalNameSpace = False
|
||||
Attribute VB_Creatable = False
|
||||
Attribute VB_PredeclaredId = False
|
||||
Attribute VB_Exposed = False
|
||||
Option Explicit
|
||||
|
||||
' _I_ Input CSV structure
|
||||
Private Enum InputStruct
|
||||
[_First] = 1
|
||||
|
||||
S_I_TASK_ID = 1
|
||||
S_I_BIBLIO_NAME = 2
|
||||
S_I_CHANGE_SCORE = 3
|
||||
S_I_DEPARTMENT = 4
|
||||
S_I_RESPONSIBLE = 5
|
||||
S_I_DEFINITION = 6
|
||||
S_I_OBJECT_TYPE = 7
|
||||
S_I_MARKERS = 8
|
||||
S_I_TAGS = 9
|
||||
S_I_SOURCE = 10
|
||||
S_I_ELECTRON_BRE = 11
|
||||
S_I_MAIN_PAGE = 12
|
||||
S_I_IS_GENERAL = 13
|
||||
S_I_ACTUALIZE_PERIOD = 14
|
||||
S_I_AGE_RESTRICTION = 15
|
||||
S_I_AUTHOR = 16
|
||||
S_I_EDITOR = 17
|
||||
|
||||
[_Last] = 17
|
||||
End Enum
|
||||
|
||||
Public row_ As Long
|
||||
|
||||
Private data_ As Excel.Worksheet
|
||||
|
||||
Public Sub Init(target As Excel.Worksheet, Optional tRow& = 1)
|
||||
Set data_ = target
|
||||
row_ = tRow
|
||||
End Sub
|
||||
|
||||
Public Function Increment(Optional inc& = 1)
|
||||
If row_ + inc > 0 Then _
|
||||
row_ = row_ + inc
|
||||
End Function
|
||||
|
||||
Public Function GoFirst()
|
||||
row_ = 1
|
||||
End Function
|
||||
|
||||
Public Function GoLast()
|
||||
row_ = data_.Columns(S_I_TASK_ID).Find(vbNullString, LookAt:=xlWhole).Row - 1
|
||||
End Function
|
||||
|
||||
Public Property Get CountRows() As Long
|
||||
CountRows = data_.Columns(S_I_TASK_ID).Find(vbNullString, LookAt:=xlWhole).Row - 1
|
||||
End Property
|
||||
|
||||
Public Function IsDone() As Boolean
|
||||
IsDone = data_.Cells(row_, S_I_TASK_ID) = vbNullString
|
||||
End Function
|
||||
|
||||
'===== Propertiy Get =====
|
||||
Public Property Get TaskID() As String
|
||||
TaskID = data_.Cells(row_, S_I_TASK_ID)
|
||||
End Property
|
||||
|
||||
Public Property Get BiblioName() As String
|
||||
BiblioName = data_.Cells(row_, S_I_BIBLIO_NAME)
|
||||
End Property
|
||||
|
||||
Public Property Get ChangeScore() As String
|
||||
ChangeScore = data_.Cells(row_, S_I_CHANGE_SCORE)
|
||||
End Property
|
||||
|
||||
Public Property Get Department() As String
|
||||
Department = data_.Cells(row_, S_I_DEPARTMENT)
|
||||
End Property
|
||||
|
||||
Public Property Get Responsible() As String
|
||||
Responsible = data_.Cells(row_, S_I_RESPONSIBLE)
|
||||
End Property
|
||||
|
||||
Public Property Get Definition() As String
|
||||
Definition = data_.Cells(row_, S_I_DEFINITION)
|
||||
End Property
|
||||
|
||||
Public Property Get ObjectType() As String
|
||||
ObjectType = data_.Cells(row_, S_I_OBJECT_TYPE)
|
||||
End Property
|
||||
|
||||
Public Property Get Markers() As String
|
||||
Markers = data_.Cells(row_, S_I_MARKERS)
|
||||
End Property
|
||||
|
||||
Public Property Get Tags() As String
|
||||
Tags = data_.Cells(row_, S_I_TAGS)
|
||||
End Property
|
||||
|
||||
Public Property Get Source() As String
|
||||
Source = data_.Cells(row_, S_I_SOURCE)
|
||||
End Property
|
||||
|
||||
Public Property Get ElectronBre() As String
|
||||
ElectronBre = data_.Cells(row_, S_I_ELECTRON_BRE)
|
||||
End Property
|
||||
|
||||
Public Property Get MainPage() As String
|
||||
MainPage = data_.Cells(row_, S_I_MAIN_PAGE)
|
||||
End Property
|
||||
|
||||
Public Property Get IsGeneral() As String
|
||||
IsGeneral = data_.Cells(row_, S_I_IS_GENERAL)
|
||||
End Property
|
||||
|
||||
Public Property Get ActualizePeriod() As String
|
||||
ActualizePeriod = data_.Cells(row_, S_I_ACTUALIZE_PERIOD)
|
||||
End Property
|
||||
|
||||
Public Property Get AgeRestriction() As String
|
||||
AgeRestriction = data_.Cells(row_, S_I_AGE_RESTRICTION)
|
||||
End Property
|
||||
|
||||
Public Property Get Author() As String
|
||||
Author = data_.Cells(row_, S_I_AUTHOR)
|
||||
End Property
|
||||
|
||||
Public Property Get Editor() As String
|
||||
Editor = data_.Cells(row_, S_I_EDITOR)
|
||||
End Property
|
118
src/IteratorCSVTasks.cls
Normal file
118
src/IteratorCSVTasks.cls
Normal file
|
@ -0,0 +1,118 @@
|
|||
VERSION 1.0 CLASS
|
||||
BEGIN
|
||||
MultiUse = -1 'True
|
||||
END
|
||||
Attribute VB_Name = "IteratorCSVTasks"
|
||||
Attribute VB_GlobalNameSpace = False
|
||||
Attribute VB_Creatable = False
|
||||
Attribute VB_PredeclaredId = False
|
||||
Attribute VB_Exposed = False
|
||||
Option Explicit
|
||||
|
||||
' _I_ Input CSV structure
|
||||
Private Enum InputStruct
|
||||
[_First] = 1
|
||||
|
||||
S_I_TASK_TYPE = 1
|
||||
S_I_STATUS = 2
|
||||
S_I_CONTENT_NAME_DB = 3
|
||||
S_I_SUPERVISOR = 4
|
||||
S_I_EXECUTOR = 5
|
||||
S_I_TARGET_DATE = 6
|
||||
S_I_TASK_ID = 7
|
||||
S_I_TASK_NAME = 8
|
||||
S_I_PARENT_ID = 9
|
||||
|
||||
[_Last] = 9
|
||||
End Enum
|
||||
|
||||
Public row_ As Long
|
||||
|
||||
Private data_ As Excel.Worksheet
|
||||
Private workers_ As DB_Workers
|
||||
|
||||
Public Sub Init(target As Excel.Worksheet, dbWorkers As DB_Workers, Optional tRow& = 1)
|
||||
Set data_ = target
|
||||
row_ = tRow
|
||||
Set workers_ = dbWorkers
|
||||
End Sub
|
||||
|
||||
Public Function Increment(Optional inc& = 1)
|
||||
If row_ + inc > 0 Then _
|
||||
row_ = row_ + inc
|
||||
End Function
|
||||
|
||||
Public Function GoFirst()
|
||||
row_ = 1
|
||||
End Function
|
||||
|
||||
Public Function GoLast()
|
||||
row_ = data_.Columns(S_I_TASK_TYPE).Find(vbNullString, LookAt:=xlWhole).Row - 1
|
||||
End Function
|
||||
|
||||
Public Property Get CountRows() As Long
|
||||
CountRows = data_.Columns(S_I_TASK_TYPE).Find(vbNullString, LookAt:=xlWhole).Row - 1
|
||||
End Property
|
||||
|
||||
Public Function IsDone() As Boolean
|
||||
IsDone = data_.Cells(row_, S_I_TASK_TYPE) = vbNullString
|
||||
End Function
|
||||
|
||||
Public Function IsCanceled() As Boolean
|
||||
IsCanceled = Status = STATUS_CANCELED
|
||||
End Function
|
||||
|
||||
Public Function HasContent() As Boolean
|
||||
Dim sContent$: sContent = ContentNameDB
|
||||
HasContent = sContent <> NO_CONTENT_PLACEHOLDER And sContent <> NO_MEDIA_PLACEHOLDER
|
||||
End Function
|
||||
|
||||
'===== Propertiy Get =====
|
||||
Public Property Get TaskType() As String
|
||||
Dim sText$: sText = data_.Cells(row_, S_I_TASK_TYPE)
|
||||
If sText = "ÑÒÀÒÜß ÈÇ ÝÂ ÁÐÝ" Then
|
||||
TaskType = "Ñòàòüÿ èç ÝÂ ÁÐÝ"
|
||||
Else
|
||||
TaskType = CapitalizeFirstLetter(VBA.LCase(sText))
|
||||
End If
|
||||
End Property
|
||||
|
||||
Public Property Get Status() As String
|
||||
Status = data_.Cells(row_, S_I_STATUS)
|
||||
End Property
|
||||
|
||||
Public Property Get ContentName() As String
|
||||
If Not HasContent Then
|
||||
ContentName = InferContentFromTask(TaskName)
|
||||
Else
|
||||
ContentName = ContentNameDB
|
||||
End If
|
||||
End Property
|
||||
|
||||
Public Property Get ContentNameDB() As String
|
||||
ContentNameDB = data_.Cells(row_, S_I_CONTENT_NAME_DB)
|
||||
End Property
|
||||
|
||||
Public Property Get Supervisor() As String
|
||||
Supervisor = workers_.ExpandShortname(data_.Cells(row_, S_I_SUPERVISOR))
|
||||
End Property
|
||||
|
||||
Public Property Get Executor() As String
|
||||
Executor = workers_.ExpandShortname(data_.Cells(row_, S_I_EXECUTOR))
|
||||
End Property
|
||||
|
||||
Public Property Get TargetDate() As Long
|
||||
TargetDate = VBA.CDate(data_.Cells(row_, S_I_TARGET_DATE))
|
||||
End Property
|
||||
|
||||
Public Property Get TaskID() As String
|
||||
TaskID = data_.Cells(row_, S_I_TASK_ID)
|
||||
End Property
|
||||
|
||||
Public Property Get TaskName() As String
|
||||
TaskName = data_.Cells(row_, S_I_TASK_NAME)
|
||||
End Property
|
||||
|
||||
Public Property Get ParentID() As String
|
||||
ParentID = data_.Cells(row_, S_I_PARENT_ID)
|
||||
End Property
|
595
src/IteratorContent.cls
Normal file
595
src/IteratorContent.cls
Normal file
|
@ -0,0 +1,595 @@
|
|||
VERSION 1.0 CLASS
|
||||
BEGIN
|
||||
MultiUse = -1 'True
|
||||
END
|
||||
Attribute VB_Name = "IteratorContent"
|
||||
Attribute VB_GlobalNameSpace = False
|
||||
Attribute VB_Creatable = False
|
||||
Attribute VB_PredeclaredId = False
|
||||
Attribute VB_Exposed = False
|
||||
Option Explicit
|
||||
|
||||
' _C_ Content structure
|
||||
Private Enum ContentStruct
|
||||
[_First] = 1
|
||||
|
||||
S_C_TASK_TYPE = 1
|
||||
S_C_STATUS = 2
|
||||
S_C_CONTENT_NAME = 3
|
||||
S_C_UPDATE_STATUS = 4
|
||||
S_C_CHANGE_SCORE = 5
|
||||
S_C_BIBLIO_NAME = 6
|
||||
S_C_DEFINITION = 7
|
||||
S_C_IS_IMMUTABLE = 8
|
||||
S_C_OBJECT_TYPE = 9
|
||||
S_C_MARKERS = 10
|
||||
S_C_TAGS = 11
|
||||
S_C_AUTHOR = 12
|
||||
S_C_SUPERVISOR = 13
|
||||
S_C_EXECUTOR = 14
|
||||
S_C_EDITOR = 15
|
||||
S_C_RESPONSIBLE = 16
|
||||
S_C_DEPARTMENT = 17
|
||||
S_C_TARGET_DATE = 18
|
||||
S_C_SOURCE = 19
|
||||
S_C_ELECTRON_BRE = 20
|
||||
S_C_MAIN_PAGE = 21
|
||||
S_C_IS_GENERAL = 22
|
||||
S_C_ACTUALIZE_PERIOD = 23
|
||||
S_C_AGE_RESTRICTION = 24
|
||||
S_C_PRIORITY = 25
|
||||
S_C_ARTICLE_TYPE = 26
|
||||
S_C_DATE_EXCHANGE = 27
|
||||
S_C_DATE_EES1 = 28
|
||||
S_C_DATE_EX_TOOLS = 29
|
||||
S_C_DATE_EES2 = 30
|
||||
S_C_EXPERT = 31
|
||||
S_C_CONTRACT = 32
|
||||
S_C_COMMENT = 33
|
||||
S_C_TASK_ID = 34
|
||||
S_C_CONTENT_NAME_DB = 35
|
||||
S_C_TASK_NAME = 36
|
||||
S_C_PARENT_ID = 37
|
||||
|
||||
[_Last] = 37
|
||||
End Enum
|
||||
|
||||
Public row_ As Long
|
||||
|
||||
Private data_ As Excel.Worksheet
|
||||
|
||||
Public Sub Init(target As Excel.Worksheet, Optional tRow& = DATA_FIRST_ROW)
|
||||
Set data_ = target
|
||||
row_ = tRow
|
||||
End Sub
|
||||
|
||||
Public Function Increment(Optional inc& = 1)
|
||||
If row_ + inc > 0 Then _
|
||||
row_ = row_ + inc
|
||||
End Function
|
||||
|
||||
Public Function GoFirst()
|
||||
row_ = DATA_FIRST_ROW
|
||||
End Function
|
||||
|
||||
Public Function GoLast()
|
||||
row_ = GetLastRow
|
||||
End Function
|
||||
|
||||
Public Function GoEmpty()
|
||||
Call GoLast
|
||||
Call Increment
|
||||
End Function
|
||||
|
||||
Public Function IsDone() As Boolean
|
||||
IsDone = row_ > GetLastRow
|
||||
End Function
|
||||
|
||||
Public Function IsCanceled() As Boolean
|
||||
IsCanceled = Status = STATUS_CANCELED
|
||||
End Function
|
||||
|
||||
Public Function HasContent() As Boolean
|
||||
Dim sContent$: sContent = ContentNameDB
|
||||
HasContent = sContent <> NO_CONTENT_PLACEHOLDER And sContent <> NO_MEDIA_PLACEHOLDER
|
||||
End Function
|
||||
|
||||
Public Function RemoveRow()
|
||||
Call data_.Rows(row_).Delete
|
||||
End Function
|
||||
|
||||
Public Function FindTaskID(sID$) As Boolean
|
||||
If sID = "" Then _
|
||||
Exit Function
|
||||
Dim iFound As Excel.Range:
|
||||
Set iFound = data_.Columns(S_C_TASK_ID).Find(sID, LookAt:=xlWhole)
|
||||
FindTaskID = Not iFound Is Nothing
|
||||
If FindTaskID Then _
|
||||
row_ = iFound.Row
|
||||
End Function
|
||||
|
||||
Public Function FindContentName(sName$, sType$) As Boolean
|
||||
If sName = "" Then _
|
||||
Exit Function
|
||||
Dim iFound As Excel.Range:
|
||||
Set iFound = data_.Columns(S_C_CONTENT_NAME).Find(sName, LookAt:=xlWhole)
|
||||
If iFound Is Nothing Then _
|
||||
Exit Function
|
||||
If data_.Cells(iFound.Row, S_C_TASK_TYPE) <> sType Then _
|
||||
Exit Function
|
||||
row_ = iFound.Row
|
||||
FindContentName = True
|
||||
End Function
|
||||
|
||||
Public Function SyncCSVTasks(iInput As IteratorCSVTasks)
|
||||
TaskType = iInput.TaskType
|
||||
Status = iInput.Status
|
||||
ContentName = iInput.ContentName
|
||||
ContentNameDB = iInput.ContentNameDB
|
||||
Supervisor = iInput.Supervisor
|
||||
Executor = iInput.Executor
|
||||
TargetDate = iInput.TargetDate
|
||||
TaskID = iInput.TaskID
|
||||
TaskName = iInput.TaskName
|
||||
ParentID = iInput.ParentID
|
||||
End Function
|
||||
|
||||
Public Function SyncCSVContent(iInput As IteratorCSVContent)
|
||||
Call ValidateValue(S_C_BIBLIO_NAME, iInput.BiblioName)
|
||||
Call ValidateValue(S_C_CHANGE_SCORE, iInput.ChangeScore)
|
||||
Call ValidateValue(S_C_DEPARTMENT, iInput.Department)
|
||||
Call ValidateValue(S_C_RESPONSIBLE, iInput.Responsible)
|
||||
Call ValidateValue(S_C_EDITOR, iInput.Editor)
|
||||
Call ValidateValue(S_C_DEFINITION, iInput.Definition)
|
||||
Call ValidateValue(S_C_OBJECT_TYPE, iInput.ObjectType)
|
||||
Call ValidateListValue(S_C_MARKERS, iInput.Markers)
|
||||
Call ValidateListValue(S_C_TAGS, iInput.Tags)
|
||||
Call ValidateValue(S_C_SOURCE, iInput.Source)
|
||||
Call ValidateValue(S_C_ELECTRON_BRE, iInput.ElectronBre)
|
||||
Call ValidateValue(S_C_MAIN_PAGE, iInput.MainPage)
|
||||
Call ValidateValue(S_C_IS_GENERAL, iInput.IsGeneral)
|
||||
Call ValidateValue(S_C_ACTUALIZE_PERIOD, iInput.ActualizePeriod)
|
||||
Call ValidateValue(S_C_AGE_RESTRICTION, iInput.AgeRestriction)
|
||||
Call ValidateValue(S_C_AUTHOR, iInput.Author)
|
||||
If UpdateStatus = T_UPD_ONCE Then _
|
||||
UpdateStatus = T_UPD_UNDEF
|
||||
Call RecalculateStatus
|
||||
End Function
|
||||
|
||||
Public Function SyncContent(iInput As IteratorContent)
|
||||
TaskType = iInput.TaskType
|
||||
Status = iInput.Status
|
||||
ContentName = iInput.ContentName
|
||||
Supervisor = iInput.Supervisor
|
||||
Executor = iInput.Executor
|
||||
TargetDate = iInput.TargetDate
|
||||
|
||||
IsImmutable = iInput.IsImmutable
|
||||
IsMain = iInput.IsMain
|
||||
IsGeneral = iInput.IsGeneral
|
||||
IsBRE = iInput.IsBRE
|
||||
|
||||
UpdateStatus = iInput.UpdateStatus
|
||||
If iInput.BiblioName <> "" Then BiblioName = iInput.BiblioName
|
||||
If iInput.ChangeScore <> "" Then ChangeScore = iInput.ChangeScore
|
||||
If iInput.Definition <> "" Then Definition = iInput.Definition
|
||||
If iInput.ObjectType <> "" Then ObjectType = iInput.ObjectType
|
||||
If iInput.Markers <> "" Then Markers = iInput.Markers
|
||||
If iInput.Author <> "" Then Author = iInput.Author
|
||||
If iInput.Tags <> "" Then Tags = iInput.Tags
|
||||
If iInput.Editor <> "" Then Editor = iInput.Editor
|
||||
If iInput.Responsible <> "" Then Responsible = iInput.Responsible
|
||||
If iInput.Department <> "" Then Department = iInput.Department
|
||||
If iInput.Source <> "" Then Source = iInput.Source
|
||||
If iInput.ActualizePeriod <> "" Then ActualizePeriod = iInput.ActualizePeriod
|
||||
If iInput.AgeRestriction <> "" Then AgeRestriction = iInput.AgeRestriction
|
||||
If iInput.Priority <> "" Then Priority = iInput.Priority
|
||||
If iInput.ArticleType <> "" Then ArticleType = iInput.ArticleType
|
||||
If iInput.Expert <> "" Then Expert = iInput.Expert
|
||||
If iInput.Contract <> "" Then Contract = iInput.Contract
|
||||
If iInput.ContentNameDB <> "" Then ContentNameDB = iInput.ContentNameDB
|
||||
If iInput.TaskID <> "" Then TaskID = iInput.TaskID
|
||||
If iInput.TaskName <> "" Then TaskName = iInput.TaskName
|
||||
If iInput.ParentID <> "" Then ParentID = iInput.ParentID
|
||||
|
||||
Call RecalculateStatus
|
||||
End Function
|
||||
|
||||
Public Function HasBlanks() As Boolean
|
||||
HasBlanks = True
|
||||
|
||||
If BiblioName = "" Then _
|
||||
Exit Function
|
||||
If Definition = "" Then _
|
||||
Exit Function
|
||||
If Markers = "" Then _
|
||||
Exit Function
|
||||
If ObjectType = "" Then _
|
||||
Exit Function
|
||||
If Supervisor = "" Then _
|
||||
Exit Function
|
||||
If Editor = "" Then _
|
||||
Exit Function
|
||||
|
||||
HasBlanks = False
|
||||
End Function
|
||||
|
||||
Public Function IsIgnored() As Boolean
|
||||
Dim oldStatus As TUpdateStatus: oldStatus = UpdateStatus
|
||||
IsIgnored = oldStatus = T_UPD_COMPLETE Or oldStatus = T_UPD_IGNORE
|
||||
End Function
|
||||
|
||||
Public Function NeedsUpdate() As Boolean
|
||||
Dim oldStatus As TUpdateStatus: oldStatus = UpdateStatus
|
||||
NeedsUpdate = oldStatus = T_UPD_AUTO Or oldStatus = T_UPD_ONCE Or oldStatus = T_UPD_ALWAYS
|
||||
End Function
|
||||
|
||||
Public Function RecalculateStatus() As TUpdateStatus
|
||||
Dim oldStatus As TUpdateStatus: oldStatus = UpdateStatus
|
||||
RecalculateStatus = oldStatus
|
||||
If oldStatus = T_UPD_ONCE Or oldStatus = T_UPD_ALWAYS Or oldStatus = T_UPD_IGNORE Then _
|
||||
Exit Function
|
||||
If TaskID = "" Then
|
||||
UpdateStatus = T_UPD_UNDEF
|
||||
ElseIf ContentNameDB = "Íåò Êîíòåíòà" Or Status = "Îòìåíåíà" Then
|
||||
UpdateStatus = T_UPD_UNDEF
|
||||
ElseIf HasBlanks Then
|
||||
UpdateStatus = T_UPD_AUTO
|
||||
Else
|
||||
UpdateStatus = T_UPD_COMPLETE
|
||||
End If
|
||||
RecalculateStatus = UpdateStatus
|
||||
End Function
|
||||
|
||||
' ======== Property Get =========
|
||||
Public Property Get TaskType() As String
|
||||
TaskType = data_.Cells(row_, S_C_TASK_TYPE)
|
||||
End Property
|
||||
|
||||
Public Property Get Status() As String
|
||||
Status = data_.Cells(row_, S_C_STATUS)
|
||||
End Property
|
||||
|
||||
Public Property Get ContentName() As String
|
||||
ContentName = data_.Cells(row_, S_C_CONTENT_NAME)
|
||||
End Property
|
||||
|
||||
Public Property Get UpdateStatus() As TUpdateStatus
|
||||
UpdateStatus = UpdateStatusFromText(data_.Cells(row_, S_C_UPDATE_STATUS))
|
||||
End Property
|
||||
|
||||
Public Property Get ChangeScore() As String
|
||||
ChangeScore = data_.Cells(row_, S_C_CHANGE_SCORE)
|
||||
End Property
|
||||
|
||||
Public Property Get BiblioName() As String
|
||||
BiblioName = data_.Cells(row_, S_C_BIBLIO_NAME)
|
||||
End Property
|
||||
|
||||
Public Property Get Definition() As String
|
||||
Definition = data_.Cells(row_, S_C_DEFINITION)
|
||||
End Property
|
||||
|
||||
Public Property Get IsImmutable() As Boolean
|
||||
IsImmutable = data_.Cells(row_, S_C_IS_IMMUTABLE) = BOOL_TEXT_YES
|
||||
End Property
|
||||
|
||||
Public Property Get ObjectType() As String
|
||||
ObjectType = data_.Cells(row_, S_C_OBJECT_TYPE)
|
||||
End Property
|
||||
|
||||
Public Property Get Markers() As String
|
||||
Markers = data_.Cells(row_, S_C_MARKERS)
|
||||
End Property
|
||||
|
||||
Public Property Get Tags() As String
|
||||
Tags = data_.Cells(row_, S_C_TAGS)
|
||||
End Property
|
||||
|
||||
Public Property Get Author() As String
|
||||
Author = data_.Cells(row_, S_C_AUTHOR)
|
||||
End Property
|
||||
|
||||
Public Property Get Supervisor() As String
|
||||
Supervisor = data_.Cells(row_, S_C_SUPERVISOR)
|
||||
End Property
|
||||
|
||||
Public Property Get Executor() As String
|
||||
Executor = data_.Cells(row_, S_C_EXECUTOR)
|
||||
End Property
|
||||
|
||||
Public Property Get Editor() As String
|
||||
Editor = data_.Cells(row_, S_C_EDITOR)
|
||||
End Property
|
||||
|
||||
Public Property Get Responsible() As String
|
||||
Responsible = data_.Cells(row_, S_C_RESPONSIBLE)
|
||||
End Property
|
||||
|
||||
Public Property Get Department() As String
|
||||
Department = data_.Cells(row_, S_C_DEPARTMENT)
|
||||
End Property
|
||||
|
||||
Public Property Get TargetDate() As String
|
||||
TargetDate = data_.Cells(row_, S_C_TARGET_DATE)
|
||||
End Property
|
||||
|
||||
Public Property Get Source() As String
|
||||
Source = data_.Cells(row_, S_C_SOURCE)
|
||||
End Property
|
||||
|
||||
Public Property Get IsBRE() As Boolean
|
||||
IsBRE = data_.Cells(row_, S_C_ELECTRON_BRE) = BOOL_TEXT_YES
|
||||
End Property
|
||||
|
||||
Public Property Get IsMain() As Boolean
|
||||
IsMain = data_.Cells(row_, S_C_MAIN_PAGE) = BOOL_TEXT_YES
|
||||
End Property
|
||||
|
||||
Public Property Get IsGeneral() As Boolean
|
||||
IsGeneral = data_.Cells(row_, S_C_IS_GENERAL) = BOOL_TEXT_YES
|
||||
End Property
|
||||
|
||||
Public Property Get ActualizePeriod() As String
|
||||
ActualizePeriod = data_.Cells(row_, S_C_ACTUALIZE_PERIOD)
|
||||
End Property
|
||||
|
||||
Public Property Get AgeRestriction() As String
|
||||
AgeRestriction = data_.Cells(row_, S_C_AGE_RESTRICTION)
|
||||
End Property
|
||||
|
||||
Public Property Get Priority() As String
|
||||
Priority = data_.Cells(row_, S_C_PRIORITY)
|
||||
End Property
|
||||
|
||||
Public Property Get ArticleType() As String
|
||||
ArticleType = data_.Cells(row_, S_C_ARTICLE_TYPE)
|
||||
End Property
|
||||
|
||||
Public Property Get DateExchange() As String
|
||||
DateExchange = data_.Cells(row_, S_C_DATE_EXCHANGE)
|
||||
End Property
|
||||
|
||||
Public Property Get DateEES1() As String
|
||||
DateEES1 = data_.Cells(row_, S_C_DATE_EES1)
|
||||
End Property
|
||||
|
||||
Public Property Get DateTools() As String
|
||||
DateTools = data_.Cells(row_, S_C_DATE_EX_TOOLS)
|
||||
End Property
|
||||
|
||||
Public Property Get DateEES2() As String
|
||||
DateEES2 = data_.Cells(row_, S_C_DATE_EES2)
|
||||
End Property
|
||||
|
||||
Public Property Get Expert() As String
|
||||
Expert = data_.Cells(row_, S_C_EXPERT)
|
||||
End Property
|
||||
|
||||
Public Property Get Contract() As String
|
||||
Contract = data_.Cells(row_, S_C_CONTRACT)
|
||||
End Property
|
||||
|
||||
Public Property Get Comment() As String
|
||||
Comment = data_.Cells(row_, S_C_COMMENT)
|
||||
End Property
|
||||
|
||||
Public Property Get TaskID() As String
|
||||
TaskID = data_.Cells(row_, S_C_TASK_ID)
|
||||
End Property
|
||||
|
||||
Public Property Get ContentNameDB() As String
|
||||
ContentNameDB = data_.Cells(row_, S_C_CONTENT_NAME_DB)
|
||||
End Property
|
||||
|
||||
Public Property Get TaskName() As String
|
||||
TaskName = data_.Cells(row_, S_C_TASK_NAME)
|
||||
End Property
|
||||
|
||||
Public Property Get ParentID() As String
|
||||
ParentID = data_.Cells(row_, S_C_PARENT_ID)
|
||||
End Property
|
||||
|
||||
' ==== Property Let ====
|
||||
Public Property Let TaskID(newVal$)
|
||||
Dim oldVal$: oldVal = data_.Cells(row_, S_C_TASK_ID)
|
||||
data_.Cells(row_, S_C_TASK_ID) = newVal
|
||||
If newVal <> "" And (oldVal <> newVal Or data_.Cells(row_, S_C_TASK_TYPE).Hyperlinks.Count = 0) Then
|
||||
Call XLUpdateHyperlink(data_.Cells(row_, S_C_TASK_TYPE), URL_PREFIX_TASK & newVal)
|
||||
If HasContent Then
|
||||
Call XLUpdateHyperlink(data_.Cells(row_, S_C_CONTENT_NAME), URL_PREFIX_CONTENT & newVal)
|
||||
Else
|
||||
Call data_.Cells(row_, S_C_CONTENT_NAME).Hyperlinks.Delete
|
||||
End If
|
||||
End If
|
||||
End Property
|
||||
|
||||
Public Property Let TaskType(newVal$)
|
||||
data_.Cells(row_, S_C_TASK_TYPE) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let Status(newVal$)
|
||||
data_.Cells(row_, S_C_STATUS) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let ContentName(newVal$)
|
||||
data_.Cells(row_, S_C_CONTENT_NAME) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let UpdateStatus(newVal As TUpdateStatus)
|
||||
data_.Cells(row_, S_C_UPDATE_STATUS) = UpdateStatusToText(newVal)
|
||||
End Property
|
||||
|
||||
Public Property Let ChangeScore(newVal$)
|
||||
data_.Cells(row_, S_C_CHANGE_SCORE) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let BiblioName(newVal$)
|
||||
data_.Cells(row_, S_C_BIBLIO_NAME) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let Definition(newVal$)
|
||||
data_.Cells(row_, S_C_DEFINITION) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let IsImmutable(newVal As Boolean)
|
||||
data_.Cells(row_, S_C_IS_IMMUTABLE) = IIf(newVal, BOOL_TEXT_YES, BOOL_TEXT_NO)
|
||||
End Property
|
||||
|
||||
Public Property Let ObjectType(newVal$)
|
||||
data_.Cells(row_, S_C_OBJECT_TYPE) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let Markers(newVal$)
|
||||
data_.Cells(row_, S_C_MARKERS) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let Tags(newVal$)
|
||||
data_.Cells(row_, S_C_TAGS) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let Author(newVal$)
|
||||
data_.Cells(row_, S_C_AUTHOR) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let Supervisor(newVal$)
|
||||
data_.Cells(row_, S_C_SUPERVISOR) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let Executor(newVal$)
|
||||
data_.Cells(row_, S_C_EXECUTOR) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let Editor(newVal$)
|
||||
data_.Cells(row_, S_C_EDITOR) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let Responsible(newVal$)
|
||||
data_.Cells(row_, S_C_RESPONSIBLE) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let Department(newVal$)
|
||||
data_.Cells(row_, S_C_DEPARTMENT) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let TargetDate(newVal$)
|
||||
data_.Cells(row_, S_C_TARGET_DATE) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let Source(newVal$)
|
||||
data_.Cells(row_, S_C_SOURCE) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let IsBRE(newVal As Boolean)
|
||||
data_.Cells(row_, S_C_ELECTRON_BRE) = IIf(newVal, BOOL_TEXT_YES, BOOL_TEXT_NO)
|
||||
End Property
|
||||
|
||||
Public Property Let IsMain(newVal As Boolean)
|
||||
data_.Cells(row_, S_C_MAIN_PAGE) = IIf(newVal, BOOL_TEXT_YES, BOOL_TEXT_NO)
|
||||
End Property
|
||||
|
||||
Public Property Let IsGeneral(newVal As Boolean)
|
||||
data_.Cells(row_, S_C_IS_GENERAL) = IIf(newVal, BOOL_TEXT_YES, BOOL_TEXT_NO)
|
||||
End Property
|
||||
|
||||
Public Property Let ActualizePeriod(newVal$)
|
||||
data_.Cells(row_, S_C_ACTUALIZE_PERIOD) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let AgeRestriction(newVal$)
|
||||
data_.Cells(row_, S_C_AGE_RESTRICTION) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let Priority(newVal$)
|
||||
data_.Cells(row_, S_C_PRIORITY) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let ArticleType(newVal$)
|
||||
data_.Cells(row_, S_C_ARTICLE_TYPE) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let DateExchange(newVal$)
|
||||
data_.Cells(row_, S_C_DATE_EXCHANGE) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let DateEES1(newVal$)
|
||||
data_.Cells(row_, S_C_DATE_EES1) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let DateTools(newVal$)
|
||||
data_.Cells(row_, S_C_DATE_EX_TOOLS) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let DateEES2(newVal$)
|
||||
data_.Cells(row_, S_C_DATE_EES2) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let Expert(newVal$)
|
||||
data_.Cells(row_, S_C_EXPERT) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let Contract(newVal$)
|
||||
data_.Cells(row_, S_C_CONTRACT) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let ContentNameDB(newVal$)
|
||||
data_.Cells(row_, S_C_CONTENT_NAME_DB) = newVal
|
||||
End Property
|
||||
|
||||
Public Property Let TaskName(newVal$)
|
||||
data_.Cells(row_, S_C_TASK_NAME) = newVal
|
||||
If data_.Cells(row_, S_C_IS_IMMUTABLE) = "" Then
|
||||
If newVal Like "Íåèçìåííûå *" Then
|
||||
data_.Cells(row_, S_C_IS_IMMUTABLE) = BOOL_TEXT_YES
|
||||
Else
|
||||
data_.Cells(row_, S_C_IS_IMMUTABLE) = BOOL_TEXT_NO
|
||||
End If
|
||||
End If
|
||||
End Property
|
||||
|
||||
Public Property Let ParentID(newVal$)
|
||||
data_.Cells(row_, S_C_PARENT_ID) = newVal
|
||||
End Property
|
||||
|
||||
' =======
|
||||
Private Function GetLastRow() As Long
|
||||
GetLastRow = data_.Cells(data_.Rows.Count, 1).End(xlUp).Row
|
||||
End Function
|
||||
|
||||
Private Function ColorCell(nColumn&, nColor&)
|
||||
data_.Cells(row_, nColumn).Interior.Color = nColor
|
||||
End Function
|
||||
|
||||
Private Function ValidateValue(nColumn&, portalValue$)
|
||||
Dim sValue$: sValue = data_.Cells(row_, nColumn)
|
||||
If sValue = portalValue Then _
|
||||
Exit Function
|
||||
|
||||
If sValue = "" Then
|
||||
data_.Cells(row_, nColumn) = portalValue
|
||||
Exit Function
|
||||
End If
|
||||
|
||||
If portalValue = "" Then
|
||||
Call ColorCell(nColumn, RGB(142, 169, 219))
|
||||
Else
|
||||
Call ColorCell(nColumn, RGB(255, 151, 151))
|
||||
End If
|
||||
End Function
|
||||
|
||||
Private Function ValidateListValue(nColumn&, portalValue$)
|
||||
Dim sValue$: sValue = data_.Cells(row_, nColumn)
|
||||
If CheckListsEqual(sValue, portalValue) Then _
|
||||
Exit Function
|
||||
|
||||
If sValue = "" Then
|
||||
data_.Cells(row_, nColumn) = portalValue
|
||||
Exit Function
|
||||
End If
|
||||
|
||||
If portalValue = "" Then
|
||||
Call ColorCell(nColumn, RGB(142, 169, 219))
|
||||
Else
|
||||
Call ColorCell(nColumn, RGB(255, 151, 151))
|
||||
End If
|
||||
End Function
|
152
src/Main.bas
Normal file
152
src/Main.bas
Normal file
|
@ -0,0 +1,152 @@
|
|||
Attribute VB_Name = "Main"
|
||||
Option Explicit
|
||||
|
||||
Public Sub RunImportCSV()
|
||||
Dim sFile$: sFile = UserInteraction.PromptFileFilter(ThisWorkbook.Path, _
|
||||
sDescription:="Âûãðóçêà ïîðòàëà CSV", _
|
||||
sFilter:="*.csv")
|
||||
If sFile = vbNullString Then _
|
||||
Exit Sub
|
||||
If Not ProcessCSV(sFile) Then _
|
||||
Exit Sub
|
||||
Call UserInteraction.ShowMessage(IM_IMPORT_SUCCESS)
|
||||
End Sub
|
||||
|
||||
Public Sub RunImportDB()
|
||||
Dim sFile$: sFile = UserInteraction.PromptFileFilter(ThisWorkbook.Path, _
|
||||
sDescription:="Òàáëèöà Excel", _
|
||||
sFilter:="*.xlsx;*.xls;*.xlsm")
|
||||
If sFile = vbNullString Then _
|
||||
Exit Sub
|
||||
|
||||
Dim xlInput As New API_XLWrapper: Call xlInput.SetApplication(ThisWorkbook.Application)
|
||||
If xlInput.OpenDocument(sFile, bReadOnly:=True) Is Nothing Then
|
||||
Call UserInteraction.ShowMessage(EM_FILE_CANNOT_OPEN, sFile)
|
||||
Exit Sub
|
||||
End If
|
||||
|
||||
Call xlInput.PauseUI
|
||||
|
||||
Dim iInput As New DB_Content: Call iInput.Init(xlInput.Document.Sheets(SHEET_CONTENT), xlInput.Document.Worksheets(SHEET_ATTRIBUTES))
|
||||
Call ImportDataFromDB(iInput, AccessContent)
|
||||
|
||||
Call xlInput.ResumeUI
|
||||
Call xlInput.ReleaseDocument
|
||||
Call UserInteraction.ShowMessage(IM_IMPORT_SUCCESS)
|
||||
End Sub
|
||||
|
||||
Public Sub RunEditConfig()
|
||||
Call ThisWorkbook.Worksheets(SHEET_CONFIG).Activate
|
||||
End Sub
|
||||
|
||||
Public Sub RunUpdateTasks()
|
||||
Dim iConfig As InfoConfig: Set iConfig = AccessConfig
|
||||
Call iConfig.SetScanTasks(True)
|
||||
Call iConfig.SetScanContent(False)
|
||||
Call ExecuteUpdateRequest(iConfig)
|
||||
End Sub
|
||||
|
||||
Public Sub RunUpdateContent()
|
||||
Dim iConfig As InfoConfig: Set iConfig = AccessConfig
|
||||
Call iConfig.SetScanTasks(False)
|
||||
Call iConfig.SetScanContent(True)
|
||||
Call ExecuteUpdateRequest(iConfig)
|
||||
End Sub
|
||||
|
||||
Public Sub RunUpdatePortal()
|
||||
Dim iConfig As InfoConfig: Set iConfig = AccessConfig
|
||||
Call iConfig.SetScanTasks(True)
|
||||
Call iConfig.SetScanContent(True)
|
||||
Call ExecuteUpdateRequest(iConfig)
|
||||
End Sub
|
||||
|
||||
Public Sub RunClearData()
|
||||
Call ClearData
|
||||
Call UserInteraction.ShowMessage(IM_DATA_DELETED)
|
||||
End Sub
|
||||
|
||||
Public Sub RunUnstuck()
|
||||
Dim uiWrap As New API_XLWrapper: Call uiWrap.SetDocument(ThisWorkbook)
|
||||
Call uiWrap.ResumeUI
|
||||
End Sub
|
||||
|
||||
Public Sub RunInputMarks()
|
||||
Dim iTarget As Excel.Range: Set iTarget = Excel.Selection.Cells(1, 1)
|
||||
Call CSE_ListSelector.Init(ThisWorkbook.Worksheets(SHEET_OPTIONS).ListObjects(TABLE_MARKERS))
|
||||
Call CSE_ListSelector.Show
|
||||
If CSE_ListSelector.isCanceled_ Then _
|
||||
Exit Sub
|
||||
|
||||
iTarget = CSE_ListSelector.GetSelectedStr
|
||||
Call Unload(CSE_ListSelector)
|
||||
End Sub
|
||||
|
||||
Public Sub RunInputTags()
|
||||
Dim iTarget As Excel.Range: Set iTarget = Excel.Selection.Cells(1, 1)
|
||||
Call CSE_ListSelector.Init(ThisWorkbook.Worksheets(SHEET_OPTIONS).ListObjects(TABLE_TAGS))
|
||||
Call CSE_ListSelector.Show
|
||||
If CSE_ListSelector.isCanceled_ Then _
|
||||
Exit Sub
|
||||
|
||||
iTarget = CSE_ListSelector.GetSelectedStr
|
||||
Call Unload(CSE_ListSelector)
|
||||
End Sub
|
||||
|
||||
' =======
|
||||
Private Function ProcessCSV(sFile$) As Boolean
|
||||
ProcessCSV = False
|
||||
|
||||
Dim dataIn As Excel.Worksheet: Set dataIn = ThisWorkbook.Worksheets.Add
|
||||
With dataIn.QueryTables.Add(Connection:="TEXT;" & sFile, Destination:=dataIn.Cells(1, 1))
|
||||
.TextFileParseType = xlDelimited
|
||||
.TextFileCommaDelimiter = True
|
||||
.TextFilePlatform = 65001 ' UTF-8
|
||||
.Refresh
|
||||
End With
|
||||
|
||||
Dim sID$: sID = dataIn.Cells(1, 1)
|
||||
If sID <> "" Then
|
||||
If VBA.Left(sID, 1) Like "[0-9a-f]" Then
|
||||
Dim iContent As New IteratorCSVContent: Call iContent.Init(dataIn)
|
||||
Call ImportContentFromCSV(iContent, AccessContent)
|
||||
Else
|
||||
Dim iTasks As New IteratorCSVTasks: Call iTasks.Init(dataIn, AccessWorkers)
|
||||
Call ImportTasksFromCSV(iTasks, AccessContent)
|
||||
End If
|
||||
End If
|
||||
|
||||
|
||||
Dim bAlerts As Boolean: bAlerts = Excel.Application.DisplayAlerts
|
||||
Excel.Application.DisplayAlerts = False
|
||||
Call dataIn.QueryTables(1).Delete
|
||||
Call dataIn.Delete
|
||||
Excel.Application.DisplayAlerts = bAlerts
|
||||
|
||||
ProcessCSV = True
|
||||
End Function
|
||||
|
||||
Private Function ExecuteUpdateRequest(iConfig As InfoConfig)
|
||||
Call iConfig.CreateConfigFile
|
||||
|
||||
Dim bScanPortal As Boolean: bScanPortal = PortalUpdate(iConfig)
|
||||
' Call iConfig.DeleteConfigFile
|
||||
If Not bScanPortal Then _
|
||||
Exit Function
|
||||
|
||||
Dim bProcessTasks As Boolean: bProcessTasks = True
|
||||
If iConfig.ScanTasks Then
|
||||
Dim sFile$: sFile = iConfig.OutputFileTasks
|
||||
bProcessTasks = ProcessCSV(sFile)
|
||||
' Call Kill(sFile)
|
||||
End If
|
||||
|
||||
Dim bProcessContent As Boolean: bProcessContent = True
|
||||
If iConfig.ScanContent Then
|
||||
sFile = iConfig.OutputFileContent
|
||||
bProcessContent = ProcessCSV(sFile)
|
||||
' Call Kill(sFile)
|
||||
End If
|
||||
|
||||
If bProcessTasks And bProcessContent Then _
|
||||
Call UserInteraction.ShowMessage(IM_IMPORT_SUCCESS)
|
||||
End Function
|
143
src/MainImpl.bas
Normal file
143
src/MainImpl.bas
Normal file
|
@ -0,0 +1,143 @@
|
|||
Attribute VB_Name = "MainImpl"
|
||||
Option Private Module
|
||||
Option Explicit
|
||||
|
||||
Public Function AccessContent() As DB_Content
|
||||
Static s_Content As DB_Content
|
||||
|
||||
If s_Content Is Nothing Then
|
||||
Set s_Content = New DB_Content
|
||||
Call s_Content.Init(ThisWorkbook.Worksheets(SHEET_CONTENT), ThisWorkbook.Worksheets(SHEET_ATTRIBUTES))
|
||||
End If
|
||||
|
||||
Set AccessContent = s_Content
|
||||
End Function
|
||||
|
||||
Public Function AccessWorkers() As DB_Workers
|
||||
Static s_Workerks As DB_Workers
|
||||
|
||||
If s_Workerks Is Nothing Then
|
||||
Set s_Workerks = New DB_Workers
|
||||
Call s_Workerks.Init(ThisWorkbook.Worksheets(SHEET_OPTIONS).ListObjects(TABLE_WORKERS))
|
||||
End If
|
||||
|
||||
Set AccessWorkers = s_Workerks
|
||||
End Function
|
||||
|
||||
Public Function AccessConfig() As InfoConfig
|
||||
Set AccessConfig = New InfoConfig
|
||||
Call AccessConfig.Init(ThisWorkbook.Worksheets(SHEET_CONFIG), AccessContent)
|
||||
End Function
|
||||
|
||||
Public Function ClearData()
|
||||
Call XLShowAllData(ThisWorkbook.Sheets(SHEET_CONTENT))
|
||||
Call ThisWorkbook.Sheets(SHEET_CONTENT).UsedRange.Offset(1, 0).Rows.EntireRow.Delete
|
||||
|
||||
Call XLShowAllData(ThisWorkbook.Sheets(SHEET_ATTRIBUTES))
|
||||
Call ThisWorkbook.Sheets(SHEET_ATTRIBUTES).UsedRange.Offset(1, 0).Rows.EntireRow.Delete
|
||||
End Function
|
||||
|
||||
Public Function ImportTasksFromCSV(iInput As IteratorCSVTasks, iOutput As DB_Content)
|
||||
Call CSE_ProgressBar.Init("Çàãðóçêà çàäà÷è èç CSV", maxVal:=iInput.CountRows)
|
||||
Call CSE_ProgressBar.ShowModeless
|
||||
|
||||
Call iOutput.EnsureDataVisible
|
||||
Call iOutput.ImportCSVTasks(iInput)
|
||||
|
||||
Call Unload(CSE_ProgressBar)
|
||||
End Function
|
||||
|
||||
Public Function ImportContentFromCSV(iInput As IteratorCSVContent, iOutput As DB_Content)
|
||||
Call CSE_ProgressBar.Init("Çàãðóçêà ìåòàäàííûõ èç CSV", maxVal:=iInput.CountRows)
|
||||
Call CSE_ProgressBar.ShowModeless
|
||||
|
||||
Call iOutput.EnsureDataVisible
|
||||
Call iOutput.ImportCSVContent(iInput)
|
||||
|
||||
Call Unload(CSE_ProgressBar)
|
||||
End Function
|
||||
|
||||
Public Function ImportDataFromDB(iInput As DB_Content, iOutput As DB_Content)
|
||||
Call CSE_ProgressBar.Init("Çàãðóçêà áàçû", maxVal:=iInput.Count)
|
||||
Call CSE_ProgressBar.ShowModeless
|
||||
|
||||
Call iInput.EnsureDataVisible
|
||||
Call iOutput.EnsureDataVisible
|
||||
Call iOutput.ImportDB(iInput)
|
||||
|
||||
Call Unload(CSE_ProgressBar)
|
||||
End Function
|
||||
|
||||
Public Function PortalUpdate(iConfig As InfoConfig) As Boolean
|
||||
PortalUpdate = False
|
||||
Dim sExec$: sExec = EXPORTER_EXECUTABLE & " " & EXPORTER_CONFIG_FILE
|
||||
Dim fso As New Scripting.FileSystemObject
|
||||
If Not fso.FileExists(ThisWorkbook.Path & "\" & EXPORTER_EXECUTABLE) Then
|
||||
Call UserInteraction.ShowMessage(EM_MISSING_EXEC, EXPORTER_EXECUTABLE)
|
||||
Exit Function
|
||||
End If
|
||||
|
||||
Dim iShell As New WshShell
|
||||
iShell.CurrentDirectory = ThisWorkbook.Path
|
||||
On Error GoTo REPORT_EXEC
|
||||
If iShell.Run(sExec, WaitOnReturn:=True) <> 0 Then
|
||||
REPORT_EXEC:
|
||||
Call UserInteraction.ShowMessage(EM_CANNOT_EXEC, sExec)
|
||||
Exit Function
|
||||
End If
|
||||
On Error GoTo 0
|
||||
|
||||
Dim sOutput1$: sOutput1 = iConfig.OutputFileTasks
|
||||
If Not fso.FileExists(sOutput1) Then
|
||||
Call UserInteraction.ShowMessage(EM_MISSING_FILE, sExec)
|
||||
Exit Function
|
||||
End If
|
||||
|
||||
Dim sOutput2$: sOutput2 = iConfig.OutputFileContent
|
||||
If iConfig.ScanContent And Not fso.FileExists(sOutput2) Then
|
||||
Call UserInteraction.ShowMessage(EM_MISSING_FILE, sExec)
|
||||
Exit Function
|
||||
End If
|
||||
PortalUpdate = True
|
||||
End Function
|
||||
|
||||
Public Function CheckListsEqual(list1$, list2$) As Boolean
|
||||
CheckListsEqual = False
|
||||
Dim items1 As Variant: items1 = VBA.Split(list1, ";")
|
||||
Dim items2 As Variant: items2 = VBA.Split(list2, ";")
|
||||
If UBound(items1) <> UBound(items2) Then _
|
||||
Exit Function
|
||||
|
||||
Dim it1 As Variant
|
||||
Dim it2 As Variant
|
||||
Dim flagExists As Boolean
|
||||
For Each it1 In items1
|
||||
flagExists = False
|
||||
|
||||
For Each it2 In items2
|
||||
If it1 = it2 Then
|
||||
flagExists = True
|
||||
Exit For
|
||||
End If
|
||||
Next it2
|
||||
|
||||
If Not flagExists Then _
|
||||
Exit Function
|
||||
Next it1
|
||||
|
||||
For Each it2 In items2
|
||||
flagExists = False
|
||||
|
||||
For Each it1 In items1
|
||||
If it1 = it2 Then
|
||||
flagExists = True
|
||||
Exit For
|
||||
End If
|
||||
Next it1
|
||||
|
||||
If Not flagExists Then _
|
||||
Exit Function
|
||||
Next it2
|
||||
|
||||
CheckListsEqual = True
|
||||
End Function
|
61
src/z_UIMessages.bas
Normal file
61
src/z_UIMessages.bas
Normal file
|
@ -0,0 +1,61 @@
|
|||
Attribute VB_Name = "z_UIMessages"
|
||||
' Ìîäóëü âûâîäà ñîîáùåíèé è âçàèìîäåéñòâèÿ ñ ïîëüçîâàòåëåì
|
||||
Option Explicit
|
||||
|
||||
Public Enum MsgCode
|
||||
MSG_OK = 0
|
||||
|
||||
EM_FILE_CANNOT_OPEN
|
||||
EM_CANNOT_EXEC
|
||||
EM_MISSING_FILE
|
||||
EM_ITEM_EXISTS
|
||||
EM_MISSING_EXEC
|
||||
|
||||
IM_IMPORT_SUCCESS
|
||||
IM_DATA_DELETED
|
||||
|
||||
' QM_MERGE_WARNING
|
||||
End Enum
|
||||
|
||||
Private g_UI As API_UserInteraction
|
||||
|
||||
Public Function UserInteraction() As API_UserInteraction
|
||||
If g_UI Is Nothing Then _
|
||||
Set g_UI = New API_UserInteraction
|
||||
Set UserInteraction = g_UI
|
||||
End Function
|
||||
|
||||
Public Function SetUserInteraction(newUI As API_UserInteraction)
|
||||
Set g_UI = newUI
|
||||
End Function
|
||||
|
||||
Public Function UIShowMessage(theCode As MsgCode, ParamArray params() As Variant)
|
||||
Dim unwrapped As Variant: unwrapped = params
|
||||
unwrapped = FixForwardedParams(unwrapped)
|
||||
|
||||
Select Case theCode
|
||||
Case EM_FILE_CANNOT_OPEN: Call MsgBox(Fmt("Íå óäàëîñü îòêðûòü ôàéë {1}", unwrapped), vbExclamation)
|
||||
Case EM_CANNOT_EXEC: Call MsgBox(Fmt("Íå óäàëîñü âûïîëíèòü êîìàíäó" & vbNewLine & """{1}""", unwrapped), vbExclamation)
|
||||
Case EM_MISSING_FILE: Call MsgBox(Fmt("Íå íàéäåí ôàéë {1}", unwrapped), vbExclamation)
|
||||
Case EM_ITEM_EXISTS: Call MsgBox(Fmt("Ýëåìåíò óæå ïðèñóòñòâóåò â ñïèñêå: {1}", unwrapped), vbExclamation)
|
||||
Case EM_MISSING_EXEC: Call MsgBox(Fmt("Íå íàéäåí èñïîëíÿåìûé ôàéë Python: {1}", unwrapped), vbExclamation)
|
||||
|
||||
Case IM_IMPORT_SUCCESS: Call MsgBox("Èìïîðò çàâåðøåí óñïåøíî", vbInformation)
|
||||
Case IM_DATA_DELETED: Call MsgBox("Äàííûå óäàëåíû", vbInformation)
|
||||
|
||||
Case Else: Call MsgBox("Íåâåðíûé êîä ñîîáùåíèÿ", vbCritical)
|
||||
End Select
|
||||
End Function
|
||||
|
||||
Public Function UIAskQuestion(theCode As MsgCode, ParamArray params() As Variant) As Boolean
|
||||
Dim unwrapped As Variant: unwrapped = params
|
||||
unwrapped = FixForwardedParams(unwrapped)
|
||||
|
||||
Dim answer&: answer = vbNo
|
||||
Select Case theCode
|
||||
' Case QM_DELETE_LAW: answer = MsgBox(Fmt("Âíèìàíèå! Âñå äàííûå äîêóìåíòà {1} áóäóò óäàëåíû!" & vbNewLine & "Ïðîäîëæèòü?", unwrapped), vbYesNo + vbQuestion)
|
||||
|
||||
Case Else: Call MsgBox("Íåâåðíûé êîä ñîîáùåíèÿ", vbCritical)
|
||||
End Select
|
||||
UIAskQuestion = answer = vbYes
|
||||
End Function
|
20
src/z_UIRibbon.bas
Normal file
20
src/z_UIRibbon.bas
Normal file
|
@ -0,0 +1,20 @@
|
|||
Attribute VB_Name = "z_UIRibbon"
|
||||
Option Explicit
|
||||
|
||||
Public Sub OnRibbonBtn(iControl As IRibbonControl)
|
||||
Select Case iControl.ID
|
||||
Case "ImportCSV": Call RunImportCSV
|
||||
Case "ImportDB": Call RunImportDB
|
||||
|
||||
Case "EditConfig": Call RunEditConfig
|
||||
Case "UpdateTasks": Call RunUpdateTasks
|
||||
Case "UpdateContent": Call RunUpdateContent
|
||||
Case "UpdatePortal": Call RunUpdatePortal
|
||||
|
||||
Case "InputMarks": Call RunInputMarks
|
||||
Case "InputTags": Call RunInputTags
|
||||
|
||||
Case "Unstuck": Call RunUnstuck
|
||||
Case "ClearData": Call RunClearData
|
||||
End Select
|
||||
End Sub
|
2
ui/.rels
Normal file
2
ui/.rels
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/><Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/><Relationship Id="rId5" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId4" Type="http://schemas.microsoft.com/office/2006/relationships/ui/extensibility" Target="customUI/customUI.xml"/></Relationships>
|
66
ui/customUI.xml
Normal file
66
ui/customUI.xml
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" >
|
||||
<ribbon>
|
||||
<tabs>
|
||||
<tab id="BRE" label="БРЭ">
|
||||
<group id="Actions" label="Работа с данными">
|
||||
<button id="ImportCSV" size="large"
|
||||
label="Импорт CSV"
|
||||
supertip="Импорт данных из выгрузки Портала"
|
||||
imageMso="GetPowerQueryDataFromCsv"
|
||||
onAction="OnRibbonBtn"/>
|
||||
<button id="ImportDB" size="large"
|
||||
label="Импорт базы"
|
||||
supertip="Импорт данных из другой базы"
|
||||
imageMso="FormExportToExcel"
|
||||
onAction="OnRibbonBtn"/>
|
||||
<button id="EditConfig" size="large"
|
||||
label="Настройки импорта"
|
||||
supertip="Настройка фильтров импорта данных с портала"
|
||||
imageMso="GroupDocumentLibraryManage"
|
||||
onAction="OnRibbonBtn"/>
|
||||
<button id="UpdateTasks" size="large"
|
||||
label="Запросить задачи"
|
||||
supertip="Импорт данных о задачах с портала (5-10 минут)"
|
||||
imageMso="BlogCategoriesRefresh"
|
||||
onAction="OnRibbonBtn"/>
|
||||
<button id="UpdateContent" size="large"
|
||||
label="Запросить контент"
|
||||
supertip="Импорт данных о контенте по задачам, отмеченным в Excel"
|
||||
imageMso="AnimationDuration"
|
||||
onAction="OnRibbonBtn"/>
|
||||
<button id="UpdatePortal" size="large"
|
||||
label="Обновить все"
|
||||
supertip="Менеджер импорта данных из портала"
|
||||
imageMso="AccessRefreshAllLists"
|
||||
onAction="OnRibbonBtn"/>
|
||||
|
||||
</group>
|
||||
<group id="Edit" label="Редактирование" >
|
||||
<button id="InputMarks" size="large"
|
||||
label="Словник"
|
||||
supertip="Ввод списка из словника в текущую ячейку"
|
||||
imageMso="ActiveXLabel"
|
||||
onAction="OnRibbonBtn"/>
|
||||
<button id="InputTags" size="large"
|
||||
label="Метки"
|
||||
supertip="Ввод списка из меток в текущую ячейку"
|
||||
imageMso="DataGraphicIconSet"
|
||||
onAction="OnRibbonBtn"/>
|
||||
</group>
|
||||
<group id="Debug" label="Отладка" >
|
||||
<button id="Unstuck" size="large"
|
||||
label="Восстановить"
|
||||
supertip="Включить интерфейс, отключенный в результате некорректного завершения макроса"
|
||||
imageMso="GroupListManage"
|
||||
onAction="OnRibbonBtn"/>
|
||||
<button id="ClearData" size="large"
|
||||
label="Очистить данные"
|
||||
supertip="Удалить ВСЕ данные из книги"
|
||||
imageMso="Clear"
|
||||
onAction="OnRibbonBtn"/>
|
||||
</group>
|
||||
</tab>
|
||||
</tabs>
|
||||
</ribbon>
|
||||
</customUI>
|
634
webapi/.pylintrc
Normal file
634
webapi/.pylintrc
Normal file
|
@ -0,0 +1,634 @@
|
|||
[MAIN]
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Clear in-memory caches upon conclusion of linting. Useful if running pylint
|
||||
# in a server-like mode.
|
||||
clear-cache-post-run=no
|
||||
|
||||
# Load and enable all available extensions. Use --list-extensions to see a list
|
||||
# all available extensions.
|
||||
#enable-all-extensions=
|
||||
|
||||
# In error mode, messages with a category besides ERROR or FATAL are
|
||||
# suppressed, and no reports are done by default. Error mode is compatible with
|
||||
# disabling specific errors.
|
||||
#errors-only=
|
||||
|
||||
# Always return a 0 (non-error) status code, even if lint errors are found.
|
||||
# This is primarily useful in continuous integration scripts.
|
||||
#exit-zero=
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code.
|
||||
extension-pkg-allow-list=pyconcept
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
|
||||
# for backward compatibility.)
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Return non-zero exit code if any of these messages/categories are detected,
|
||||
# even if score is above --fail-under value. Syntax same as enable. Messages
|
||||
# specified are enabled, while categories only check already-enabled messages.
|
||||
fail-on=
|
||||
|
||||
# Specify a score threshold under which the program will exit with error.
|
||||
fail-under=10
|
||||
|
||||
# Interpret the stdin as a python script, whose filename needs to be passed as
|
||||
# the module_or_package argument.
|
||||
#from-stdin=
|
||||
|
||||
# Files or directories to be skipped. They should be base names, not paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regular expressions patterns to the
|
||||
# ignore-list. The regex matches against paths and can be in Posix or Windows
|
||||
# format. Because '\\' represents the directory delimiter on Windows systems,
|
||||
# it can't be used as an escape character.
|
||||
ignore-paths=t_.*,.*migrations.*
|
||||
|
||||
# Files or directories matching the regular expression patterns are skipped.
|
||||
# The regex matches against base names, not paths. The default value ignores
|
||||
# Emacs file locks
|
||||
ignore-patterns=t_.*?py
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis). It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use, and will cap the count on Windows to
|
||||
# avoid hangs.
|
||||
jobs=1
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python module names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Minimum Python version to use for version dependent checks. Will default to
|
||||
# the version used to run pylint.
|
||||
py-version=3.9
|
||||
|
||||
# Discover python modules and packages in the file system subtree.
|
||||
recursive=no
|
||||
|
||||
# Add paths to the list of the source roots. Supports globbing patterns. The
|
||||
# source root is an absolute path or a path relative to the current working
|
||||
# directory used to determine a package namespace for modules located under the
|
||||
# source root.
|
||||
source-roots=
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
# In verbose mode, extra non-checker-related info will be displayed.
|
||||
#verbose=
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names.
|
||||
argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style. If left empty, argument names will be checked with the set
|
||||
# naming style.
|
||||
#argument-rgx=
|
||||
|
||||
# Naming style matching correct attribute names.
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style. If left empty, attribute names will be checked with the set naming
|
||||
# style.
|
||||
#attr-rgx=
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma.
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Bad variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be refused
|
||||
bad-names-rgxs=
|
||||
|
||||
# Naming style matching correct class attribute names.
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style. If left empty, class attribute names will be checked
|
||||
# with the set naming style.
|
||||
#class-attribute-rgx=
|
||||
|
||||
# Naming style matching correct class constant names.
|
||||
class-const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct class constant names. Overrides class-
|
||||
# const-naming-style. If left empty, class constant names will be checked with
|
||||
# the set naming style.
|
||||
#class-const-rgx=
|
||||
|
||||
# Naming style matching correct class names.
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-
|
||||
# style. If left empty, class names will be checked with the set naming style.
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names.
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style. If left empty, constant names will be checked with the set naming
|
||||
# style.
|
||||
#const-rgx=
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names.
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style. If left empty, function names will be checked with the set
|
||||
# naming style.
|
||||
#function-rgx=
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=i,
|
||||
j,
|
||||
k,
|
||||
ex,
|
||||
Run,
|
||||
_
|
||||
|
||||
# Good variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be accepted
|
||||
good-names-rgxs=
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name.
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names.
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style. If left empty, inline iteration names will be checked
|
||||
# with the set naming style.
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names.
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style. If left empty, method names will be checked with the set naming style.
|
||||
#method-rgx=
|
||||
|
||||
# Naming style matching correct module names.
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style. If left empty, module names will be checked with the set naming style.
|
||||
#module-rgx=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
# These decorators are taken in consideration only for invalid-name.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Regular expression matching correct type alias names. If left empty, type
|
||||
# alias names will be checked with the set naming style.
|
||||
#typealias-rgx=
|
||||
|
||||
# Regular expression matching correct type variable names. If left empty, type
|
||||
# variable names will be checked with the set naming style.
|
||||
#typevar-rgx=
|
||||
|
||||
# Naming style matching correct variable names.
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style. If left empty, variable names will be checked with the set
|
||||
# naming style.
|
||||
#variable-rgx=
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# Warn about protected attribute access inside special methods
|
||||
check-protected-access-in-special-methods=no
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp,
|
||||
asyncSetUp,
|
||||
__post_init__
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# List of regular expressions of class ancestor names to ignore when counting
|
||||
# public methods (see R0903)
|
||||
exclude-too-few-public-methods=
|
||||
|
||||
# List of qualified class names to ignore when counting class parents (see
|
||||
# R0901)
|
||||
ignored-parents=
|
||||
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an if statement (see R0916).
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when caught.
|
||||
overgeneral-exceptions=builtins.BaseException,builtins.Exception
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# List of modules that can be imported at any level, not just the top level
|
||||
# one.
|
||||
allow-any-import-level=
|
||||
|
||||
# Allow explicit reexports by alias from a package __init__.
|
||||
allow-reexport-from-package=no
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma.
|
||||
deprecated-modules=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of external dependencies
|
||||
# to the given file (report RP0402 must not be disabled).
|
||||
ext-import-graph=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of all (i.e. internal and
|
||||
# external) dependencies to the given file (report RP0402 must not be
|
||||
# disabled).
|
||||
import-graph=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of internal dependencies
|
||||
# to the given file (report RP0402 must not be disabled).
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
# Couples of modules and preferred modules, separated by a comma.
|
||||
preferred-modules=
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# The type of string formatting that logging methods do. `old` means using %
|
||||
# formatting, `new` is for `{}` formatting.
|
||||
logging-format-style=old
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
|
||||
# UNDEFINED.
|
||||
confidence=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once). You can also use "--disable=all" to
|
||||
# disable everything first and then re-enable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=too-many-public-methods,
|
||||
invalid-name,
|
||||
no-else-break,
|
||||
no-else-continue,
|
||||
no-else-return,
|
||||
no-member,
|
||||
too-many-return-statements,
|
||||
too-many-locals,
|
||||
too-many-instance-attributes,
|
||||
too-few-public-methods,
|
||||
unused-argument,
|
||||
missing-function-docstring,
|
||||
attribute-defined-outside-init,
|
||||
ungrouped-imports,
|
||||
abstract-method
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=c-extension-no-member
|
||||
|
||||
|
||||
[METHOD_ARGS]
|
||||
|
||||
# List of qualified names (i.e., library.method) which require a timeout
|
||||
# parameter e.g. 'requests.api.get,requests.api.post'
|
||||
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
|
||||
max-line-length=120
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
# Regular expression of note tags to take in consideration.
|
||||
notes-rgx=
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=sys.exit,argparse.parse_error
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a score less than or equal to 10. You
|
||||
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
|
||||
# 'convention', and 'info' which contain the number of messages in each
|
||||
# category, as well as 'statement' which is the total number of statements
|
||||
# analyzed. This score is used by the global evaluation report (RP0004).
|
||||
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details.
|
||||
msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
#output-format=
|
||||
|
||||
# Tells whether to display a full report or only the messages.
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Comments are removed from the similarity computation
|
||||
ignore-comments=yes
|
||||
|
||||
# Docstrings are removed from the similarity computation
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Imports are removed from the similarity computation
|
||||
ignore-imports=yes
|
||||
|
||||
# Signatures are removed from the similarity computation
|
||||
ignore-signatures=yes
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes.
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. No available dictionaries : You need to install
|
||||
# both the python package and the system dependency for enchant to work..
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should be considered directives if they
|
||||
# appear at the beginning of a comment and should not be checked.
|
||||
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains the private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to the private dictionary (see the
|
||||
# --spelling-private-dict-file option) instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[STRING]
|
||||
|
||||
# This flag controls whether inconsistent-quotes generates a warning when the
|
||||
# character used as a quote delimiter is used inconsistently within a module.
|
||||
check-quote-consistency=no
|
||||
|
||||
# This flag controls whether the implicit-str-concat should generate a warning
|
||||
# on implicit string concatenation in sequences defined over several lines.
|
||||
check-str-concat-over-line-jumps=no
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of symbolic message names to ignore for Mixin members.
|
||||
ignored-checks-for-mixins=no-member,
|
||||
not-async-context-manager,
|
||||
not-context-manager,
|
||||
attribute-defined-outside-init
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
# Regex pattern to define which classes are considered mixins.
|
||||
mixin-class-rgx=.*[Mm]ixin
|
||||
|
||||
# List of decorators that change the signature of a decorated function.
|
||||
signature-mutators=
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid defining new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of names allowed to shadow builtins
|
||||
allowed-redefined-builtins=
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,
|
||||
_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||
# not be used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored.
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
67
webapi/.vscode/launch.json
vendored
Normal file
67
webapi/.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Exporter",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "exporter.py",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true,
|
||||
"args": [
|
||||
"test.ini"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Menu",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "menu.py",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true
|
||||
},
|
||||
{
|
||||
"name": "Add metadata",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "run_metadata.py",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true,
|
||||
"args": [
|
||||
"test.ini",
|
||||
"input.xlsx",
|
||||
"breAccessK3y"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Add cardslots",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "run_cardslot.py",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true,
|
||||
"args": [
|
||||
"test.ini",
|
||||
"input.xlsx",
|
||||
"breAccessK3y"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Existence check",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "run_cardslot.py",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true,
|
||||
"args": [
|
||||
"test.ini",
|
||||
"input.xlsx",
|
||||
"breAccessK3y",
|
||||
"-c"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
3
webapi/.vscode/settings.json
vendored
Normal file
3
webapi/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"python.analysis.typeCheckingMode": "off"
|
||||
}
|
14
webapi/configs/!AppData.ini
Normal file
14
webapi/configs/!AppData.ini
Normal file
|
@ -0,0 +1,14 @@
|
|||
[AppData]
|
||||
# Feel free to modify these parameters
|
||||
|
||||
SlovnikExecutor=Фунтов Евгений Евгеньевич
|
||||
|
||||
FilterTask=Статья;Микропонятие;Актуализация статьи из ЭВ БРЭ;Медиаконтент;Сюжет
|
||||
FilterDepartment=Редакция технологий и техники;Редакция энергетики, промышленности
|
||||
FilterSupervisor=
|
||||
FilterExecutor=
|
||||
FilterStatus=
|
||||
FilterObserver=
|
||||
|
||||
# Папка для раскрытых текстов
|
||||
DocxFolder=./docx
|
4
webapi/configs/!Menu.ini
Normal file
4
webapi/configs/!Menu.ini
Normal file
|
@ -0,0 +1,4 @@
|
|||
[Launch]
|
||||
Output=output.csv
|
||||
Config=portal.ini
|
||||
User=iborisov
|
20
webapi/configs/!Options.ini
Normal file
20
webapi/configs/!Options.ini
Normal file
|
@ -0,0 +1,20 @@
|
|||
[Options]
|
||||
# Warning! Do not change this parameters unless you are sure what they influence
|
||||
|
||||
# TestRun=false to enable modifications in portal
|
||||
TestRun=false
|
||||
|
||||
# Debug=false will disable exceptions
|
||||
Debug=false
|
||||
|
||||
# Duration in seconds for login to be considered valid
|
||||
LoginTimeout=900
|
||||
|
||||
# Duration in seconds to wait for data to be transmitted after pressing OK
|
||||
WaitData=10
|
||||
|
||||
# Duration in seconds to wait for change of status to transmit to server
|
||||
WaitStatus=20
|
||||
|
||||
# Ammount of times to try creating task after FAILED attempt
|
||||
CardslotRetries=2
|
5
webapi/configs/akostyuk.ini
Normal file
5
webapi/configs/akostyuk.ini
Normal file
|
@ -0,0 +1,5 @@
|
|||
[UserData]
|
||||
User=Костюк Алексей Владимирович
|
||||
Input=!Контент.xlsm
|
||||
UserLogin=a.kostyuk
|
||||
Password=gAAAAABidSZD_kif_iE_2rjp_zF8o-VsHudEW7UZtT4TfTWr1D77V1q1edtoNp_4wWxjUyATu0KhU2SFIrqnvxCuMdZsLxuApvxqJuUvenDT7xEm2vkoffE=
|
5
webapi/configs/amarzoeva.ini
Normal file
5
webapi/configs/amarzoeva.ini
Normal file
|
@ -0,0 +1,5 @@
|
|||
[UserData]
|
||||
User=Марзоева Анжелика Владиславовна
|
||||
Input=!Контент.xlsm
|
||||
UserLogin=a.marzoeva
|
||||
Password=gAAAAABjKxsXA3Cp9T4XKQ0zK0d3-3s1KEbtmEgK23mUIZSEgkJiD0LlTyoYz6S8xO4R_wBrPep4bNGcdzA88yAu-EQdaAnftjMSLHPyBDI-UzvWLexC_ro=
|
5
webapi/configs/anikitin.ini
Normal file
5
webapi/configs/anikitin.ini
Normal file
|
@ -0,0 +1,5 @@
|
|||
[UserData]
|
||||
User=Никитин Алексей Валерьевич
|
||||
Input=!Контент.xlsm
|
||||
UserLogin=a.nikitin
|
||||
Password=gAAAAABiMOMOoBGkME4YvhxuTYnipry3CKak2jfAMTHcxKP8Uhx_biFk2eBfPwIrcm--NTx4DoYogT_Bk70TAU52717R6A9ixHn4ES2qHHD5NnclOxv9Qdg=
|
5
webapi/configs/iborisov.ini
Normal file
5
webapi/configs/iborisov.ini
Normal file
|
@ -0,0 +1,5 @@
|
|||
[UserData]
|
||||
User=Борисов Иван Романович
|
||||
Input=!Контент.xlsm
|
||||
UserLogin=i.borisov
|
||||
Password=gAAAAABlI6fdFAeVM8UhkPYJaOP0pVhE6_GCF91EyY8kaFEyrgkr4SAm3lm4INIXPEQowAlvBr3M9uNtVwjqlGWIAiJPoKNeAg5sDOOP5lpZLYxy4HgdheY=
|
48
webapi/exporter.py
Normal file
48
webapi/exporter.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
'''Data exporter from BRE used mainly for automation'''
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import portal as bre
|
||||
|
||||
def _extract_password(token: str) -> str:
|
||||
return bre.decrypt(token, '3EJalkimf91-muM') # hard-coded password meant to obfuscate master password on call
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
if not os.path.exists("logs/"):
|
||||
os.makedirs("logs/")
|
||||
logging.basicConfig(
|
||||
filename=datetime.now().strftime('logs/%Y%m%d_%H%M export.log'), encoding='utf-8', level=logging.INFO,
|
||||
format='%(asctime)s %(levelname)s %(funcName)s: %(message)s',
|
||||
datefmt='%Y%m%d_%H:%M:%S'
|
||||
)
|
||||
logging.getLogger().addHandler(logging.StreamHandler())
|
||||
|
||||
parser = argparse.ArgumentParser(description='BRE automation parameters')
|
||||
parser.add_argument('filename')
|
||||
parser.add_argument('config', help='Configuration INI file')
|
||||
|
||||
args = parser.parse_args(sys.argv)
|
||||
|
||||
config = bre.read_config(args.config)
|
||||
config['AppData']['Password'] = _extract_password(config['AppData']['AccessToken'])
|
||||
|
||||
portal = bre.PortalAPI(config)
|
||||
if not portal.validate(config['AppData']['Password']):
|
||||
sys.exit(-1)
|
||||
if not portal.set_output_tasks(config['AppData']['Output']):
|
||||
sys.exit(-1)
|
||||
if config['AppData']['ScanContent'] and not portal.set_output_content(config['AppData']['OutputContent']):
|
||||
sys.exit(-1)
|
||||
|
||||
try:
|
||||
exit_code: int = portal.export_tasks()
|
||||
except SystemExit as e:
|
||||
exit_code = e
|
||||
except: # pylint: disable=bare-except
|
||||
logging.exception("message")
|
||||
raise
|
||||
del portal
|
||||
sys.exit(exit_code)
|
203
webapi/menu.py
Normal file
203
webapi/menu.py
Normal file
|
@ -0,0 +1,203 @@
|
|||
'''Main menu for BRE'''
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
|
||||
from datetime import datetime
|
||||
from enum import IntEnum, unique
|
||||
|
||||
import portal as bre
|
||||
|
||||
_CONFIG_FOLDER = 'configs/'
|
||||
_CONFIG_MAIN = '!Menu.ini'
|
||||
_CONFIG_APPDATA = '!AppData.ini'
|
||||
_CONFIG_OPTIONS = '!Options.ini'
|
||||
_SYSTEM_CONFIGS = [_CONFIG_MAIN, _CONFIG_APPDATA, _CONFIG_OPTIONS]
|
||||
|
||||
_TEXT_MAIN_MENU = '''Системное меню [{}]
|
||||
0. Выбрать пользователя
|
||||
1. Шифровать пароль пользователя
|
||||
2. Проверить наличие контента
|
||||
3. Завести карт-слоты
|
||||
4. Ввести метаданные
|
||||
5. Обновить метаданные
|
||||
6. Загрузить тексты
|
||||
|
||||
8. Выгрузить список задач
|
||||
9. Завершить работу
|
||||
'''
|
||||
|
||||
'''Pairs of flags to indicate if actions needs input / output setup'''
|
||||
_ACCTIONS_REQUIREMENTS= [
|
||||
(False, False), # 0
|
||||
(False, False), # 1
|
||||
(True, True), # 2
|
||||
(True, True), # 3
|
||||
(True, False), # 4
|
||||
(True, False), # 5
|
||||
(True, False), # 6
|
||||
(False, False), # 7
|
||||
(False, True), # 8
|
||||
(False, False), # 9
|
||||
]
|
||||
|
||||
@unique
|
||||
class MenuAction(IntEnum):
|
||||
'''Действие в главном меню'''
|
||||
choose_user = 0
|
||||
encrypt_password = 1
|
||||
check_existence = 2
|
||||
import_cards = 3
|
||||
import_meta = 4
|
||||
update_meta = 5
|
||||
load_content_text = 6
|
||||
|
||||
export_tasks = 8
|
||||
exit = 9
|
||||
|
||||
def needs_input(self) -> bool:
|
||||
'''Get text label for field'''
|
||||
return _ACCTIONS_REQUIREMENTS[self.value][0]
|
||||
|
||||
def needs_output(self) -> bool:
|
||||
'''Get text label for field'''
|
||||
return _ACCTIONS_REQUIREMENTS[self.value][1]
|
||||
|
||||
def _list_configs() -> list[str]:
|
||||
filenames = next(os.walk(_CONFIG_FOLDER), (None, None, []))[2]
|
||||
return [item.split('.')[0] for item in filenames if item not in _SYSTEM_CONFIGS]
|
||||
|
||||
class Menu:
|
||||
'''Portal menu UI'''
|
||||
def __init__(self):
|
||||
self._config = bre.read_config(_CONFIG_FOLDER + _CONFIG_MAIN)
|
||||
self._password = ''
|
||||
|
||||
def __del__(self):
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
'''Entry point'''
|
||||
password = input('Введите пароль: ')
|
||||
if not self._unlock(password):
|
||||
print('Пароль не прошел проверку')
|
||||
sys.exit()
|
||||
self._main_loop()
|
||||
|
||||
def _unlock(self, password: str) -> bool:
|
||||
if not bre.validate_password(password):
|
||||
return False
|
||||
self._password = password
|
||||
return True
|
||||
|
||||
def _main_loop(self):
|
||||
while True:
|
||||
os.system('cls')
|
||||
print(_TEXT_MAIN_MENU.format(self._config['Launch']['User']))
|
||||
action = MenuAction(int(input('Введите число: ')))
|
||||
self._process_action(action)
|
||||
|
||||
def _process_action(self, action: MenuAction):
|
||||
if action == MenuAction.exit:
|
||||
sys.exit(0)
|
||||
elif action == MenuAction.choose_user:
|
||||
self._change_user()
|
||||
return
|
||||
elif action == MenuAction.encrypt_password:
|
||||
self._encrypt()
|
||||
return
|
||||
|
||||
portal = self._setup_action(action)
|
||||
if portal is None:
|
||||
return
|
||||
|
||||
if action == MenuAction.import_meta:
|
||||
result = portal.import_meta() == 0
|
||||
elif action == MenuAction.update_meta:
|
||||
result = portal.update_meta() == 0
|
||||
elif action == MenuAction.export_tasks:
|
||||
result = portal.export_tasks() == 0
|
||||
elif action == MenuAction.check_existence:
|
||||
result = portal.check_existence() == 0
|
||||
elif action == MenuAction.import_cards:
|
||||
result = portal.import_cardslots() == 0
|
||||
elif action == MenuAction.load_content_text:
|
||||
result = portal.load_texts() == 0
|
||||
|
||||
del portal
|
||||
if result:
|
||||
print('Действие завершено успешно')
|
||||
else:
|
||||
print('Действие не выполнено')
|
||||
print('Введите любой символ для возврата в основное меню')
|
||||
input()
|
||||
|
||||
def _setup_action(self, action: MenuAction) -> bre.PortalAPI:
|
||||
user_config = self._prepare_config()
|
||||
portal = bre.PortalAPI(user_config)
|
||||
if not portal.validate(user_config['AppData']['Password']):
|
||||
print('Portal API invalid...')
|
||||
input()
|
||||
return None
|
||||
if action.needs_input() and not portal.set_input(user_config['UserData']['Input']):
|
||||
print('Input init failed...')
|
||||
input()
|
||||
return None
|
||||
if action.needs_output() and not portal.set_output_tasks(self._config['Launch']['Output']):
|
||||
print('Output init failed...')
|
||||
input()
|
||||
return None
|
||||
return portal
|
||||
|
||||
def _change_user(self):
|
||||
os.system('cls')
|
||||
print('Текущий пользователь: {}'.format(self._config['Launch']['User']))
|
||||
print('0. Вернуться в основное меню')
|
||||
user_configs = _list_configs()
|
||||
for idx, name in enumerate(user_configs):
|
||||
print('{}. {}'.format(idx + 1, name))
|
||||
|
||||
action = int(input('Введите число: '))
|
||||
if action == 0:
|
||||
return
|
||||
self._config['Launch']['User'] = user_configs[action - 1]
|
||||
|
||||
def _encrypt(self):
|
||||
os.system('cls')
|
||||
user = input('Введите логин пользователя: ')
|
||||
password = input('Введите пароль пользователя: ')
|
||||
cypher = bre.encrypt_user(user, password, self._password)
|
||||
print('Шифрограмма пароля: {}\nСкопируйте этот текст в конфиг-файл пользователя\n'.format(cypher))
|
||||
input('Ввдеите любой символ для возврата в основное меню\n')
|
||||
|
||||
def _prepare_config(self) -> bre.Config:
|
||||
file_name = self._config['Launch']['Config']
|
||||
output = open(file_name, 'w', encoding='UTF-8')
|
||||
inputs = [_CONFIG_APPDATA, _CONFIG_OPTIONS, self._config['Launch']['User'] + '.ini']
|
||||
for input_name in inputs:
|
||||
with open(_CONFIG_FOLDER + input_name, 'r', encoding='UTF-8') as infile:
|
||||
output.write(infile.read())
|
||||
output.write('\n')
|
||||
output.close()
|
||||
config = bre.read_config(file_name)
|
||||
os.remove(file_name)
|
||||
config['AppData']['Password'] = self._password
|
||||
return config
|
||||
|
||||
if __name__ == '__main__':
|
||||
if not os.path.exists("logs/"):
|
||||
os.makedirs("logs/")
|
||||
logging.basicConfig(
|
||||
filename=datetime.now().strftime('logs/%Y%m%d_%H%M menu.log'), encoding='utf-8', level=logging.INFO,
|
||||
format='%(asctime)s %(levelname)s %(funcName)s: %(message)s',
|
||||
datefmt='%Y%m%d_%H:%M:%S'
|
||||
)
|
||||
logging.getLogger().addHandler(logging.StreamHandler())
|
||||
|
||||
try:
|
||||
Menu().start()
|
||||
except SystemExit as e:
|
||||
sys.exit(e)
|
||||
except: # pylint: disable=bare-except
|
||||
logging.exception("message")
|
||||
raise
|
25
webapi/portal/__init__.py
Normal file
25
webapi/portal/__init__.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
''' -*- coding: utf-8 -*-
|
||||
requires pandas, openpyxl, selenium, rsa
|
||||
'''
|
||||
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
from .portal import PortalAPI
|
||||
|
||||
from .selenium_wrapper import WebBrowser
|
||||
from .electron_loading import GBDownloader
|
||||
from .uploader import GreatbookUploader
|
||||
from .info_models import FieldType, FilterType
|
||||
from .data_reader import DataReader, ContentIterator
|
||||
|
||||
from .bre_browser_options import (
|
||||
get_browser_options
|
||||
)
|
||||
|
||||
from .config import (
|
||||
Config, read_config
|
||||
)
|
||||
|
||||
from .crypto import (
|
||||
validate_password, encrypt, decrypt, encrypt_user, decrypt_user
|
||||
)
|
17
webapi/portal/bre_browser_options.py
Normal file
17
webapi/portal/bre_browser_options.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
'''BRE browser options'''
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
|
||||
def get_browser_options(show_window: bool) -> Options:
|
||||
'''Prepare browser options for BRE portal'''
|
||||
options = Options()
|
||||
if not show_window:
|
||||
options.add_argument('--headless')
|
||||
|
||||
options.add_argument("--start-maximized")
|
||||
options.add_experimental_option('excludeSwitches', ['enable-logging', 'enable-automation'])
|
||||
|
||||
prefs = {"credentials_enable_service": False, "profile.password_manager_enabled": False}
|
||||
options.add_experimental_option("prefs", prefs)
|
||||
|
||||
return options
|
12
webapi/portal/config.py
Normal file
12
webapi/portal/config.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
'''Configuration module'''
|
||||
from configparser import ConfigParser as Config
|
||||
|
||||
|
||||
def read_config(file_name: str) -> Config:
|
||||
'''Read config from a file'''
|
||||
config_file = open(file_name, 'r', encoding='utf8')
|
||||
|
||||
config = Config()
|
||||
config.read_file(config_file)
|
||||
config_file.close()
|
||||
return config
|
53
webapi/portal/crypto.py
Normal file
53
webapi/portal/crypto.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
'''Cyptographic module'''
|
||||
import base64
|
||||
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
|
||||
|
||||
def _get_crypto(password: str) -> Fernet:
|
||||
_CRYPTO16_SALT = b'\xac\xaa\xc7\xae\x99\xb1\x7fO\x01\xc6\x94<R$\xf7?'
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=_CRYPTO16_SALT,
|
||||
iterations=390000,
|
||||
)
|
||||
key = base64.urlsafe_b64encode(kdf.derive(password.encode('UTF-8')))
|
||||
return Fernet(key)
|
||||
|
||||
|
||||
def validate_password(password: str) -> bool:
|
||||
'''Validate password using test message'''
|
||||
_VERIFICATION_MESSAGE = 'Hello BRE'
|
||||
_VERIFICATION_CRYPTO = \
|
||||
'gAAAAABiMGnQn96MZkBpBE9qZRJfZ91-muMLzxMnydwcXt3ZaG6zjRt576E1waelYKxhGMazRSYwmslHpqxpgtIMSDbQSuE6_A=='
|
||||
try:
|
||||
keyphrase = decrypt(_VERIFICATION_CRYPTO, password)
|
||||
return keyphrase == _VERIFICATION_MESSAGE
|
||||
except InvalidToken:
|
||||
return False
|
||||
|
||||
|
||||
def encrypt(message: str, password: str) -> str:
|
||||
'''Encrypt message using key locked by password'''
|
||||
crypto = _get_crypto(password)
|
||||
return crypto.encrypt(message.encode('UTF-8')).decode('UTF-8')
|
||||
|
||||
|
||||
def encrypt_user(user_name: str, user_password: str, crypto_passwrod: str) -> str:
|
||||
'''Encrypt user password using key locked by crypto password'''
|
||||
return encrypt(user_name + user_password, crypto_passwrod)
|
||||
|
||||
|
||||
def decrypt(ciphertext: str, password: str):
|
||||
'''Decrypt ciphertext using key locked by password'''
|
||||
crypto = _get_crypto(password)
|
||||
return crypto.decrypt(ciphertext.encode('UTF-8')).decode('UTF-8')
|
||||
|
||||
|
||||
def decrypt_user(user_name: str, ciphertext: str, crypto_passwrod: str) -> str:
|
||||
'''Decrypt user password using key locked by crypto password'''
|
||||
text = decrypt(ciphertext, crypto_passwrod)
|
||||
return text[len(user_name):]
|
196
webapi/portal/data_reader.py
Normal file
196
webapi/portal/data_reader.py
Normal file
|
@ -0,0 +1,196 @@
|
|||
'''Reading data from Excel spreadsheets'''
|
||||
from enum import IntEnum, unique
|
||||
from datetime import datetime
|
||||
|
||||
import pandas
|
||||
|
||||
from .info_models import FieldType, InputMethod, text_to_method
|
||||
|
||||
_ContentFields = [
|
||||
FieldType.task_type,
|
||||
FieldType.status,
|
||||
FieldType.content_name,
|
||||
FieldType.skip,
|
||||
FieldType.change_score,
|
||||
FieldType.biblio_name,
|
||||
FieldType.definition,
|
||||
FieldType.is_immutable,
|
||||
FieldType.object_type,
|
||||
FieldType.markers,
|
||||
FieldType.tags,
|
||||
FieldType.author,
|
||||
FieldType.supervisor,
|
||||
FieldType.executor,
|
||||
FieldType.task_manager,
|
||||
FieldType.responsible,
|
||||
FieldType.department,
|
||||
FieldType.date_target,
|
||||
FieldType.source,
|
||||
FieldType.electron_bre,
|
||||
FieldType.main_page,
|
||||
FieldType.is_general,
|
||||
FieldType.actualize_period,
|
||||
FieldType.age_restriction,
|
||||
FieldType.priority,
|
||||
FieldType.article_type,
|
||||
FieldType.date_exchange,
|
||||
FieldType.date_ees1,
|
||||
FieldType.date_ex_tools,
|
||||
FieldType.date_ees2,
|
||||
FieldType.expert,
|
||||
FieldType.contract,
|
||||
FieldType.comment,
|
||||
FieldType.task_id,
|
||||
FieldType.content_name_db,
|
||||
FieldType.task_name
|
||||
]
|
||||
|
||||
|
||||
@unique
|
||||
class _ContentColumns(IntEnum):
|
||||
task_type = 0
|
||||
status = 1
|
||||
content_name = 2
|
||||
change_score = 4
|
||||
biblio_name = 5
|
||||
definition = 6
|
||||
is_immutable = 7
|
||||
object_type = 8
|
||||
markers = 9
|
||||
tags = 10
|
||||
author = 11
|
||||
supervisor = 12
|
||||
executor = 13
|
||||
task_manager = 14
|
||||
responsible = 15
|
||||
department = 16
|
||||
date_target = 17
|
||||
source = 18
|
||||
electron_bre = 19
|
||||
main_page = 20
|
||||
is_general = 21
|
||||
actualize_period = 22
|
||||
age_restriction = 23
|
||||
priority = 24
|
||||
article_type = 25
|
||||
date_exchange = 26
|
||||
date_ees1 = 27
|
||||
date_ex_tools = 28
|
||||
date_ees2 = 29
|
||||
expert = 30
|
||||
contract = 31
|
||||
comment = 32
|
||||
task_id = 33
|
||||
content_name_db = 34
|
||||
task_name = 35
|
||||
|
||||
def to_field(self) -> FieldType:
|
||||
'''Transform metadata column to FieldType'''
|
||||
return _ContentFields[self.value]
|
||||
|
||||
|
||||
def _get_task_name(content_name: str, is_immutable: bool) -> str:
|
||||
UNMUTABLE_TEMPLATE = 'Неизменные {} (библиография+корректура+транскрипция)'
|
||||
if not is_immutable:
|
||||
return content_name
|
||||
else:
|
||||
return UNMUTABLE_TEMPLATE.format(content_name)
|
||||
|
||||
|
||||
def _drop_from_nan(target: pandas.DataFrame) -> pandas.DataFrame:
|
||||
rows_with_nan = [index for index, row in target.iterrows() if pandas.isna(row.iloc[0])]
|
||||
if len(rows_with_nan) > 0:
|
||||
return target[:rows_with_nan[0]]
|
||||
else:
|
||||
return target
|
||||
|
||||
|
||||
class ContentIterator:
|
||||
'''Iterates over metadata sheet rows'''
|
||||
def __init__(self, data: pandas.DataFrame):
|
||||
self._data = data
|
||||
self._row = 0
|
||||
self._count = len(self._data.index)
|
||||
|
||||
def __del__(self):
|
||||
pass
|
||||
|
||||
def is_done(self) -> bool:
|
||||
'''Indicates end of iteration'''
|
||||
return self._row >= self._count
|
||||
|
||||
def next(self) -> bool:
|
||||
'''Iteration'''
|
||||
if self.is_done():
|
||||
return False
|
||||
self._row = self._row + 1
|
||||
return True
|
||||
|
||||
def read_row(self) -> dict:
|
||||
'''Data access'''
|
||||
data = {}
|
||||
for column in _ContentColumns:
|
||||
if not pandas.isna(self._data.iat[self._row, column]):
|
||||
field = column.to_field()
|
||||
value = self._data.iat[self._row, column]
|
||||
if field.input_method() == InputMethod.combo_dialog or \
|
||||
field.input_method() == InputMethod.combo_dialog_simple_list:
|
||||
data[field] = list(filter(None, [element.strip() for element in value.split(';')]))
|
||||
elif isinstance(value, str):
|
||||
data[field] = value.strip()
|
||||
elif isinstance(value, pandas.Timestamp):
|
||||
data[field] = value.strftime('%d.%m.%Y')
|
||||
elif isinstance(value, datetime):
|
||||
data[field] = value.strftime('%d.%m.%Y')
|
||||
else:
|
||||
data[field] = value
|
||||
|
||||
if FieldType.is_immutable in data:
|
||||
data[FieldType.is_immutable] = data[FieldType.is_immutable] == 'Да'
|
||||
if FieldType.electron_bre in data:
|
||||
data[FieldType.electron_bre] = data[FieldType.electron_bre] == 'Да'
|
||||
if FieldType.main_page in data:
|
||||
data[FieldType.main_page] = data[FieldType.main_page] == 'Да'
|
||||
if FieldType.is_general in data:
|
||||
data[FieldType.is_general] = data[FieldType.is_general] == 'Да'
|
||||
if FieldType.content_name_db not in data:
|
||||
data[FieldType.content_name_db] = data[FieldType.content_name]
|
||||
if FieldType.task_name not in data:
|
||||
is_immutable = FieldType.is_immutable in data and data[FieldType.is_immutable]
|
||||
data[FieldType.task_name] = _get_task_name(data[FieldType.content_name], is_immutable)
|
||||
if FieldType.department in data:
|
||||
data[FieldType.department] = 'Редакция ' + data[FieldType.department]
|
||||
data[FieldType.editorial] = data[FieldType.department]
|
||||
if FieldType.article_type in data:
|
||||
data[FieldType.article_type][0] = data[FieldType.article_type][0] + ' статья'
|
||||
return data
|
||||
|
||||
|
||||
class DataReader:
|
||||
'''BRE data reader for Excel'''
|
||||
_SHEET_CONTENT = 'Контент'
|
||||
_SHEET_ATTRIBUTES = 'Признаки'
|
||||
|
||||
def __init__(self):
|
||||
self._xls = None
|
||||
self._content = None
|
||||
self._attributes = None
|
||||
|
||||
def load(self, input_file: str) -> bool:
|
||||
'''Load file'''
|
||||
try:
|
||||
self._xls = pandas.ExcelFile(input_file)
|
||||
self._content = _drop_from_nan(pandas.read_excel(self._xls, DataReader._SHEET_CONTENT))
|
||||
self._attributes = _drop_from_nan(pandas.read_excel(self._xls, DataReader._SHEET_ATTRIBUTES))
|
||||
except (FileNotFoundError, ValueError):
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_content(self) -> ContentIterator:
|
||||
'''Return iterator for cards'''
|
||||
return ContentIterator(self._content)
|
||||
|
||||
def get_attributes_for(self, content_name) -> list:
|
||||
'''Return attributes list for specific content'''
|
||||
filtered = self._attributes.loc[self._attributes['Название контента'] == content_name]
|
||||
return [(row[1], text_to_method(row[3]), row[2]) for index, row in filtered.iterrows()]
|
75
webapi/portal/document.py
Normal file
75
webapi/portal/document.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
'''Processing docx files'''
|
||||
import os
|
||||
import subprocess
|
||||
import docx
|
||||
from docx.shared import Pt
|
||||
|
||||
|
||||
def _strip_stress_(text: str) -> str:
|
||||
''' Удаляет с текста ударения text: текст '''
|
||||
a = bytes(text, encoding='utf8').split(b"\xcc\x81")
|
||||
res = ""
|
||||
for i in a:
|
||||
res += str(i, encoding='utf-8')
|
||||
return res
|
||||
|
||||
|
||||
class DocxTextProcessor:
|
||||
'''Document processor class'''
|
||||
def __init__(self, file_name=None) -> None:
|
||||
'''
|
||||
file_name: название файла для считывания
|
||||
Конструктор автоматически создаёт Документ и парсит в текст, разделяя литературу и авторов
|
||||
'''
|
||||
self._docx = None
|
||||
self.text = ''
|
||||
self.authors = ''
|
||||
self.bibliography = ''
|
||||
self._name = ''
|
||||
if file_name is not None:
|
||||
self.process_document(file_name)
|
||||
|
||||
def process_document(self, file_name: str) -> bool:
|
||||
'''Document processing'''
|
||||
self._docx = docx.Document(file_name)
|
||||
self._name = file_name.split('/')[-1].split('.docx')[0]
|
||||
try:
|
||||
self.text = self.get_text__()
|
||||
except: # pylint: disable=bare-except
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_text__(self):
|
||||
''' Получает текст - самый стрёмный метод '''
|
||||
res = ''
|
||||
f = self._docx
|
||||
name = self._name
|
||||
is_bib_part = False
|
||||
for p in f.paragraphs:
|
||||
if p.runs[0].font.size < Pt(15):
|
||||
for run in p.runs:
|
||||
if name == _strip_stress_(run.text):
|
||||
res += (run.text[:1] + run.text[1:].lower())
|
||||
continue
|
||||
if "Лит" in run.text or is_bib_part:
|
||||
is_bib_part = True
|
||||
self.bibliography += run.text
|
||||
continue
|
||||
res += (run.text)
|
||||
res += '\n'
|
||||
test = _strip_stress_(res[:150]).lower()
|
||||
if test.find(name.lower().split('/')[-1]) > 2:
|
||||
self.authors = res[:test.find(name.lower()) - 1]
|
||||
res = res[test.find(name.lower()):]
|
||||
return res
|
||||
|
||||
def process_typograph(self):
|
||||
''' Обработка типографом '''
|
||||
with open('test', 'wb+') as file:
|
||||
file.write(bytes(self.text, encoding='utf-8'))
|
||||
cmd = 'node process_typograph.js'
|
||||
subprocess.run(cmd, capture_output=True, check=True) # mind about try
|
||||
with open('temp.log', 'r+', encoding='utf8') as file:
|
||||
self.text = file.read()
|
||||
os.remove("test")
|
||||
os.remove("temp.log")
|
171
webapi/portal/electron_loading.py
Normal file
171
webapi/portal/electron_loading.py
Normal file
|
@ -0,0 +1,171 @@
|
|||
'''
|
||||
автоматическая загруза с сайта посредством selenium
|
||||
с использованием браузера Chrome (требуется драйвер)
|
||||
'''
|
||||
import csv
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from .selenium_wrapper import WebBrowser
|
||||
|
||||
_LOGIN_PAGE = 'https://bigenc.ru/user/login'
|
||||
_ARTICLE_PAGE = 'https://bigenc.ru/user/content/articles'
|
||||
|
||||
|
||||
class GBDownloader:
|
||||
'''Downloader functionality'''
|
||||
def __init__(self, browser: WebBrowser, save_path: str, append: bool, suffix_seed: int):
|
||||
mode = 'a' if append else 'w'
|
||||
suffix = ('00' + str(suffix_seed))[-3:]
|
||||
self.browser = browser
|
||||
self.save_path = save_path
|
||||
|
||||
self._articles = open('{}\\!out_bd_{}.csv'.format(save_path, suffix), mode, newline='')
|
||||
self._references = open('{}\\!ref_bd_{}.csv'.format(save_path, suffix), mode, newline='')
|
||||
self._images = open('{}\\!img_bd_{}.csv'.format(save_path, suffix), mode, newline='')
|
||||
self._bibliography = open('{}\\!bib_bd_{}.csv'.format(save_path, suffix), mode, newline='')
|
||||
|
||||
self.writer_out = csv.writer(self._articles)
|
||||
self.writer_ref = csv.writer(self._references)
|
||||
self.writer_img = csv.writer(self._images)
|
||||
self.writer_bib = csv.writer(self._bibliography)
|
||||
if not append:
|
||||
self._create_headers()
|
||||
|
||||
def __del__(self):
|
||||
self._articles.close()
|
||||
self._references.close()
|
||||
self._images.close()
|
||||
self._bibliography.close()
|
||||
|
||||
def login(self, login: str, password: str) -> bool:
|
||||
'''Login to electron version of BRE'''
|
||||
self.browser.driver.get(_LOGIN_PAGE)
|
||||
if not self.browser.wait_presence('page-login-form'):
|
||||
return False
|
||||
|
||||
self.browser.driver.find_element(By.NAME, '__login').send_keys(login)
|
||||
self.browser.driver.find_element(By.NAME, '__password').send_keys(password)
|
||||
self.browser.driver.find_element(By.TAG_NAME, 'button').click()
|
||||
return self.browser.wait_presence('userMenu')
|
||||
|
||||
def scan(self, start: str, stop: str, max_items: int) -> str:
|
||||
'''Scan element from BRE'''
|
||||
next_id = start
|
||||
for i in range(max_items):
|
||||
print('{}-{}'.format(i, next_id))
|
||||
next_id = self._process_article(next_id, i)
|
||||
self._flush()
|
||||
if next_id == '':
|
||||
print("Stopped with no next")
|
||||
break
|
||||
if next_id == stop:
|
||||
print("Stopped at " + stop + " and worked " + str(i + 1) + "articles")
|
||||
break
|
||||
return next_id
|
||||
|
||||
def _flush(self):
|
||||
self._articles.flush()
|
||||
self._references.flush()
|
||||
self._images.flush()
|
||||
self._bibliography.flush()
|
||||
|
||||
def _create_headers(self):
|
||||
self.writer_out.writerow([
|
||||
'Indx', 'ArticleId', 'BlackWord', 'subWord', 'Rubrika', 'Year', 'Slovnik', 'Author(s)',
|
||||
'KolZnak', 'Status', 'Version', 'Litr', 'Soch', 'KolImg', 'KolCnt'
|
||||
])
|
||||
# ИД слова статьи + ИД статьи ссылаемого слова + другое слово + ссылка
|
||||
self.writer_ref.writerow(['WordId', 'RefWordId', 'RefWord', 'RefURL'])
|
||||
|
||||
# ИД слова + ИД ссылки на иллюстрацию
|
||||
self.writer_img.writerow(['WordId', 'ImgId'])
|
||||
|
||||
# ИД слова + ИД библиоссылки + тип библиографии + текст библиографии
|
||||
self.writer_bib.writerow(['WordId', 'BibId', 'BibType', 'BibText'])
|
||||
|
||||
def _process_article(self, article_id: str, index: int) -> str:
|
||||
self.browser.driver.get('{}/{}'.format(_ARTICLE_PAGE, article_id))
|
||||
if not self.browser.wait_presence('userMenu'):
|
||||
return ''
|
||||
|
||||
self._download_rtf()
|
||||
|
||||
image_count = self._extract_images(article_id)
|
||||
links_count = self._extract_links(article_id)
|
||||
literature, sochinenie = self._extract_biblio(article_id)
|
||||
self._extract_article_info(article_id, index, image_count, links_count, literature, sochinenie)
|
||||
ref = self.browser.by_xpath('//a[.="следующая >>"]').get_attribute('href')
|
||||
return ref.split('/')[-1]
|
||||
|
||||
def _download_rtf(self):
|
||||
self.browser.by_xpath('//a[@title="Rich Text Format"]').click()
|
||||
return True
|
||||
|
||||
def _extract_article_info(self, article_id: str, index: int, images: int, links: int, literature: int, soch: int):
|
||||
black_word = self.browser.by_xpath('//h1[@name="title"]').text
|
||||
sub_word = self.browser.by_xpath('//h2[@name="subtitle"]').text
|
||||
section_name = self.browser.by_xpath('//span[@name="section"]').text # Рубрика сайта
|
||||
markers = self.browser.by_xpath('//span[@name="markers"]').text # Словник
|
||||
authors = self.browser.by_xpath_m('//p[@class="Автор type-"]') # Автор // !!два значения или не одного
|
||||
author = '' if len(authors) == 0 else authors[0].text
|
||||
char_num = self.browser.by_xpath('//span[@class="art_num_chars"]').text # Количество знаков
|
||||
status = self.browser.by_xpath('//span[@class="status-text"]').text # Статус ("опубликовано" и др.)
|
||||
status = status.replace('Статус: ', '')
|
||||
version = self.browser.by_xpath('//td[@id="art-cat"]').text # Версия статьи ("Исходная" и др.)
|
||||
year = self.browser.by_xpath('//span[@class="year"]').text
|
||||
self.writer_out.writerow([
|
||||
index, article_id, black_word, sub_word, section_name, year, markers, author,
|
||||
char_num, status, version, literature, soch, images, links
|
||||
])
|
||||
|
||||
def _extract_images(self, article_id: str) -> int:
|
||||
# Гиперссылка на иллюстрации (src="")
|
||||
images = self.browser.by_xpath_m('//img')
|
||||
for img in images:
|
||||
source = img.get_attribute('src')
|
||||
self.writer_img.writerow([article_id, source])
|
||||
return len(images)
|
||||
|
||||
def _extract_links(self, article_id: str) -> int:
|
||||
links = self.browser.by_xpath_m('//a[@class="processed-link"]')
|
||||
for link in links:
|
||||
self.writer_ref.writerow([
|
||||
article_id, link.get_attribute('data-art'),
|
||||
link.get_attribute('data-word'),
|
||||
link.get_attribute('href')
|
||||
])
|
||||
return len(links)
|
||||
|
||||
def _extract_biblio(self, article_id: str):
|
||||
# Библиография // (по куску текста) + удалить '­' (\xad) заменить ' ' на пробел Возможно, подстрока 'type-' означает "Лит.:"
|
||||
biblios = self.browser.by_xpath_m('//div[@class="puretext type-biblio"]')
|
||||
litr = 0
|
||||
soch = 0
|
||||
for bib_item in biblios:
|
||||
biblio = bib_item.text.replace('\xad', '')
|
||||
itr = 0
|
||||
if biblio.find('Соч.:', 0, 5) != -1:
|
||||
soch = biblio.count(';') + 1
|
||||
for item in biblio.replace('Соч.: ', '').split('; '):
|
||||
itr = itr + 1
|
||||
self.writer_bib.writerow([article_id, itr, 'Soch', item])
|
||||
continue
|
||||
|
||||
if biblio.find('Лит.:', 0, 5) != -1:
|
||||
litr = biblio.count(';') + 1
|
||||
for item in biblio.replace('Лит.: ', '').split('; '):
|
||||
itr = itr + 1
|
||||
self.writer_bib.writerow([article_id, itr, 'Litr', item])
|
||||
continue
|
||||
|
||||
if biblio.find('Общие труды.', 0, 13) != -1:
|
||||
for item in biblio.replace('Общие труды. ', '').split('; '):
|
||||
itr = itr + 1
|
||||
self.writer_bib.writerow([article_id, itr, 'gnrl', item])
|
||||
continue
|
||||
|
||||
for item in biblio.split('; '):
|
||||
itr = itr + 1
|
||||
self.writer_bib.writerow([article_id, itr, 'inoe', item])
|
||||
return litr, soch
|
160
webapi/portal/info_models.py
Normal file
160
webapi/portal/info_models.py
Normal file
|
@ -0,0 +1,160 @@
|
|||
'''Data models for BRE portal'''
|
||||
from enum import Enum, IntEnum, unique
|
||||
|
||||
|
||||
@unique
|
||||
class InputMethod(Enum):
|
||||
'''Метод ввода данных на форме'''
|
||||
read_only = -1
|
||||
combo_box = 0
|
||||
text_input = 1
|
||||
text_area = 2
|
||||
date_picker = 3
|
||||
combo_dialog = 4
|
||||
combo_dialog_simple_list = 5 # no search required to select from list
|
||||
bool_toggle = 6
|
||||
|
||||
|
||||
def text_to_method(text: str) -> InputMethod:
|
||||
'''Input method from text transformation'''
|
||||
if text == 'Текстовое поле':
|
||||
return InputMethod.text_input
|
||||
if text == 'Тумблер':
|
||||
return InputMethod.bool_toggle
|
||||
if text == 'Выпад. Cписок':
|
||||
return InputMethod.combo_box
|
||||
if text == 'Диалог (с выбором)':
|
||||
return InputMethod.combo_dialog
|
||||
return InputMethod.read_only
|
||||
|
||||
|
||||
_FIELD_DATA = [
|
||||
('Тип задачи', InputMethod.combo_box),
|
||||
('Статус', InputMethod.read_only),
|
||||
('Название задачи', InputMethod.text_input),
|
||||
('Название контента', InputMethod.text_input),
|
||||
('Характер изменения текста', InputMethod.combo_dialog_simple_list),
|
||||
('Название контента для библиографической записи', InputMethod.text_input),
|
||||
('Краткая дефиниция', InputMethod.text_input),
|
||||
('Неизменный', InputMethod.read_only),
|
||||
('Тип объекта', InputMethod.combo_dialog),
|
||||
('Тематический словник', InputMethod.combo_dialog),
|
||||
('Метки', InputMethod.combo_dialog),
|
||||
('Курирующий редактор', InputMethod.combo_box),
|
||||
('Исполнитель', InputMethod.combo_box),
|
||||
('Ответственный за задачу', InputMethod.combo_box),
|
||||
('Ответственный', InputMethod.combo_box),
|
||||
('Ответственное подразделение', InputMethod.combo_box),
|
||||
('Ответственная редакция', InputMethod.combo_box),
|
||||
('Срок исполнения', InputMethod.date_picker),
|
||||
('Источник термина', InputMethod.combo_dialog),
|
||||
('Текст из ЭВ БРЭ', InputMethod.bool_toggle),
|
||||
('Отображать на главной', InputMethod.bool_toggle),
|
||||
('Обобщающая статья', InputMethod.bool_toggle),
|
||||
('Период актуализации', InputMethod.combo_dialog_simple_list),
|
||||
('Возрастное ограничение', InputMethod.combo_dialog_simple_list),
|
||||
('Приоритет', InputMethod.combo_box),
|
||||
('Тип статьи', InputMethod.combo_dialog_simple_list),
|
||||
('Биржа', InputMethod.date_picker),
|
||||
('Срок сдачи в ЕЭС 1', InputMethod.date_picker),
|
||||
('Биржа инструментов', InputMethod.date_picker),
|
||||
('Срок сдачи в ЕЭС 2', InputMethod.date_picker),
|
||||
('Эксперт', InputMethod.combo_box),
|
||||
('Номер договора', InputMethod.combo_box),
|
||||
('Комментарий', InputMethod.text_area),
|
||||
('Автор (на портале)', InputMethod.combo_box),
|
||||
|
||||
('ID задачи', InputMethod.read_only),
|
||||
('Название контента в базе', InputMethod.read_only),
|
||||
]
|
||||
|
||||
|
||||
@unique
|
||||
class FieldType(IntEnum):
|
||||
'''Поля ввода'''
|
||||
task_type = 0
|
||||
status = 1
|
||||
task_name = 2
|
||||
content_name = 3
|
||||
change_score = 4
|
||||
biblio_name = 5
|
||||
definition = 6
|
||||
is_immutable = 7
|
||||
object_type = 8
|
||||
markers = 9
|
||||
tags = 10
|
||||
supervisor = 11
|
||||
executor = 12
|
||||
task_manager = 13
|
||||
responsible = 14
|
||||
department = 15
|
||||
editorial = 16
|
||||
date_target = 17
|
||||
source = 18
|
||||
electron_bre = 19
|
||||
main_page = 20
|
||||
is_general = 21
|
||||
actualize_period = 22
|
||||
age_restriction = 23
|
||||
priority = 24
|
||||
article_type = 25
|
||||
date_exchange = 26
|
||||
date_ees1 = 27
|
||||
date_ex_tools = 28
|
||||
date_ees2 = 29
|
||||
expert = 30
|
||||
contract = 31
|
||||
comment = 32
|
||||
author = 33
|
||||
|
||||
task_id = 34
|
||||
content_name_db = 35
|
||||
|
||||
skip = -1
|
||||
|
||||
def to_label(self) -> str:
|
||||
'''Get text label for input field'''
|
||||
return _FIELD_DATA[self.value][0]
|
||||
|
||||
def input_method(self) -> InputMethod:
|
||||
'''Get input method for field'''
|
||||
return _FIELD_DATA[self.value][1]
|
||||
|
||||
|
||||
_FILTER_DATA = [
|
||||
('FilterTask', 'Тип задачи'),
|
||||
('FilterDepartment', 'Ответственное подразделение'),
|
||||
('FilterStatus', 'Статус задачи'),
|
||||
('FilterResponsible', 'Ответственный за задачу'),
|
||||
('FilterSupervisor', 'Курирующий редактор'),
|
||||
('FilterExecutor', 'Исполнитель'),
|
||||
('FilterObserver', 'Наблюдатель'),
|
||||
('FilterDateCreatedBegin', 'Дата создания'),
|
||||
('FilterDateCreatedEnd', 'Дата создания'),
|
||||
('FilterDateTargetBegin', 'Срок исполнения'),
|
||||
('FilterDateTargetEnd', 'Срок исполнения')
|
||||
]
|
||||
|
||||
|
||||
@unique
|
||||
class FilterType(IntEnum):
|
||||
'''Параметры фильтрации'''
|
||||
task_type = 0
|
||||
department = 1
|
||||
status = 2
|
||||
responsible = 3
|
||||
supervisor = 4
|
||||
executor = 5
|
||||
observer = 6
|
||||
created_begin = 7
|
||||
created_end = 8
|
||||
target_begin = 9
|
||||
target_end = 10
|
||||
|
||||
def to_config(self) -> str:
|
||||
'''Get input method for field'''
|
||||
return _FILTER_DATA[self.value][0]
|
||||
|
||||
def to_label(self) -> str:
|
||||
'''Get input method for field'''
|
||||
return _FILTER_DATA[self.value][1]
|
347
webapi/portal/portal.py
Normal file
347
webapi/portal/portal.py
Normal file
|
@ -0,0 +1,347 @@
|
|||
'''BRE Portal API'''
|
||||
import csv
|
||||
import time
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
from colorama import init as color_init
|
||||
from colorama import Fore, Style
|
||||
|
||||
from .selenium_wrapper import WebBrowser
|
||||
from .config import Config
|
||||
from .info_models import FieldType, FilterType
|
||||
from .uploader import GreatbookUploader
|
||||
from .data_reader import DataReader
|
||||
from .bre_browser_options import get_browser_options
|
||||
from .crypto import validate_password
|
||||
|
||||
from .document import DocxTextProcessor
|
||||
|
||||
warnings.filterwarnings('ignore', category=UserWarning, module='openpyxl')
|
||||
|
||||
|
||||
def _log_start():
|
||||
logging.info(
|
||||
'Start time is ' + Style.BRIGHT + Fore.GREEN + '%s' + Style.RESET_ALL,
|
||||
time.strftime('%H:%M:%S')
|
||||
)
|
||||
|
||||
|
||||
def _log_end():
|
||||
logging.info(
|
||||
'Done ... end time is ' + Style.BRIGHT + Fore.GREEN + '%s' + Style.RESET_ALL,
|
||||
time.strftime('%H:%M:%S')
|
||||
)
|
||||
|
||||
|
||||
def _chunks(lst, n: int):
|
||||
"""Yield successive n-sized chunks from lst."""
|
||||
for i in range(0, len(lst), n):
|
||||
yield lst[i:i + n]
|
||||
|
||||
|
||||
def _format_for(status: str):
|
||||
if status in ['OK', 'NOT EXISTS']:
|
||||
return Style.BRIGHT + Fore.GREEN
|
||||
if status in ['EXCEPTION', 'FAIL', 'EXISTS', 'NO TASK']:
|
||||
return Style.BRIGHT + Fore.RED
|
||||
if status in ['ONLY TASK']:
|
||||
return Style.BRIGHT + Fore.BLUE
|
||||
return ''
|
||||
|
||||
|
||||
class PortalAPI:
|
||||
'''Main entrpoint to Portal'''
|
||||
def __init__(self, config: Config):
|
||||
color_init(autoreset=True)
|
||||
chrome = WebBrowser()
|
||||
chrome_options = get_browser_options(show_window=True)
|
||||
chrome.start_chrome(chrome_options)
|
||||
|
||||
self.config = config
|
||||
self._browser = chrome
|
||||
self._debug = config['Options'].getboolean('Debug')
|
||||
self._loader = GreatbookUploader(chrome, config)
|
||||
|
||||
self._reader = None
|
||||
self._output_tasks = None
|
||||
self._writer_tasks = None
|
||||
self._output_content = None
|
||||
self._writer_content = None
|
||||
|
||||
self._document_processor = None
|
||||
|
||||
def __del__(self):
|
||||
pass
|
||||
|
||||
def validate(self, password: str) -> bool:
|
||||
'''Validate API status'''
|
||||
if not validate_password(password):
|
||||
return False
|
||||
if not self._loader.login():
|
||||
return False
|
||||
return True
|
||||
|
||||
def set_input(self, input_file: str) -> bool:
|
||||
'''Initialize input file'''
|
||||
self._reader = DataReader()
|
||||
if not self._reader.load(input_file):
|
||||
logging.error('Failed to access %s', input_file)
|
||||
return False
|
||||
return True
|
||||
|
||||
def set_output_tasks(self, output_file: str) -> bool:
|
||||
'''Initialize output file'''
|
||||
self._output_tasks = open(output_file, 'w', newline='', encoding='utf-8')
|
||||
self._writer_tasks = csv.writer(self._output_tasks)
|
||||
return True
|
||||
|
||||
def set_output_content(self, output_file: str) -> bool:
|
||||
'''Initialize output file'''
|
||||
self._output_content = open(output_file, 'w', newline='', encoding='utf-8')
|
||||
self._writer_content = csv.writer(self._output_content)
|
||||
return True
|
||||
|
||||
def check_existence(self) -> int:
|
||||
'''Check existence of card-slots from Excel input'''
|
||||
_log_start()
|
||||
self._writer_tasks.writerow(['Слово', 'Статус', 'Текст', 'Идентификатор'])
|
||||
content_it = self._reader.get_content()
|
||||
while not content_it.is_done():
|
||||
data = content_it.read_row()
|
||||
(content, status, has_text, task_id) = self._process_existence(data)
|
||||
self._write_task(content, status, has_text, task_id)
|
||||
content_it.next()
|
||||
self._output_tasks.close()
|
||||
_log_end()
|
||||
return 0
|
||||
|
||||
def import_cardslots(self) -> int:
|
||||
'''Check existence of card-slots from Excel input'''
|
||||
_log_start()
|
||||
self._writer_tasks.writerow(['Слово', 'Статус', 'Текст', 'Идентификатор'])
|
||||
content_it = self._reader.get_content()
|
||||
while not content_it.is_done():
|
||||
data = content_it.read_row()
|
||||
attempts = 0
|
||||
while attempts <= self.config['Options'].getint('CardslotRetries') + 1:
|
||||
if attempts > 0:
|
||||
logging.info('Retrying after failed attempt # %d...', attempts)
|
||||
(content, status, has_text, task_id) = self._process_cardslots(data)
|
||||
self._write_task(content, status, has_text, task_id)
|
||||
if status != 'FAILED':
|
||||
break
|
||||
attempts += 1
|
||||
content_it.next()
|
||||
self._output_tasks.close()
|
||||
_log_end()
|
||||
return 0
|
||||
|
||||
def import_meta(self) -> int:
|
||||
'''Import content metadata'''
|
||||
_log_start()
|
||||
content_it = self._reader.get_content()
|
||||
while not content_it.is_done():
|
||||
data = content_it.read_row()
|
||||
content = data[FieldType.content_name_db]
|
||||
attributes = self._reader.get_attributes_for(content)
|
||||
status = self._process_metadata(data, attributes)
|
||||
logging.info('%s ... ' + _format_for(status) + '[%s]' + Style.RESET_ALL, content, status)
|
||||
content_it.next()
|
||||
_log_end()
|
||||
return 0
|
||||
|
||||
def update_meta(self) -> int:
|
||||
'''Update content metadata'''
|
||||
_log_start()
|
||||
content_it = self._reader.get_content()
|
||||
while not content_it.is_done():
|
||||
data = content_it.read_row()
|
||||
content = data[FieldType.content_name_db]
|
||||
attributes = self._reader.get_attributes_for(content)
|
||||
status = self._update_metadata(data, attributes)
|
||||
logging.info('%s ... ' + _format_for(status) + '[%s]' + Style.RESET_ALL, content, status)
|
||||
content_it.next()
|
||||
_log_end()
|
||||
return 0
|
||||
|
||||
def load_texts(self) -> int:
|
||||
'''Load content text'''
|
||||
_log_start()
|
||||
self._document_processor = DocxTextProcessor()
|
||||
content_it = self._reader.get_content()
|
||||
while not content_it.is_done():
|
||||
data = content_it.read_row()
|
||||
content = data[FieldType.content_name]
|
||||
filename = self.config['AppData']['DocxFolder'] + '/' + content.upper() + '.docx'
|
||||
if not self._document_processor.process_document(filename):
|
||||
status = 'FAIL'
|
||||
else:
|
||||
authors = self._document_processor.authors
|
||||
text = self._document_processor.text
|
||||
bibliography = self._document_processor.bibliography
|
||||
status = self._load_content_text(content, authors, text, bibliography)
|
||||
content_it.next()
|
||||
logging.info('%s ... ' + _format_for(status) + '[%s]' + Style.RESET_ALL, content, status)
|
||||
_log_end()
|
||||
return 0
|
||||
|
||||
def export_tasks(self) -> int:
|
||||
'''Update content metadata'''
|
||||
_log_start()
|
||||
scanTasks = self.config['AppData']['ScanTasks'] == 'true'
|
||||
scanContent = self.config['AppData']['ScanContent'] == 'true'
|
||||
excluded = self.config['AppData']['ExcludeID'].split(';')
|
||||
if '' in excluded:
|
||||
excluded.remove('')
|
||||
included = self.config['AppData']['IncludeID'].split(';')
|
||||
if '' in included:
|
||||
included.remove('')
|
||||
|
||||
logging.info('Excluded tasks: %s', len(excluded))
|
||||
logging.info('Included tasks: %s', len(included))
|
||||
logging.info('Scan tasks: %s', scanTasks)
|
||||
logging.info('Scan content: %s', scanContent)
|
||||
|
||||
if scanTasks:
|
||||
logging.info('Loading tasks data...')
|
||||
|
||||
filters = []
|
||||
for filter_id in FilterType:
|
||||
filters.append([s.strip() for s in self.config['AppData'][filter_id.to_config()].split(';')])
|
||||
|
||||
data = self._loader.get_tasks_data(filters)
|
||||
logging.info('Loaded %s tasks', len(data))
|
||||
self._writer_tasks.writerows(data)
|
||||
self._output_tasks.close()
|
||||
|
||||
if not scanContent:
|
||||
_log_end()
|
||||
return 0
|
||||
|
||||
tasks_with_content = [
|
||||
item[6] for item in
|
||||
filter(
|
||||
lambda x: x[0] in ['МИКРОПОНЯТИЕ', 'СТАТЬЯ', 'АКТУАЛИЗАЦИЯ СТАТЬИ ИЗ ЭВ БРЭ', 'СЮЖЕТ']
|
||||
and x[1] not in ['Отменена']
|
||||
and x[2] not in ['Нет Контента', 'Нет Медиа']
|
||||
and x[6] not in excluded
|
||||
and x[6] not in included,
|
||||
data
|
||||
)
|
||||
]
|
||||
included = included + tasks_with_content
|
||||
|
||||
chunks = list(_chunks(included, 50))
|
||||
logging.info('Scanning %s content in %s bundles', len(included), len(chunks))
|
||||
for index, tasks_bundle in enumerate(chunks): # Split in 50 bunches to ensure login is valid
|
||||
logging.info('%s: Processing bundle %s / %s', time.strftime('%H:%M:%S'), index + 1, len(chunks))
|
||||
try:
|
||||
content = self._loader.get_tasks_content(tasks_bundle)
|
||||
self._writer_content.writerows(content)
|
||||
self._output_content.flush()
|
||||
except: # pylint: disable=bare-except
|
||||
logging.info('EXCEPTION during processing! Skipping bundle %s', index + 1)
|
||||
logging.info('\n'.join(tasks_bundle))
|
||||
self._output_content.close()
|
||||
_log_end()
|
||||
return 0
|
||||
|
||||
def _write_task(self, content: str, status: str, has_text: bool, task_id: str):
|
||||
has_text_str = 'Да' if has_text else 'Нет'
|
||||
# pylint: disable=logging-not-lazy
|
||||
logging.info(
|
||||
'%s ... ' + _format_for(status) + '[%s]' + Style.RESET_ALL + ' ... [%s] ... %s',
|
||||
content, status, has_text_str, task_id
|
||||
)
|
||||
self._writer_tasks.writerow([content, status, has_text_str, task_id])
|
||||
self._output_tasks.flush()
|
||||
|
||||
def _process_existence(self, data: dict):
|
||||
content = data[FieldType.content_name_db]
|
||||
has_text = False
|
||||
try:
|
||||
task_id = self._loader.find_task_id(data[FieldType.task_name])
|
||||
if self._loader.content_exists(content):
|
||||
status = 'EXISTS'
|
||||
has_text = self._loader.content_has_text(content)
|
||||
else:
|
||||
status = 'ONLY TASK' if task_id != '' else 'NOT EXISTS'
|
||||
except: # pylint: disable=bare-except
|
||||
if not self._debug:
|
||||
logging.exception('Got exception...')
|
||||
task_id = ''
|
||||
status = 'EXCEPTION'
|
||||
else:
|
||||
raise
|
||||
if content == 'Нет Контента':
|
||||
content = data[FieldType.task_name]
|
||||
return (content, status, has_text, task_id)
|
||||
|
||||
def _process_cardslots(self, data: dict):
|
||||
content = data[FieldType.content_name_db]
|
||||
has_text = False
|
||||
try:
|
||||
task_id = self._loader.find_task_id(data[FieldType.task_name])
|
||||
if self._loader.content_exists(content):
|
||||
status = 'EXISTS'
|
||||
has_text = self._loader.content_has_text(content)
|
||||
elif task_id != '':
|
||||
status = 'ONLY TASK'
|
||||
else:
|
||||
task_id = self._loader.create_task(data)
|
||||
status = 'OK' if task_id != '' else 'FAILED'
|
||||
except: # pylint: disable=bare-except
|
||||
if not self._debug:
|
||||
logging.exception('Got exception...')
|
||||
status = 'EXCEPTION'
|
||||
task_id = ''
|
||||
else:
|
||||
raise
|
||||
return (content, status, has_text, task_id)
|
||||
|
||||
def _process_metadata(self, data: dict, attributes: list) -> str:
|
||||
try:
|
||||
task_id = self._loader.find_task_id(data[FieldType.task_name])
|
||||
if task_id == '':
|
||||
return 'NO TASK'
|
||||
if self._loader.fill_metadata(task_id, data, attributes):
|
||||
return 'OK'
|
||||
else:
|
||||
return 'FAIL'
|
||||
except: # pylint: disable=bare-except
|
||||
if not self._debug:
|
||||
logging.exception('Got exception...')
|
||||
return 'EXCEPTION'
|
||||
else:
|
||||
raise
|
||||
|
||||
def _update_metadata(self, data: dict, attributes: list) -> str:
|
||||
try:
|
||||
task_id = self._loader.find_task_id(data[FieldType.task_name])
|
||||
if task_id == '':
|
||||
return 'NO TASK'
|
||||
if self._loader.update_metadata(task_id, data, attributes):
|
||||
return 'OK'
|
||||
else:
|
||||
return 'FAIL'
|
||||
except: # pylint: disable=bare-except
|
||||
if not self._debug:
|
||||
logging.exception('Got exception...')
|
||||
return 'EXCEPTION'
|
||||
else:
|
||||
raise
|
||||
|
||||
def _load_content_text(self, content: str, authors, text: str, bibliography: str) -> str:
|
||||
try:
|
||||
|
||||
if self._loader.load_content(content, authors, text, bibliography):
|
||||
return 'OK'
|
||||
else:
|
||||
return 'FAIL'
|
||||
except: # pylint: disable=bare-except
|
||||
if not self._debug:
|
||||
logging.exception('Got exception...')
|
||||
return 'EXCEPTION'
|
||||
else:
|
||||
raise
|
19
webapi/portal/process_typograph.js
Normal file
19
webapi/portal/process_typograph.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
const Typograph = require('./typograf/build/typograf.js')
|
||||
const tp = new Typograph({locale : ['ru' , 'en-US']})
|
||||
tp.enableRule('common/nbsp/replaceNbsp');
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
var file = '';
|
||||
|
||||
file = fs.readFile('test', 'utf-8', function(err,data){
|
||||
if (!err) {
|
||||
fs.writeFile('temp.log', tp.execute(data), (err) => {
|
||||
if(err){
|
||||
console.log(err);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.log(err);
|
||||
}})
|
||||
|
97
webapi/portal/selenium_wrapper.py
Normal file
97
webapi/portal/selenium_wrapper.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
'''Selenium wrapper'''
|
||||
import time
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.common.exceptions import TimeoutException, WebDriverException
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.remote.webelement import WebElement
|
||||
|
||||
_LOAD_TIMEOUT = 10 # seconds
|
||||
_AJAX_TIMEOUT = 10 # seconds
|
||||
|
||||
|
||||
def _ajax_complete(driver) -> bool:
|
||||
try:
|
||||
return driver.execute_script("return jQuery.active") == 0
|
||||
except WebDriverException:
|
||||
return True
|
||||
|
||||
|
||||
class WebBrowser:
|
||||
'''Wrapper for selenium webdriver'''
|
||||
def __init__(self):
|
||||
self.driver = None
|
||||
|
||||
def __del__(self):
|
||||
self.stop_chrome()
|
||||
|
||||
def start_chrome(self, chrome_options):
|
||||
'''Start Google Chrome driver'''
|
||||
self.driver = webdriver.Chrome(options=chrome_options)
|
||||
|
||||
def stop_chrome(self):
|
||||
'''Stop driver'''
|
||||
if self.driver:
|
||||
self.driver.quit()
|
||||
|
||||
def by_xpath(self, xpath: str) -> WebElement:
|
||||
'''Return web element found by xpath'''
|
||||
return self.driver.find_element(By.XPATH, xpath)
|
||||
|
||||
def by_xpath_m(self, xpath: str) -> list[WebElement]:
|
||||
'''Return multimple web elements found by xpath'''
|
||||
return self.driver.find_elements(By.XPATH, xpath)
|
||||
|
||||
def wait_presence(self, elem_id: str, by_what: str = By.ID) -> bool:
|
||||
'''Wait for presence of specific element'''
|
||||
try:
|
||||
element_present = EC.presence_of_element_located((by_what, elem_id))
|
||||
WebDriverWait(self.driver, _LOAD_TIMEOUT).until(element_present)
|
||||
return True
|
||||
except TimeoutException:
|
||||
return False
|
||||
|
||||
def wait_visibility(self, elem_id: str, by_what: str = By.ID) -> bool:
|
||||
'''Wait for presence of specific element'''
|
||||
try:
|
||||
element_visible = EC.visibility_of_element_located((by_what, elem_id))
|
||||
WebDriverWait(self.driver, _LOAD_TIMEOUT).until(element_visible)
|
||||
return True
|
||||
except TimeoutException:
|
||||
return False
|
||||
|
||||
def wait_clickable(self, elem_id: str, by_what: str = By.ID) -> bool:
|
||||
'''Wait for presence of specific element'''
|
||||
try:
|
||||
element_clickable = EC.element_to_be_clickable((by_what, elem_id))
|
||||
WebDriverWait(self.driver, _LOAD_TIMEOUT).until(element_clickable)
|
||||
return True
|
||||
except TimeoutException:
|
||||
return False
|
||||
|
||||
def wait_ajax(self):
|
||||
'''Wait for ajax to finish'''
|
||||
wait_token = WebDriverWait(self.driver, _AJAX_TIMEOUT)
|
||||
wait_token.until(_ajax_complete)
|
||||
wait_token.until(lambda driver: driver.execute_script('return document.readyState') == 'complete')
|
||||
|
||||
# WebDriverWait wait = new WebDriverWait(getWebDriver(), 10);
|
||||
# WebElement element = wait.until(ExpectedConditions.visibilityOf(element));
|
||||
|
||||
# jQuery doesnt work for some reason so we just wait implicitly
|
||||
time.sleep(1)
|
||||
|
||||
def send_esc(self):
|
||||
'''Send Escape key to browser'''
|
||||
webdriver.ActionChains(self.driver).send_keys(Keys.ESCAPE).perform()
|
||||
|
||||
def send_enter(self):
|
||||
'''Send Enter key to browser'''
|
||||
webdriver.ActionChains(self.driver).send_keys(Keys.ENTER).perform()
|
||||
|
||||
def send_tab(self):
|
||||
'''Send Tab key to browser'''
|
||||
webdriver.ActionChains(self.driver).send_keys(Keys.TAB).perform()
|
946
webapi/portal/uploader.py
Normal file
946
webapi/portal/uploader.py
Normal file
|
@ -0,0 +1,946 @@
|
|||
"""Uploading data to rk.greatbook.ru"""
|
||||
import time
|
||||
import logging
|
||||
import random
|
||||
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.common.exceptions import (
|
||||
NoSuchElementException,
|
||||
InvalidElementStateException,
|
||||
ElementNotInteractableException
|
||||
)
|
||||
from selenium.webdriver.remote.webelement import WebElement
|
||||
|
||||
from .selenium_wrapper import WebBrowser
|
||||
from .info_models import InputMethod, FieldType, FilterType
|
||||
from .config import Config
|
||||
from .crypto import decrypt_user
|
||||
|
||||
|
||||
def _replace_comma_separators(value: str) -> str:
|
||||
start = 0
|
||||
while True:
|
||||
pos = value.find(", ", start)
|
||||
if pos == -1 or pos + 2 >= len(value):
|
||||
return value
|
||||
if value[pos + 2].isupper():
|
||||
value = ';'.join([value[:pos], value[pos + 2:]])
|
||||
start = pos + 1
|
||||
|
||||
|
||||
class GreatbookUploader:
|
||||
"""Entry point into portal data input automation"""
|
||||
def __init__(self, browser: WebBrowser, config: Config):
|
||||
self._impl = _UploaderImpl(browser, config)
|
||||
self._login_timeout = config['Options'].getint('LoginTimeout')
|
||||
self._time_login = 0
|
||||
|
||||
def __del__(self):
|
||||
pass
|
||||
|
||||
def login(self) -> bool:
|
||||
'''Login into Portal'''
|
||||
if not self._impl.login():
|
||||
return False
|
||||
self._time_login = time.time()
|
||||
return True
|
||||
|
||||
def logout(self):
|
||||
'''Logout from current user'''
|
||||
self._impl.logout()
|
||||
|
||||
def content_exists(self, content: str) -> bool:
|
||||
'''Check if content name is taken'''
|
||||
self._ensure_login()
|
||||
return self._impl.content_exists(content)
|
||||
|
||||
def content_has_text(self, content: str) -> bool:
|
||||
'''Warning! Requires to run content_exists first'''
|
||||
return self._impl.content_has_text(content)
|
||||
|
||||
def create_task(self, data: dict) -> bool:
|
||||
'''Create task in system. Returns empty string on error'''
|
||||
self._ensure_login()
|
||||
return self._impl.create_task(data)
|
||||
|
||||
def find_task_id(self, task_name: str) -> str:
|
||||
'''Find task ID or empty string if not found'''
|
||||
self._ensure_login()
|
||||
return self._impl.find_task_id(task_name)
|
||||
|
||||
def set_task_status(self, task_id: str, new_status: str, status_label: str) -> bool:
|
||||
'''Change task status. Can fail (return false) if status is unavailable'''
|
||||
self._ensure_login()
|
||||
return self._impl.set_task_status(task_id, new_status, status_label)
|
||||
|
||||
def fill_metadata(self, task_id: str, data: dict, attributes: list) -> bool:
|
||||
'''Create metadata. Also changes status for new content'''
|
||||
self._ensure_login()
|
||||
return self._impl.fill_metadata(task_id, data, attributes)
|
||||
|
||||
def update_metadata(self, task_id: str, data: dict, attributes: list) -> bool:
|
||||
'''Update metadata'''
|
||||
self._ensure_login()
|
||||
return self._impl.update_metadata(task_id, data, attributes)
|
||||
|
||||
def get_tasks_data(self, filters: list[list]) -> list:
|
||||
'''Export data for tasks matching task filter and department filter'''
|
||||
self._ensure_login()
|
||||
return self._impl.get_tasks_data(filters)
|
||||
|
||||
def get_tasks_content(self, task_ids: list[str]) -> list:
|
||||
'''Export content for specific task ids'''
|
||||
self._ensure_login()
|
||||
return self._impl.get_tasks_content(task_ids)
|
||||
|
||||
def _ensure_login(self):
|
||||
if time.time() - self._time_login < self._login_timeout:
|
||||
return
|
||||
logging.info('Relogging at %s', time.strftime('%H:%M:%S'))
|
||||
self.logout()
|
||||
self.login()
|
||||
|
||||
def load_content(self, content: str, authors: str, text: str, bibliography: str) -> bool:
|
||||
'''Loads content'''
|
||||
self._ensure_login()
|
||||
return self._impl.load_content(content, authors, text, bibliography)
|
||||
|
||||
|
||||
class _UploaderImpl:
|
||||
"""Implementation of portal data input automation"""
|
||||
_CONTEXT_CONTENT = 'CRUD EDITOR'
|
||||
_CONTEXT_SEARCH = 'SEARCH'
|
||||
_CONTENT_OMITTED = 'Заполните обязательный признак'
|
||||
|
||||
_XP_PREFIX_DIALOG = '//span[contains(text(),"{0}")]/parent::*/parent::*/descendant::'
|
||||
_XP_PREFIX_CONTENT = '//section[@class="article-CRUD__widget"]/descendant::'
|
||||
|
||||
_XP_NOTIFICATION_UPDATE_OK = '//*[contains(text(),"{0} успешно отредактирован")]'
|
||||
|
||||
_PAGE_PORTAL = 'https://rk.greatbook.ru'
|
||||
_PAGE_LOGIN = _PAGE_PORTAL
|
||||
_PAGE_TASK = _PAGE_PORTAL + '/tasks/kanban/all'
|
||||
_PAGE_CONTENT = _PAGE_PORTAL + '/glossary/all'
|
||||
_PAGE_TASK_ID = _PAGE_PORTAL + '/tasks/{0}'
|
||||
_PAGE_CONTENT_BY_TASK = _PAGE_PORTAL + '/widgets?link=task&id={0}'
|
||||
|
||||
_XP_LOGIN_TEST = 'app-login__right-container'
|
||||
_XP_MAIN_ADD_BUTTON = '//button[@class="el-tooltip button-input-add primary"]'
|
||||
|
||||
_XP_LIST_ITEM_CONTAINS = '//li[contains(text(),"{0}")]'
|
||||
_XP_SPAN_CONTAINS = '//span[contains(text(),"{0}")]'
|
||||
_XP_SPAN_EXACT_MATCH = '//span[text()=" {0} " or text()="{0}"]'
|
||||
_XP_BUTTON_CONTAINS = '//button[contains(text(),"{0}")]'
|
||||
|
||||
_XP_TABLE_ROW = '//div[@class="table__row"]'
|
||||
_XP_TABLE_CELL = '(descendant::div[@class="table__row-cell sortable"])[{0}]/child::*/child::*'
|
||||
_XP_TABLE_ROW_EXPAND = 'ancestor::div[@class="table__row-content"]/child::div[@class="table__row-expand"]'
|
||||
|
||||
_XP_NOTIFICATION_CLOSE = '//button[@class="notifications-layout__item-close"]'
|
||||
|
||||
_XP_NEW_TASK_BTN = '//div[@class="sidebar-tasks"]/button'
|
||||
_XP_SPAN_STATUS = '//span[@class="task-form__row-status"]'
|
||||
|
||||
_XP_LOGIN_USER = '//input[@placeholder="Имя пользователя"]'
|
||||
_XP_LOGIN_PW = '//input[@placeholder="Пароль"]'
|
||||
_XP_LOGIN_BTN = '//button[@type="button"]'
|
||||
|
||||
_XP_USER_PROFILE = '//div[@class="el-tooltip top-bar__user-profile"]'
|
||||
_XP_USER_EXIT = '//div[contains(text(),"Выйти")]'
|
||||
|
||||
_XP_TASKS_NEXT_BTN = '//i[@class="el-icon el-icon-arrow-right"]'
|
||||
_XP_TASKS_PAGER = '//ul[@class="el-pager"]'
|
||||
|
||||
_XP_TASK_SELECT_STEP = '//span[text()="Выбрать шаг"]/parent::button'
|
||||
_XP_TASK_CREATE_SUB = '//button[contains(text(),"Создать подзадачу")]'
|
||||
_XP_TASK_DATA = '//label[text()=" {0} " or text()="{0}"]/following-sibling::div/descendant::span'
|
||||
_XP_TASK_INPUT = '//label[text()=" {0} " or text()="{0}"]/following-sibling::div/descendant::input'
|
||||
_XP_TASK_CONTENT_URL = '//a[text()="{0}"]'
|
||||
_XP_TASK_RADIO_BTNS = '//span[@class="el-radio__inner"]'
|
||||
_XP_TASK_PAGINATION_CONTROL = '//span[@class="el-pagination__sizes"]/descendant::input'
|
||||
_XP_INPUT_ACCEPT = 'ancestor::div[@class="deferred-form-input__edit edit-mode"]/descendant::button[@class="button-with-icon-24x24 button-toggle"]'
|
||||
|
||||
_XP_DIALOG_LABEL = _XP_PREFIX_DIALOG + 'label[text()=" {1} " or text()="{1}"]'
|
||||
_XP_DIALOG_INPUT = _XP_DIALOG_LABEL + '/following-sibling::div/descendant::input'
|
||||
_XP_DIALOG_COMBO = _XP_DIALOG_LABEL + '/following-sibling::div/child::div/child::div'
|
||||
_XP_DIALOG_TEXT_AREA = _XP_DIALOG_LABEL + '/following-sibling::div/descendant::textarea'
|
||||
_XP_DIALOG_SWITCH = _XP_PREFIX_DIALOG + 'div[@id="{1}"]'
|
||||
|
||||
_XP_DIALOG_BTN_SELECT = '//button[contains(text(),"Выбрать")]'
|
||||
_XP_DIALOG_BTN_CLEAN = '(//span[@class="tasks-filters-dialog__footer"]/button)[2]'
|
||||
_XP_DIALOG_BTN_OK = '(//span[@class="tasks-filters-dialog__footer"]/button)[3]'
|
||||
_XP_DIALOG_BTN_PRIMARY = _XP_PREFIX_DIALOG + 'button[@class="button-dialog button-type-primary"]'
|
||||
_XP_DIALOG_BTN_CANCEL = _XP_PREFIX_DIALOG + 'button[@aria-label="Close"]'
|
||||
|
||||
_XP_CONTENT_LABEL = _XP_PREFIX_CONTENT + 'label[text()=" {0} " or text()="{0}"]'
|
||||
_XP_CONTENT_INPUT = _XP_CONTENT_LABEL + '/following-sibling::div/descendant::input'
|
||||
_XP_CONTENT_COMBO = _XP_CONTENT_LABEL + '/following-sibling::div/child::div/child::div'
|
||||
_XP_CONTENT_TEXT_AREA = _XP_CONTENT_LABEL + '/following-sibling::div/descendant::textarea'
|
||||
_XP_CONTENT_SWITCH = _XP_PREFIX_CONTENT + 'div[@id="{0}"]'
|
||||
|
||||
_XP_CONTENT_BTN_PRIMARY = _XP_PREFIX_CONTENT + 'button[@class="button-dialog button-type-primary"]'
|
||||
_XP_CONTENT_BTN_CANCEL = _XP_PREFIX_CONTENT + 'button[@class="button-dialog button-type-primary"]'
|
||||
_XP_CONTENT_BTN_EDIT = '//button[text()="Редактировать" or text()=" Редактировать "]'
|
||||
_XP_CONTENT_IS_EDITING = '//*[contains(text(),"атрибуты и признаки")]'
|
||||
|
||||
_XP_CONTENT_CHAPTER = '//p[contains(text(), "Раздел")]'
|
||||
_XP_CONTENT_VIEW_LABEL = '//span[@class="view-section__label" and text()="{0}:"]'
|
||||
_XP_CONTENT_VIEW_VALUE = _XP_CONTENT_VIEW_LABEL + '/following-sibling::span/span'
|
||||
_XP_CONTENT_TITLE = '//div[@class="top-bar__title-col flex"]/h2[text()="Редактор"]'
|
||||
|
||||
_XP_SEARCH_INPUT = '//label[contains(text(),"{0}")]/following-sibling::div/descendant::input'
|
||||
_XP_SCROLLER_ITEM = '//div[text()=" {0} " or text()="{0}" or text()=" {0} "]'
|
||||
_XP_SCROLLER_CHECKBOX = 'preceding-sibling::div[@class="tree__node-checkbox"]/label[@class="el-checkbox"]'
|
||||
_XP_LIST_ITEM = '//li[.="{0}" or .=" {0} "]'
|
||||
|
||||
_XP_RELATIVE_BTN_CLOSE = 'descendant::button[@class="catalogs-block__close icon-close-tags"]'
|
||||
_XP_RELATIVE_BTN_PLUS = 'following-sibling::button[@class="button-input-add"]'
|
||||
_XP_RELATIVE_OUTPUT_END = 'following-sibling::input[@placeholder="Конец"]'
|
||||
|
||||
_XP_SEARCH_QUERY = '//input[@placeholder="Поиск"]'
|
||||
_XP_SEARCH_BAR = '//div[@class="el-input el-input-group el-input-group--append el-input--suffix"]/input'
|
||||
_XP_SEARCH_BTN = '//button[@slot="append"]'
|
||||
_XP_SEARCH_OPTIONS = '//button[@class="tasks-filters__search-btn button-graphic"]'
|
||||
_XP_SEARCH_NO_RESULTS = '//div[@class="search-list__no-results"]'
|
||||
_XP_TASKS_VIEW_LIST = '(//button[@class="el-tooltip button-graphic"])[3]'
|
||||
_XP_SEARCH_MAIN_HREF = '(//span[@class="parent-placeholder el-tooltip"]/parent::*/parent::*/preceding-sibling::*)[5]/descendant::a'
|
||||
|
||||
_XP_DIV_TAB = '//p[contains(text(),"{0}")]/parent::*/parent::*'
|
||||
_XP_CONTENT_BTN_EDIT_TEXT = '//button[contains(text(),"Редактировать раздел")]'
|
||||
|
||||
_XP_EDITOR_TEXT_AREA = '//*[@class="ProseMirror"]'
|
||||
|
||||
def __init__(self, browser: WebBrowser, config: Config):
|
||||
self.browser = browser
|
||||
self._config = config
|
||||
self._output_context = ''
|
||||
self._test_run = self._config['Options'].getboolean('TestRun')
|
||||
|
||||
def __del__(self):
|
||||
pass
|
||||
|
||||
def login(self) -> bool:
|
||||
'''Login into Portal'''
|
||||
self.browser.driver.get(self._PAGE_LOGIN)
|
||||
if not self.browser.wait_presence(self._XP_LOGIN_TEST, By.CLASS_NAME):
|
||||
return False
|
||||
|
||||
user = self._config['UserData']['UserLogin']
|
||||
password = decrypt_user(user, self._config['UserData']['Password'], self._config['AppData']['Password'])
|
||||
self.browser.by_xpath(self._XP_LOGIN_USER).send_keys(user)
|
||||
self.browser.by_xpath(self._XP_LOGIN_PW).send_keys(password)
|
||||
self.browser.by_xpath(self._XP_LOGIN_BTN).click()
|
||||
return self._wait_loading()
|
||||
|
||||
def logout(self):
|
||||
'''Logout from current user'''
|
||||
try:
|
||||
self.browser.by_xpath(self._XP_USER_PROFILE).click()
|
||||
self.browser.by_xpath(self._XP_USER_EXIT).click()
|
||||
self.browser.wait_ajax()
|
||||
except NoSuchElementException:
|
||||
pass
|
||||
|
||||
def content_exists(self, content: str) -> bool:
|
||||
'''Check if content name is taken'''
|
||||
self._load_page(self._PAGE_CONTENT)
|
||||
|
||||
search = self.browser.by_xpath(self._XP_SEARCH_BAR)
|
||||
search.clear()
|
||||
search.send_keys(content)
|
||||
search.send_keys(Keys.RETURN)
|
||||
self.browser.wait_ajax()
|
||||
|
||||
results = self.browser.by_xpath_m(self._XP_SPAN_EXACT_MATCH.format(content))
|
||||
return len(results) > 0
|
||||
|
||||
def content_has_text(self, content: str) -> bool:
|
||||
'''Check if found content has attached text'''
|
||||
content_cell = self.browser.by_xpath(self._XP_SPAN_EXACT_MATCH.format(content))
|
||||
return len(content_cell.find_elements(By.XPATH, self._XP_TABLE_ROW_EXPAND)) > 0
|
||||
|
||||
def create_task(self, data: dict) -> bool:
|
||||
'''Create task in system. Returns empty string on error'''
|
||||
if not self._open_new_task_form():
|
||||
return ''
|
||||
|
||||
self._output_context = 'Создание задачи'
|
||||
for field in FieldType:
|
||||
if field in data:
|
||||
if field in data:
|
||||
self._output_field(field, data[field])
|
||||
self._finalize_output()
|
||||
self._wait_dialog_done()
|
||||
return self.find_task_id(data[FieldType.task_name])
|
||||
|
||||
def find_task_id(self, task_name: str) -> str:
|
||||
'''Find task ID or empty string if not found'''
|
||||
self._load_page(self._PAGE_TASK)
|
||||
self._list_view()
|
||||
self._search_task_name(task_name)
|
||||
|
||||
for row in self.browser.by_xpath_m(self._XP_TABLE_ROW):
|
||||
found_name = row.find_element(By.XPATH, self._XP_TABLE_CELL.format(3)).text
|
||||
if found_name != task_name:
|
||||
continue
|
||||
parent = row.find_element(By.XPATH, self._XP_TABLE_CELL.format(4)).text
|
||||
if parent != 'Нет':
|
||||
continue
|
||||
status = row.find_element(By.XPATH, self._XP_TABLE_CELL.format(10)).text
|
||||
if status == 'Отменена':
|
||||
continue
|
||||
reference = row.find_element(By.XPATH, self._XP_TABLE_CELL.format(3))
|
||||
return reference.get_attribute('href').split('/')[-1]
|
||||
return ''
|
||||
|
||||
def set_task_status(self, task_id: str, new_status: str, status_label: str) -> bool:
|
||||
'''Change task status. Can fail (return false) if status is unavailable'''
|
||||
if task_id != '':
|
||||
self._load_page(self._PAGE_TASK_ID.format(task_id))
|
||||
self.browser.by_xpath(self._XP_TASK_SELECT_STEP).click()
|
||||
self.browser.wait_ajax()
|
||||
status_element = self.browser.by_xpath_m(self._XP_LIST_ITEM_CONTAINS.format(new_status))
|
||||
if len(status_element) == 0:
|
||||
return False
|
||||
if not self._test_run:
|
||||
status_element[0].click()
|
||||
time.sleep(self._config['Options'].getint('WaitStatus'))
|
||||
self.browser.driver.refresh()
|
||||
self._wait_loading()
|
||||
self._ensure_no_notifications()
|
||||
status = self._get_task_status()
|
||||
if status != status_label:
|
||||
return False
|
||||
return True
|
||||
|
||||
def fill_metadata(self, task_id: str, data: dict, attributes: list) -> bool:
|
||||
'''Create metadata. Also changes status for new content'''
|
||||
if not self._load_task_page(task_id, data[FieldType.task_name]):
|
||||
return False
|
||||
if self._has_content():
|
||||
return False
|
||||
|
||||
is_article = data[FieldType.task_type] in ['Статья', 'Актуализация статьи из ЭВ БРЭ']
|
||||
status = self._get_task_status()
|
||||
if status == 'Новая':
|
||||
status = 'Заполнение метаданных статьи' if is_article else 'Написание контента'
|
||||
status_label = status
|
||||
if not self.set_task_status('', status, status_label):
|
||||
return False
|
||||
|
||||
if not self._create_content(data, attributes) or not self._wait_loading():
|
||||
return False
|
||||
if not self._has_bibliography():
|
||||
self._create_bibliography_task()
|
||||
return self._progress_metedata(is_article)
|
||||
|
||||
def update_metadata(self, task_id: str, data: dict, attributes: list) -> bool:
|
||||
'''Update metadata'''
|
||||
if not self._load_task_page(task_id, data[FieldType.task_name]):
|
||||
return False
|
||||
content = self.browser.by_xpath_m(self._XP_TASK_CONTENT_URL.format(data[FieldType.content_name_db]))
|
||||
if len(content) != 1:
|
||||
return False
|
||||
self._load_page(content[0].get_attribute('href'))
|
||||
return self._update_content(data, attributes)
|
||||
|
||||
def get_tasks_data(self, filters: list[list]) -> list:
|
||||
'''Export data for tasks matching task filter and department filter'''
|
||||
self._load_page(self._PAGE_TASK)
|
||||
self._list_view()
|
||||
self.browser.by_xpath(self._XP_TASK_PAGINATION_CONTROL).click()
|
||||
self.browser.wait_ajax()
|
||||
self.browser.by_xpath(self._XP_SPAN_CONTAINS.format('50 на странице')).click()
|
||||
self.browser.wait_ajax()
|
||||
|
||||
if not self._search_all_tasks(filters):
|
||||
return []
|
||||
else:
|
||||
return self._read_all_tasks()
|
||||
|
||||
def get_tasks_content(self, task_ids: list[str]) -> list[list[str]]:
|
||||
'''Export content for specific task ids'''
|
||||
result = []
|
||||
for task_id in task_ids:
|
||||
try:
|
||||
self._load_page(self._PAGE_CONTENT_BY_TASK.format(task_id))
|
||||
self.browser.wait_ajax()
|
||||
title = self.browser.by_xpath_m(self._XP_CONTENT_TITLE)
|
||||
if len(title) == 0:
|
||||
continue
|
||||
result.append(self._read_content(task_id))
|
||||
|
||||
self._load_page(self._PAGE_TASK_ID.format(task_id))
|
||||
self.browser.wait_presence(self._XP_TASK_DATA.format(FieldType.task_manager.to_label()), By.XPATH)
|
||||
editor = self.browser.by_xpath(self._XP_TASK_DATA.format(FieldType.task_manager.to_label())).text
|
||||
result[-1].append(editor)
|
||||
except Exception as e:
|
||||
logging.info('Exception while reading content %s\n%s', task_id, e)
|
||||
return result
|
||||
|
||||
def load_content(self, content: str, authors: str, text: str, bibliography: str) -> bool:
|
||||
'''Update metadata
|
||||
if not self._load_task_page(task_id, data[FieldType.task_name]):
|
||||
return False
|
||||
content = self.browser.by_xpath_m(self._XP_TASK_CONTENT_URL.format(data[FieldType.content_name_db]))
|
||||
if len(content) != 1:
|
||||
return False
|
||||
self._load_page(content[0].get_attribute('href'))
|
||||
return self._update_content(data, attributes)'''
|
||||
|
||||
self._load_page(self._PAGE_CONTENT)
|
||||
|
||||
search = self.browser.by_xpath(self._XP_SEARCH_BAR)
|
||||
search.clear()
|
||||
search.send_keys(content)
|
||||
search.send_keys(Keys.RETURN)
|
||||
self._wait_loading()
|
||||
|
||||
results = self.browser.by_xpath_m(self._XP_SPAN_EXACT_MATCH.format(content))
|
||||
if len(results) != 1:
|
||||
return False
|
||||
|
||||
results[0].click()
|
||||
if not self._wait_loading():
|
||||
return False
|
||||
|
||||
self.browser.wait_ajax()
|
||||
main_tab = self.browser.by_xpath(self._XP_DIV_TAB.format('Раздел'))
|
||||
|
||||
main_tab.click()
|
||||
self.browser.wait_ajax()
|
||||
|
||||
if len(authors) != 0:
|
||||
self.browser.by_xpath(self._XP_CONTENT_BTN_EDIT).click()
|
||||
self._wait_loading()
|
||||
self._add_attrbute("Автор (на портале)")
|
||||
self._output_field(FieldType.author, authors)
|
||||
self.browser.by_xpath(self._XP_BUTTON_CONTAINS.format("Сохранить")).click() # test method
|
||||
else:
|
||||
self.browser.by_xpath(self._XP_CONTENT_BTN_EDIT).click()
|
||||
self._wait_loading()
|
||||
# field = self.browser.by_xpath('//label[text()=" Аффилированные организации " or text()="Аффилированные организации"]/parent::*//span[contains(text(),"Добавьте")]')
|
||||
# if len(field) != 0:
|
||||
# self._output_field(FieldType.affiliated_organizations, "Редакция технологий и техники")
|
||||
self._add_attrbute("Аффилированные организации")
|
||||
self._output_item("Аффилированные организации", InputMethod.combo_dialog, ["Редакция технологий и техники"]) # remove feature
|
||||
|
||||
self.browser.by_xpath(self._XP_BUTTON_CONTAINS.format("Сохранить")).click() # test method
|
||||
|
||||
self.browser.by_xpath(self._XP_BUTTON_CONTAINS.format("Отменить")).click() # test method
|
||||
# parse data_reader module
|
||||
# разобрать на методы заведение авторов, библиографии, текст
|
||||
|
||||
if text != '':
|
||||
self.browser.by_xpath(self._XP_CONTENT_BTN_EDIT_TEXT).click()
|
||||
|
||||
self.browser.wait_ajax()
|
||||
textarea = self.browser.by_xpath(self._XP_EDITOR_TEXT_AREA)
|
||||
|
||||
textarea.click()
|
||||
|
||||
textarea.send_keys(text)
|
||||
|
||||
# self.browser.by_xpath(self._XP_BUTTON_CONTAINS.format("Сохранить")).click()
|
||||
return True
|
||||
|
||||
def _load_page(self, page_url: str):
|
||||
self.browser.driver.get(page_url)
|
||||
if not self._wait_loading():
|
||||
raise BaseException(f"Cannot load page {page_url}")
|
||||
|
||||
if not self._login_required():
|
||||
return
|
||||
if not self.login():
|
||||
raise BaseException("Cannot login")
|
||||
|
||||
self.browser.driver.get(page_url)
|
||||
if not self._wait_loading():
|
||||
raise BaseException(f"Cannot load page {page_url}")
|
||||
|
||||
def _login_required(self) -> bool:
|
||||
return len(self.browser.driver.find_elements(By.CLASS_NAME, self._XP_LOGIN_TEST)) != 0
|
||||
|
||||
def _has_content(self) -> bool:
|
||||
return len(self.browser.by_xpath_m(self._XP_BUTTON_CONTAINS.format('+ Создать'))) == 0
|
||||
|
||||
def _is_content_editing(self) -> bool:
|
||||
elems = self.browser.by_xpath_m(self._XP_CONTENT_IS_EDITING)
|
||||
return len(elems) > 0
|
||||
|
||||
def _has_bibliography(self):
|
||||
return len(self.browser.by_xpath_m(self._XP_SPAN_CONTAINS.format('Раздел библиография'))) != 0
|
||||
|
||||
def _get_task_status(self) -> str:
|
||||
return self.browser.by_xpath(self._XP_SPAN_STATUS).text
|
||||
|
||||
def _wait_dialog_done(self):
|
||||
time.sleep(self._config['Options'].getint('WaitData'))
|
||||
|
||||
def _wait_loading(self) -> bool:
|
||||
if not self.browser.wait_presence(self._XP_MAIN_ADD_BUTTON, By.XPATH):
|
||||
return False
|
||||
self.browser.wait_ajax()
|
||||
return True
|
||||
|
||||
def _ensure_no_notifications(self):
|
||||
while True:
|
||||
close_btns = self.browser.by_xpath_m(self._XP_NOTIFICATION_CLOSE)
|
||||
if len(close_btns) == 0:
|
||||
return
|
||||
try:
|
||||
close_btns[0].click()
|
||||
except ElementNotInteractableException:
|
||||
pass
|
||||
self.browser.wait_ajax()
|
||||
|
||||
def _list_view(self):
|
||||
self.browser.by_xpath(self._XP_TASKS_VIEW_LIST).click()
|
||||
self.browser.wait_ajax()
|
||||
|
||||
def _load_task_page(self, task_id: str, task_name: str) -> bool:
|
||||
self._load_page(self._PAGE_TASK_ID.format(task_id))
|
||||
task_header = self.browser.by_xpath('//h3').text
|
||||
return task_header == task_name
|
||||
|
||||
def _search_task_name(self, task_name: str):
|
||||
search = self.browser.by_xpath(self._XP_SEARCH_BAR)
|
||||
search.clear()
|
||||
search.click()
|
||||
search.send_keys(task_name)
|
||||
|
||||
options = self.browser.by_xpath(self._XP_SEARCH_OPTIONS)
|
||||
options.click()
|
||||
self.browser.wait_ajax()
|
||||
|
||||
self.browser.wait_presence(self._XP_DIALOG_BTN_CLEAN, By.XPATH)
|
||||
self.browser.by_xpath(self._XP_DIALOG_BTN_CLEAN).click()
|
||||
self.browser.wait_ajax()
|
||||
|
||||
self.browser.by_xpath(self._XP_SPAN_CONTAINS.format('Точное совпадение')).click()
|
||||
self.browser.by_xpath(self._XP_DIALOG_BTN_OK).click()
|
||||
self.browser.wait_ajax()
|
||||
|
||||
def _search_all_tasks(self, filters: list[list]):
|
||||
search = self.browser.by_xpath(self._XP_SEARCH_BAR)
|
||||
search.clear()
|
||||
|
||||
options = self.browser.by_xpath(self._XP_SEARCH_OPTIONS)
|
||||
options.click()
|
||||
self.browser.wait_ajax()
|
||||
|
||||
self.browser.wait_presence(self._XP_DIALOG_BTN_CLEAN, By.XPATH)
|
||||
self.browser.by_xpath(self._XP_DIALOG_BTN_CLEAN).click()
|
||||
self.browser.wait_ajax()
|
||||
|
||||
self._output_context = self._CONTEXT_SEARCH
|
||||
for filter_id in range(FilterType.task_type, FilterType.observer):
|
||||
label = FilterType(filter_id).to_label()
|
||||
for value in filters[filter_id]:
|
||||
if value != '':
|
||||
self._output_combobox(label, value)
|
||||
if filters[FilterType.created_begin] != '':
|
||||
self._output_date_range(FilterType.created_begin.to_label(), filters[FilterType.created_begin], filters[FilterType.created_end])
|
||||
if filters[FilterType.target_begin] != '':
|
||||
self._output_date_range(FilterType.target_begin.to_label(), filters[FilterType.target_begin], filters[FilterType.target_end])
|
||||
|
||||
self.browser.by_xpath(self._XP_DIALOG_BTN_OK).click()
|
||||
self.browser.wait_ajax()
|
||||
|
||||
no_result = self.browser.by_xpath(self._XP_SEARCH_NO_RESULTS)
|
||||
return not no_result.is_displayed()
|
||||
|
||||
def _read_all_tasks(self) -> list:
|
||||
result = []
|
||||
next_btn = self.browser.by_xpath(self._XP_TASKS_NEXT_BTN)
|
||||
count_pages = int(self.browser.by_xpath(self._XP_TASKS_PAGER).find_elements(By.XPATH, 'descendant::li')[-1].text)
|
||||
for _ in range(count_pages):
|
||||
result += self._scan_tasks_table()
|
||||
next_btn.click()
|
||||
self.browser.wait_ajax()
|
||||
time.sleep(random.randint(0, 100)/100)
|
||||
self._ensure_no_notifications()
|
||||
return result
|
||||
|
||||
def _scan_tasks_table(self) -> list:
|
||||
result = []
|
||||
for row in self.browser.by_xpath_m(self._XP_TABLE_ROW):
|
||||
task_type = self._read_cell(row, 2).text
|
||||
task_name = self._read_cell(row, 3).text
|
||||
task_id = self._read_cell(row, 3).get_attribute('href').split('/')[-1]
|
||||
parent = self._read_cell(row, 4)
|
||||
if parent.text != 'Нет':
|
||||
parent_id = parent.find_element(By.XPATH, 'descendant::a').get_attribute('href').split('/')[-1]
|
||||
else:
|
||||
parent_id = 'Нет'
|
||||
content_name = self._read_cell(row, 6).text
|
||||
target_date = self._read_cell(row, 7).text
|
||||
supervisor = self._read_cell(row, 8).text
|
||||
worker = self._read_cell(row, 9).text
|
||||
status = self._read_cell(row, 10).text
|
||||
result.append([task_type, status, content_name, supervisor, worker, target_date, task_id, task_name, parent_id])
|
||||
return result
|
||||
|
||||
def _read_content(self, task_id: str) -> list:
|
||||
editorial = self._read_field(FieldType.editorial)
|
||||
if editorial.startswith('Редакция '):
|
||||
editorial = editorial[len('Редакция '):]
|
||||
result = [
|
||||
task_id,
|
||||
self._read_field(FieldType.biblio_name),
|
||||
self._read_field(FieldType.change_score),
|
||||
editorial,
|
||||
self._read_field(FieldType.responsible),
|
||||
self._read_field(FieldType.definition),
|
||||
self._read_field(FieldType.object_type),
|
||||
self._read_field(FieldType.markers),
|
||||
self._read_field(FieldType.tags),
|
||||
self._read_field(FieldType.source),
|
||||
self._read_field(FieldType.electron_bre),
|
||||
self._read_field(FieldType.main_page),
|
||||
self._read_field(FieldType.is_general),
|
||||
self._read_field(FieldType.actualize_period),
|
||||
self._read_field(FieldType.age_restriction)
|
||||
]
|
||||
|
||||
chapters = self.browser.by_xpath_m(self._XP_CONTENT_CHAPTER)
|
||||
if len(chapters) == 0:
|
||||
author = ''
|
||||
else:
|
||||
chapters[0].click()
|
||||
self.browser.wait_ajax()
|
||||
author = self._read_field(FieldType.author)
|
||||
result.append(author)
|
||||
return result
|
||||
|
||||
def _create_content(self, data: dict, attributes: list) -> bool:
|
||||
self._ensure_no_notifications()
|
||||
|
||||
if not self.browser.wait_clickable(self._XP_BUTTON_CONTAINS.format('+ Создать'), By.XPATH):
|
||||
return False
|
||||
self.browser.by_xpath(self._XP_BUTTON_CONTAINS.format('+ Создать')).click()
|
||||
self.browser.wait_ajax()
|
||||
|
||||
self._output_context = 'Создать контент'
|
||||
self._output_content_data(data, attributes)
|
||||
self._wait_dialog_done()
|
||||
|
||||
if self._test_run:
|
||||
return True
|
||||
return self._has_content()
|
||||
|
||||
def _update_content(self, data: dict, attributes: list) -> bool:
|
||||
''' updates meta-data '''
|
||||
btn_edit = self.browser.by_xpath_m(self._XP_CONTENT_BTN_EDIT)
|
||||
if len(btn_edit) != 1:
|
||||
logging.warning('Не доступна кнопка Редактировать на странице контента')
|
||||
return False
|
||||
btn_edit[0].click()
|
||||
if not self._wait_loading() or not self._is_content_editing():
|
||||
return False
|
||||
|
||||
self._output_context = self._CONTEXT_CONTENT
|
||||
self._output_content_data(data, attributes)
|
||||
notifications = self.browser.by_xpath_m(self._XP_NOTIFICATION_UPDATE_OK.format(data[FieldType.content_name_db]))
|
||||
return len(notifications) > 0
|
||||
|
||||
def _create_bibliography_task(self):
|
||||
self._ensure_no_notifications()
|
||||
|
||||
executor = self.browser.by_xpath_m(self._XP_TASK_DATA.format(FieldType.executor.to_label()))[0].text
|
||||
supervisor = self.browser.by_xpath_m(self._XP_TASK_DATA.format(FieldType.supervisor.to_label()))[0].text
|
||||
department = self.browser.by_xpath_m(self._XP_TASK_DATA.format(FieldType.department.to_label()))[0].text
|
||||
date_target = self.browser.by_xpath_m(self._XP_TASK_DATA.format(FieldType.date_target.to_label()))[0].text
|
||||
|
||||
self._output_context = 'Создание подзадачи'
|
||||
self.browser.by_xpath(self._XP_TASK_CREATE_SUB).click()
|
||||
self.browser.by_xpath_m(self._XP_TASK_RADIO_BTNS)[0].click()
|
||||
self.browser.by_xpath(self._XP_DIALOG_BTN_PRIMARY.format(self._output_context)).click()
|
||||
self.browser.wait_ajax()
|
||||
|
||||
self._output_field(FieldType.task_type, 'Раздел библиография')
|
||||
self._output_field(FieldType.executor, executor)
|
||||
self._output_field(FieldType.supervisor, supervisor)
|
||||
self._output_field(FieldType.department, department)
|
||||
self._output_field(FieldType.date_target, date_target)
|
||||
|
||||
self._finalize_output()
|
||||
self._wait_dialog_done()
|
||||
|
||||
def _progress_metedata(self, is_article: bool) -> bool:
|
||||
if is_article:
|
||||
if not self.set_task_status('', 'В службу', 'Ожидание проверки службой "Словник"'):
|
||||
return False
|
||||
new_executor = self._config['AppData']['SlovnikExecutor']
|
||||
else:
|
||||
if not self.set_task_status('', 'Заведующему редакцией', 'Проверка Заведующим редакцией'):
|
||||
return False
|
||||
new_executor = self.browser.by_xpath_m(self._XP_TASK_DATA.format(FieldType.supervisor.to_label()))[0].text
|
||||
self._ensure_no_notifications()
|
||||
self._output_task_combo(FieldType.executor.to_label(), new_executor)
|
||||
return True
|
||||
|
||||
def _read_cell(self, row: WebElement, column: int) -> WebElement:
|
||||
return row.find_element(By.XPATH, self._XP_TABLE_CELL.format(column))
|
||||
|
||||
def _read_field(self, field: FieldType) -> str:
|
||||
label = field.to_label()
|
||||
input_method = field.input_method()
|
||||
return self._read_item(label, input_method)
|
||||
|
||||
def _read_item(self, field_label: str, input_method: InputMethod) -> str:
|
||||
xpath = self._XP_CONTENT_VIEW_VALUE.format(field_label)
|
||||
retries = 3
|
||||
value = ''
|
||||
while True:
|
||||
elems = self.browser.by_xpath_m(xpath)
|
||||
if len(elems) == 0:
|
||||
return ''
|
||||
value = elems[0].text
|
||||
if (self._validate_input(value) or retries == 0):
|
||||
break
|
||||
retries = retries - 1
|
||||
time.sleep(0.5)
|
||||
value = value.replace('\xad', '#')
|
||||
value = value.replace('\xd7', '#')
|
||||
if value == self._CONTENT_OMITTED:
|
||||
return ''
|
||||
if input_method in [InputMethod.combo_box, InputMethod.combo_dialog, InputMethod.combo_dialog_simple_list]:
|
||||
value = _replace_comma_separators(value)
|
||||
return value
|
||||
|
||||
def _validate_input(self, value: str) -> bool:
|
||||
if value == '...':
|
||||
return False
|
||||
if value == 'Загрузка...':
|
||||
return False
|
||||
return True
|
||||
|
||||
def _open_new_task_form(self) -> bool:
|
||||
self._load_page(self._PAGE_TASK)
|
||||
self.browser.by_xpath(self._XP_NEW_TASK_BTN).click()
|
||||
return self.browser.wait_presence(self._XP_DIALOG_BTN_PRIMARY.format('Создание задачи'), By.XPATH)
|
||||
|
||||
def _output_content_data(self, data: dict, attributes: list):
|
||||
for field in FieldType:
|
||||
if field in data:
|
||||
self._output_field(field, data[field])
|
||||
self.browser.wait_ajax()
|
||||
|
||||
for attr in attributes:
|
||||
self._output_attribute(attr[0], attr[1], attr[2])
|
||||
self.browser.wait_ajax()
|
||||
|
||||
self._finalize_output()
|
||||
self.browser.wait_ajax()
|
||||
|
||||
def _finalize_output(self):
|
||||
self._ensure_no_notifications()
|
||||
btn_path = self._xpath_finalize_btn(self._output_context)
|
||||
self.browser.by_xpath(btn_path).click()
|
||||
self.browser.wait_ajax()
|
||||
|
||||
def _output_field(self, field: FieldType, value):
|
||||
label = field.to_label()
|
||||
input_method = field.input_method()
|
||||
self._output_item(label, input_method, value)
|
||||
|
||||
def _output_attribute(self, name: str, input_method: InputMethod, value):
|
||||
if not self._test_output_exists(name):
|
||||
self._add_attrbute(name)
|
||||
self._output_item(name, input_method, value)
|
||||
|
||||
def _add_attrbute(self, name: str):
|
||||
self._ensure_no_notifications()
|
||||
self.browser.by_xpath(self._XP_BUTTON_CONTAINS.format('Добавить признак')).click()
|
||||
self.browser.wait_ajax()
|
||||
self._select_dialog_item(name)
|
||||
|
||||
def _output_item(self, field_label: str, input_method: InputMethod, value):
|
||||
if value == '':
|
||||
return
|
||||
if input_method == InputMethod.read_only:
|
||||
return
|
||||
if not self._test_output_exists(field_label):
|
||||
return
|
||||
|
||||
if input_method == InputMethod.combo_box:
|
||||
self._output_combobox(field_label, value)
|
||||
elif input_method == InputMethod.text_input:
|
||||
self._output_text_input(field_label, value)
|
||||
elif input_method == InputMethod.text_area:
|
||||
self._output_text_area(field_label, value)
|
||||
elif input_method == InputMethod.date_picker:
|
||||
self._output_date(field_label, value)
|
||||
elif input_method == InputMethod.bool_toggle:
|
||||
self._output_bool(field_label, value)
|
||||
elif input_method == InputMethod.combo_dialog_simple_list:
|
||||
self._output_dialog(field_label, value, False)
|
||||
elif input_method == InputMethod.combo_dialog:
|
||||
if not isinstance(value, list):
|
||||
raise "List argument expected. Given: %s" % type(value)
|
||||
self._output_dialog(field_label, value, True)
|
||||
|
||||
def _test_output_exists(self, field_label: str) -> bool:
|
||||
xpath = self._xpath_label(field_label, self._output_context)
|
||||
elems = self.browser.by_xpath_m(xpath)
|
||||
return len(elems) > 0
|
||||
|
||||
def _output_bool(self, field_label: str, value: bool):
|
||||
bool_switch = self.browser.by_xpath(self._xpath_switch(field_label, self._output_context))
|
||||
current_value = 'is-checked' in bool_switch.get_attribute('class')
|
||||
if current_value != value:
|
||||
bool_switch.click()
|
||||
|
||||
def _output_text_input(self, field_label: str, value: str):
|
||||
self._output(self._xpath_input(field_label, self._output_context), value)
|
||||
|
||||
def _output_text_area(self, field_label: str, value: str):
|
||||
self._output(self._xpath_textarea(field_label, self._output_context), value)
|
||||
|
||||
def _output_combobox(self, field_label: str, value: str):
|
||||
if not self._output(self._xpath_input(field_label, self._output_context), value):
|
||||
return
|
||||
self.browser.wait_ajax()
|
||||
list_item = self._find_active_list(value)
|
||||
list_item.click()
|
||||
self.browser.wait_ajax()
|
||||
self.browser.send_esc()
|
||||
|
||||
def _output_date_range(self, field_label: str, value1: str, value2: str):
|
||||
element = self.browser.by_xpath(self._xpath_input(field_label, self._output_context))
|
||||
element.click()
|
||||
element.send_keys(value1)
|
||||
element = element.find_element(By.XPATH, self._XP_RELATIVE_OUTPUT_END)
|
||||
element.click()
|
||||
element.send_keys(value2)
|
||||
self.browser.wait_ajax()
|
||||
self.browser.send_enter()
|
||||
|
||||
def _output_task_combo(self, label: str, value: str):
|
||||
element = self.browser.by_xpath_m(self._XP_TASK_DATA.format(label))[0]
|
||||
element.click()
|
||||
self.browser.wait_ajax()
|
||||
|
||||
element = self.browser.by_xpath_m(self._XP_TASK_INPUT.format(label))[0]
|
||||
element.send_keys(value)
|
||||
self.browser.wait_ajax()
|
||||
|
||||
list_item = self._find_active_list(value)
|
||||
list_item.click()
|
||||
self.browser.wait_ajax()
|
||||
|
||||
accept_btn = element.find_element(By.XPATH, self._XP_INPUT_ACCEPT)
|
||||
if not self._test_run:
|
||||
accept_btn.click()
|
||||
self.browser.wait_ajax()
|
||||
|
||||
def _output_date(self, field_label: str, value: str):
|
||||
self._output(self._xpath_input(field_label, self._output_context), value)
|
||||
|
||||
def _output_dialog(self, field_label, values: list[str], search_flag=True):
|
||||
self._ensure_no_notifications()
|
||||
item = self.browser.by_xpath(self._xpath_combo(field_label, self._output_context))
|
||||
remove_btns = item.find_elements(By.XPATH, self._XP_RELATIVE_BTN_CLOSE)
|
||||
for existing_item in remove_btns:
|
||||
existing_item.click()
|
||||
|
||||
btn_plus = item.find_element(By.XPATH, self._XP_RELATIVE_BTN_PLUS)
|
||||
for value in values:
|
||||
btn_plus.click()
|
||||
self.browser.wait_ajax()
|
||||
self._select_dialog_item(value, search_flag)
|
||||
|
||||
def _output(self, xpath: str, value: str) -> bool:
|
||||
element = self.browser.by_xpath(xpath)
|
||||
element.click()
|
||||
try:
|
||||
element.clear()
|
||||
except InvalidElementStateException:
|
||||
pass
|
||||
element.send_keys(value)
|
||||
return True
|
||||
|
||||
def _find_active_list(self, value: str):
|
||||
list_items = self.browser.by_xpath_m(self._XP_LIST_ITEM.format(value))
|
||||
count_active = sum(1 for x in list_items if x.is_displayed())
|
||||
if count_active > 1:
|
||||
logging.info('%s multiple list results', value)
|
||||
for list_item in list_items:
|
||||
if list_item.is_displayed():
|
||||
return list_item
|
||||
logging.info('Cannot find list item: %s', value)
|
||||
return None
|
||||
|
||||
def _find_active_scroller(self, value: str):
|
||||
list_items = self.browser.by_xpath_m(self._XP_SCROLLER_ITEM.format(value))
|
||||
count_active = sum(1 for x in list_items if x.is_displayed())
|
||||
if count_active == 0:
|
||||
logging.warning('Cannot find checkbox list item: %s', value)
|
||||
raise NoSuchElementException
|
||||
if count_active > 1:
|
||||
logging.info('%s multiple list results', value)
|
||||
for list_item in list_items:
|
||||
checkbox = list_item.find_elements(By.XPATH, self._XP_SCROLLER_CHECKBOX)
|
||||
if len(checkbox) > 0 and list_item.is_displayed():
|
||||
return list_item
|
||||
return None
|
||||
|
||||
def _select_dialog_item(self, value: str, search_flag=True):
|
||||
if search_flag:
|
||||
search = self.browser.by_xpath(self._XP_SEARCH_QUERY)
|
||||
search.send_keys(value)
|
||||
search.send_keys(Keys.ENTER)
|
||||
self.browser.wait_ajax()
|
||||
time.sleep(1) # additional wait for list to load
|
||||
|
||||
list_item = self._find_active_scroller(value)
|
||||
list_item.click()
|
||||
|
||||
self.browser.by_xpath(self._XP_DIALOG_BTN_SELECT).click()
|
||||
self.browser.wait_ajax()
|
||||
|
||||
def _xpath_label(self, field_label: str, context: str) -> str:
|
||||
if context == self._CONTEXT_CONTENT:
|
||||
return self._XP_CONTENT_LABEL.format(field_label)
|
||||
else:
|
||||
return self._XP_DIALOG_LABEL.format(context, field_label)
|
||||
|
||||
def _xpath_input(self, field_label: str, context: str) -> str:
|
||||
if context == self._CONTEXT_CONTENT:
|
||||
return self._XP_CONTENT_INPUT.format(field_label)
|
||||
elif context == self._CONTEXT_SEARCH:
|
||||
return self._XP_SEARCH_INPUT.format(field_label)
|
||||
else:
|
||||
return self._XP_DIALOG_INPUT.format(context, field_label)
|
||||
|
||||
def _xpath_textarea(self, field_label: str, context: str) -> str:
|
||||
if context == self._CONTEXT_CONTENT:
|
||||
return self._XP_CONTENT_TEXT_AREA.format(field_label)
|
||||
else:
|
||||
return self._XP_DIALOG_TEXT_AREA.format(context, field_label)
|
||||
|
||||
def _xpath_combo(self, field_label: str, context: str) -> str:
|
||||
if context == self._CONTEXT_CONTENT:
|
||||
return self._XP_CONTENT_COMBO.format(field_label)
|
||||
else:
|
||||
return self._XP_DIALOG_COMBO.format(context, field_label)
|
||||
|
||||
def _xpath_switch(self, field_label: str, context: str) -> str:
|
||||
if context == self._CONTEXT_CONTENT:
|
||||
return self._XP_CONTENT_SWITCH.format(field_label)
|
||||
else:
|
||||
return self._XP_DIALOG_SWITCH.format(context, field_label)
|
||||
|
||||
def _xpath_finalize_btn(self, context: str) -> str:
|
||||
if not self._test_run:
|
||||
if context == self._CONTEXT_CONTENT:
|
||||
return self._XP_CONTENT_BTN_PRIMARY
|
||||
else:
|
||||
return self._XP_DIALOG_BTN_PRIMARY.format(context)
|
||||
else:
|
||||
if context == self._CONTEXT_CONTENT:
|
||||
return self._XP_CONTENT_BTN_CANCEL
|
||||
else:
|
||||
return self._XP_DIALOG_BTN_CANCEL.format(context)
|
15
webapi/pyinstaller_run.bat
Normal file
15
webapi/pyinstaller_run.bat
Normal file
|
@ -0,0 +1,15 @@
|
|||
@echo off
|
||||
|
||||
venv\Scripts\pyinstaller --clean --onefile --distpath ./bin --hidden-import pandas._libs.tslibs.timedeltas, --hidden-import pandas._libs.tslibs.base menu.py
|
||||
|
||||
del menu.spec
|
||||
rmdir /S /Q build
|
||||
rmdir /S /Q __pycache__
|
||||
|
||||
venv\Scripts\pyinstaller --clean --onefile --distpath ./bin --hidden-import pandas._libs.tslibs.timedeltas, --hidden-import pandas._libs.tslibs.base exporter.py
|
||||
|
||||
del exporter.spec
|
||||
rmdir /S /Q build
|
||||
rmdir /S /Q __pycache__
|
||||
|
||||
exit
|
BIN
webapi/requirements.txt
Normal file
BIN
webapi/requirements.txt
Normal file
Binary file not shown.
BIN
webapi/requirements_dev.txt
Normal file
BIN
webapi/requirements_dev.txt
Normal file
Binary file not shown.
33
webapi/run_cardslot.py
Normal file
33
webapi/run_cardslot.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
'''Cardslot manipulation'''
|
||||
import sys
|
||||
import logging
|
||||
|
||||
import portal as bre
|
||||
|
||||
DEFAULT_OUTPUT = 'output.csv'
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
|
||||
|
||||
parser = argparse.ArgumentParser(description='BRE automation parameters')
|
||||
parser.add_argument('filename')
|
||||
parser.add_argument('config', help='Configuration INI file')
|
||||
parser.add_argument('input', help='Input xlsx file')
|
||||
parser.add_argument('password', help='Password to access encrypted user data')
|
||||
parser.add_argument('-check', '-c', action='store_true', help='Optional flag to only check existense')
|
||||
|
||||
args = parser.parse_args(sys.argv)
|
||||
config = bre.read_config(args.config)
|
||||
config['AppData']['Password'] = args.password
|
||||
|
||||
portal = bre.PortalAPI(config)
|
||||
if not portal.set_input(args.input):
|
||||
sys.exit()
|
||||
if not portal.set_output_tasks(DEFAULT_OUTPUT):
|
||||
sys.exit()
|
||||
|
||||
if args.check:
|
||||
sys.exit(portal.check_existence())
|
||||
else:
|
||||
sys.exit(portal.import_cardslots())
|
56
webapi/run_loader.py
Normal file
56
webapi/run_loader.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
'''Run electron BRE loading'''
|
||||
import sys
|
||||
import time
|
||||
|
||||
import portal
|
||||
|
||||
START_ROUND = 1
|
||||
NUMBER_OF_ROUNDS = 10
|
||||
NUMBER_OF_ARTICLES = 10
|
||||
|
||||
CONTINUE_FLAG = False
|
||||
START_ID = '5052657'
|
||||
STOP_ID = ''
|
||||
|
||||
SAVE_TO_PATH = 'PATH_TO_SAVE_FOLDER'
|
||||
|
||||
LOGIN_NAME = 'LOGIN'
|
||||
LOGIN_PW = 'PASSWORD'
|
||||
|
||||
# MAIN START
|
||||
|
||||
curr_id = START_ID
|
||||
|
||||
print("Start time is", time.strftime("%H") + ":" + time.strftime("%M"))
|
||||
|
||||
chrome_options = portal.Options()
|
||||
chrome_options.add_experimental_option("prefs", {"download.default_directory": SAVE_TO_PATH, "download.prompt_for_download": False, })
|
||||
chrome_options.add_argument('--headless')
|
||||
chrome = portal.WebBrowser()
|
||||
|
||||
for round_number in range(NUMBER_OF_ROUNDS):
|
||||
if curr_id == '':
|
||||
sys.exit(0)
|
||||
print("*********************************")
|
||||
print("** round #" + str(round_number + START_ROUND) + " of " + str(START_ROUND + NUMBER_OF_ROUNDS - 1))
|
||||
print("** start time is", time.strftime("%H") + ":" + time.strftime("%M"))
|
||||
print("*********************************")
|
||||
|
||||
chrome.start_chrome(chrome_options)
|
||||
processor = portal.GBDownloader(chrome, SAVE_TO_PATH, CONTINUE_FLAG, suffix_seed = round_number + START_ROUND)
|
||||
if not processor.login(LOGIN_NAME, LOGIN_PW):
|
||||
sys.exit(0)
|
||||
|
||||
curr_id = processor.scan(curr_id, STOP_ID, NUMBER_OF_ARTICLES)
|
||||
|
||||
time.sleep(2)
|
||||
del processor
|
||||
|
||||
chrome.stop_chrome()
|
||||
time.sleep(3)
|
||||
|
||||
print("** end time is", time.strftime("%H") + ":" + time.strftime("%M"))
|
||||
print("")
|
||||
|
||||
print("next start article ID = " + curr_id)
|
||||
print("next START_ROUND = " + str(START_ROUND + NUMBER_OF_ROUNDS))
|
26
webapi/run_metadata.py
Normal file
26
webapi/run_metadata.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
'''Metadata manipulations'''
|
||||
import sys
|
||||
import logging
|
||||
|
||||
import portal as bre
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
|
||||
|
||||
parser = argparse.ArgumentParser(description='BRE automation parameters')
|
||||
parser.add_argument('filename')
|
||||
parser.add_argument('config', help='Configuration INI file')
|
||||
parser.add_argument('input', help ='Input xlsx file')
|
||||
parser.add_argument('password', help='Password to access encrypted user data')
|
||||
|
||||
args = parser.parse_args(sys.argv)
|
||||
|
||||
config = bre.read_config(args.config)
|
||||
config['AppData']['Password'] = args.password
|
||||
|
||||
portal = bre.PortalAPI(config)
|
||||
if not portal.set_input(args.input):
|
||||
sys.exit()
|
||||
|
||||
sys.exit(portal.import_meta())
|
29
webapi/test.ini
Normal file
29
webapi/test.ini
Normal file
|
@ -0,0 +1,29 @@
|
|||
[AppData]
|
||||
FilterTask=Актуализация статьи из ЭВ БРЭ;Микропонятие
|
||||
FilterDepartment=Редакция технологий и техники;Редакция энергетики, промышленности
|
||||
FilterStatus=
|
||||
FilterResponsible=
|
||||
FilterSupervisor=
|
||||
FilterExecutor=
|
||||
FilterObserver=
|
||||
FilterDateCreatedBegin=01.01.2023
|
||||
FilterDateCreatedEnd=01.02.2024
|
||||
FilterDateTargetBegin=01.01.2023
|
||||
FilterDateTargetEnd=01.01.2024
|
||||
Output=output_tasks.csv
|
||||
ScanContent=true
|
||||
ScanTasks=false
|
||||
ExcludeID=1234
|
||||
IncludeID=60143e42-8d2a-4d96-8e5f-5217b5d824c6
|
||||
OutputContent=output_content.csv
|
||||
AccessToken=gAAAAABiY8Q8m1Ef6gd_UO3KIVjvSshJWGWaSGZQ8uqYe-Yn2eGH4kYQcPGxZ4g7iKt4w0L4-YFDEkLp9V6PkJHrB6kx0qb3hA==
|
||||
|
||||
[Options]
|
||||
Debug=false
|
||||
TestRun=false
|
||||
LoginTimeout=900
|
||||
[UserData]
|
||||
User=Борисов Иван Романович
|
||||
UserLogin=i.borisov
|
||||
Password=gAAAAABlI6fdFAeVM8UhkPYJaOP0pVhE6_GCF91EyY8kaFEyrgkr4SAm3lm4INIXPEQowAlvBr3M9uNtVwjqlGWIAiJPoKNeAg5sDOOP5lpZLYxy4HgdheY=
|
||||
|
Loading…
Reference in New Issue
Block a user