commit da0bfe21eca7bd5da932e4c2ce99fd6248b722ee
Author: IRBorisov <8611739+IRBorisov@users.noreply.github.com>
Date: Fri Jun 7 19:50:21 2024 +0300
Initial commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c516907
--- /dev/null
+++ b/.gitignore
@@ -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
diff --git a/VBAMake.txt b/VBAMake.txt
new file mode 100644
index 0000000..51cc9d2
--- /dev/null
+++ b/VBAMake.txt
@@ -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
\ No newline at end of file
diff --git a/script/LocalDevSetup.ps1 b/script/LocalDevSetup.ps1
new file mode 100644
index 0000000..661e428
--- /dev/null
+++ b/script/LocalDevSetup.ps1
@@ -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
\ No newline at end of file
diff --git a/script/databaseManifest.txt b/script/databaseManifest.txt
new file mode 100644
index 0000000..33e47b2
--- /dev/null
+++ b/script/databaseManifest.txt
@@ -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
\ No newline at end of file
diff --git a/script/run_menu.bat b/script/run_menu.bat
new file mode 100644
index 0000000..75aa02b
--- /dev/null
+++ b/script/run_menu.bat
@@ -0,0 +1,5 @@
+@echo off
+
+menu.exe
+
+pause
\ No newline at end of file
diff --git a/skeleton/!Контент.xlsm b/skeleton/!Контент.xlsm
new file mode 100644
index 0000000..8f635c9
Binary files /dev/null and b/skeleton/!Контент.xlsm differ
diff --git a/src/DB_Content.cls b/src/DB_Content.cls
new file mode 100644
index 0000000..639085b
--- /dev/null
+++ b/src/DB_Content.cls
@@ -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
diff --git a/src/DB_Workers.cls b/src/DB_Workers.cls
new file mode 100644
index 0000000..8615a04
--- /dev/null
+++ b/src/DB_Workers.cls
@@ -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
diff --git a/src/Declarations.bas b/src/Declarations.bas
new file mode 100644
index 0000000..58266f0
--- /dev/null
+++ b/src/Declarations.bas
@@ -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
+
+
diff --git a/src/DevHelper.bas b/src/DevHelper.bas
new file mode 100644
index 0000000..6efaa08
--- /dev/null
+++ b/src/DevHelper.bas
@@ -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
diff --git a/src/InfoConfig.cls b/src/InfoConfig.cls
new file mode 100644
index 0000000..0c5ca40
--- /dev/null
+++ b/src/InfoConfig.cls
@@ -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
diff --git a/src/IteratorAttribute.cls b/src/IteratorAttribute.cls
new file mode 100644
index 0000000..688872c
--- /dev/null
+++ b/src/IteratorAttribute.cls
@@ -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
diff --git a/src/IteratorCSVContent.cls b/src/IteratorCSVContent.cls
new file mode 100644
index 0000000..265a7a4
--- /dev/null
+++ b/src/IteratorCSVContent.cls
@@ -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
diff --git a/src/IteratorCSVTasks.cls b/src/IteratorCSVTasks.cls
new file mode 100644
index 0000000..14a09da
--- /dev/null
+++ b/src/IteratorCSVTasks.cls
@@ -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
diff --git a/src/IteratorContent.cls b/src/IteratorContent.cls
new file mode 100644
index 0000000..f2488e0
--- /dev/null
+++ b/src/IteratorContent.cls
@@ -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
diff --git a/src/Main.bas b/src/Main.bas
new file mode 100644
index 0000000..b366fc5
--- /dev/null
+++ b/src/Main.bas
@@ -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
diff --git a/src/MainImpl.bas b/src/MainImpl.bas
new file mode 100644
index 0000000..c5af364
--- /dev/null
+++ b/src/MainImpl.bas
@@ -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
diff --git a/src/z_UIMessages.bas b/src/z_UIMessages.bas
new file mode 100644
index 0000000..f1e94a3
--- /dev/null
+++ b/src/z_UIMessages.bas
@@ -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
diff --git a/src/z_UIRibbon.bas b/src/z_UIRibbon.bas
new file mode 100644
index 0000000..4c1a76f
--- /dev/null
+++ b/src/z_UIRibbon.bas
@@ -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
diff --git a/ui/.rels b/ui/.rels
new file mode 100644
index 0000000..3107a8e
--- /dev/null
+++ b/ui/.rels
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/ui/customUI.xml b/ui/customUI.xml
new file mode 100644
index 0000000..12ec4bc
--- /dev/null
+++ b/ui/customUI.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/webapi/.pylintrc b/webapi/.pylintrc
new file mode 100644
index 0000000..64be31a
--- /dev/null
+++ b/webapi/.pylintrc
@@ -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*(# )??$
+
+# 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
diff --git a/webapi/.vscode/launch.json b/webapi/.vscode/launch.json
new file mode 100644
index 0000000..6343906
--- /dev/null
+++ b/webapi/.vscode/launch.json
@@ -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"
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/webapi/.vscode/settings.json b/webapi/.vscode/settings.json
new file mode 100644
index 0000000..6a6333a
--- /dev/null
+++ b/webapi/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "python.analysis.typeCheckingMode": "off"
+}
\ No newline at end of file
diff --git a/webapi/configs/!AppData.ini b/webapi/configs/!AppData.ini
new file mode 100644
index 0000000..97ff90f
--- /dev/null
+++ b/webapi/configs/!AppData.ini
@@ -0,0 +1,14 @@
+[AppData]
+# Feel free to modify these parameters
+
+SlovnikExecutor=Фунтов Евгений Евгеньевич
+
+FilterTask=Статья;Микропонятие;Актуализация статьи из ЭВ БРЭ;Медиаконтент;Сюжет
+FilterDepartment=Редакция технологий и техники;Редакция энергетики, промышленности
+FilterSupervisor=
+FilterExecutor=
+FilterStatus=
+FilterObserver=
+
+# Папка для раскрытых текстов
+DocxFolder=./docx
diff --git a/webapi/configs/!Menu.ini b/webapi/configs/!Menu.ini
new file mode 100644
index 0000000..f1fd5b7
--- /dev/null
+++ b/webapi/configs/!Menu.ini
@@ -0,0 +1,4 @@
+[Launch]
+Output=output.csv
+Config=portal.ini
+User=iborisov
diff --git a/webapi/configs/!Options.ini b/webapi/configs/!Options.ini
new file mode 100644
index 0000000..ad0bcf3
--- /dev/null
+++ b/webapi/configs/!Options.ini
@@ -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
diff --git a/webapi/configs/akostyuk.ini b/webapi/configs/akostyuk.ini
new file mode 100644
index 0000000..a0aa94c
--- /dev/null
+++ b/webapi/configs/akostyuk.ini
@@ -0,0 +1,5 @@
+[UserData]
+User=Костюк Алексей Владимирович
+Input=!Контент.xlsm
+UserLogin=a.kostyuk
+Password=gAAAAABidSZD_kif_iE_2rjp_zF8o-VsHudEW7UZtT4TfTWr1D77V1q1edtoNp_4wWxjUyATu0KhU2SFIrqnvxCuMdZsLxuApvxqJuUvenDT7xEm2vkoffE=
diff --git a/webapi/configs/amarzoeva.ini b/webapi/configs/amarzoeva.ini
new file mode 100644
index 0000000..f3fd15e
--- /dev/null
+++ b/webapi/configs/amarzoeva.ini
@@ -0,0 +1,5 @@
+[UserData]
+User=Марзоева Анжелика Владиславовна
+Input=!Контент.xlsm
+UserLogin=a.marzoeva
+Password=gAAAAABjKxsXA3Cp9T4XKQ0zK0d3-3s1KEbtmEgK23mUIZSEgkJiD0LlTyoYz6S8xO4R_wBrPep4bNGcdzA88yAu-EQdaAnftjMSLHPyBDI-UzvWLexC_ro=
diff --git a/webapi/configs/anikitin.ini b/webapi/configs/anikitin.ini
new file mode 100644
index 0000000..764311d
--- /dev/null
+++ b/webapi/configs/anikitin.ini
@@ -0,0 +1,5 @@
+[UserData]
+User=Никитин Алексей Валерьевич
+Input=!Контент.xlsm
+UserLogin=a.nikitin
+Password=gAAAAABiMOMOoBGkME4YvhxuTYnipry3CKak2jfAMTHcxKP8Uhx_biFk2eBfPwIrcm--NTx4DoYogT_Bk70TAU52717R6A9ixHn4ES2qHHD5NnclOxv9Qdg=
diff --git a/webapi/configs/iborisov.ini b/webapi/configs/iborisov.ini
new file mode 100644
index 0000000..d8e6720
--- /dev/null
+++ b/webapi/configs/iborisov.ini
@@ -0,0 +1,5 @@
+[UserData]
+User=Борисов Иван Романович
+Input=!Контент.xlsm
+UserLogin=i.borisov
+Password=gAAAAABlI6fdFAeVM8UhkPYJaOP0pVhE6_GCF91EyY8kaFEyrgkr4SAm3lm4INIXPEQowAlvBr3M9uNtVwjqlGWIAiJPoKNeAg5sDOOP5lpZLYxy4HgdheY=
diff --git a/webapi/exporter.py b/webapi/exporter.py
new file mode 100644
index 0000000..277061c
--- /dev/null
+++ b/webapi/exporter.py
@@ -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)
diff --git a/webapi/menu.py b/webapi/menu.py
new file mode 100644
index 0000000..60c93ee
--- /dev/null
+++ b/webapi/menu.py
@@ -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
diff --git a/webapi/portal/__init__.py b/webapi/portal/__init__.py
new file mode 100644
index 0000000..e6713f3
--- /dev/null
+++ b/webapi/portal/__init__.py
@@ -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
+)
diff --git a/webapi/portal/bre_browser_options.py b/webapi/portal/bre_browser_options.py
new file mode 100644
index 0000000..dfa36be
--- /dev/null
+++ b/webapi/portal/bre_browser_options.py
@@ -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
diff --git a/webapi/portal/config.py b/webapi/portal/config.py
new file mode 100644
index 0000000..91abfdb
--- /dev/null
+++ b/webapi/portal/config.py
@@ -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
diff --git a/webapi/portal/crypto.py b/webapi/portal/crypto.py
new file mode 100644
index 0000000..c8c045b
--- /dev/null
+++ b/webapi/portal/crypto.py
@@ -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 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):]
diff --git a/webapi/portal/data_reader.py b/webapi/portal/data_reader.py
new file mode 100644
index 0000000..f40d2f1
--- /dev/null
+++ b/webapi/portal/data_reader.py
@@ -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()]
diff --git a/webapi/portal/document.py b/webapi/portal/document.py
new file mode 100644
index 0000000..534520f
--- /dev/null
+++ b/webapi/portal/document.py
@@ -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")
diff --git a/webapi/portal/electron_loading.py b/webapi/portal/electron_loading.py
new file mode 100644
index 0000000..8d78dc2
--- /dev/null
+++ b/webapi/portal/electron_loading.py
@@ -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
diff --git a/webapi/portal/info_models.py b/webapi/portal/info_models.py
new file mode 100644
index 0000000..7692320
--- /dev/null
+++ b/webapi/portal/info_models.py
@@ -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]
diff --git a/webapi/portal/portal.py b/webapi/portal/portal.py
new file mode 100644
index 0000000..bef70e4
--- /dev/null
+++ b/webapi/portal/portal.py
@@ -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
diff --git a/webapi/portal/process_typograph.js b/webapi/portal/process_typograph.js
new file mode 100644
index 0000000..de133c8
--- /dev/null
+++ b/webapi/portal/process_typograph.js
@@ -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);
+ }})
+
diff --git a/webapi/portal/selenium_wrapper.py b/webapi/portal/selenium_wrapper.py
new file mode 100644
index 0000000..b023780
--- /dev/null
+++ b/webapi/portal/selenium_wrapper.py
@@ -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()
diff --git a/webapi/portal/uploader.py b/webapi/portal/uploader.py
new file mode 100644
index 0000000..b927a8b
--- /dev/null
+++ b/webapi/portal/uploader.py
@@ -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)
diff --git a/webapi/pyinstaller_run.bat b/webapi/pyinstaller_run.bat
new file mode 100644
index 0000000..037c114
--- /dev/null
+++ b/webapi/pyinstaller_run.bat
@@ -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
\ No newline at end of file
diff --git a/webapi/requirements.txt b/webapi/requirements.txt
new file mode 100644
index 0000000..cf06165
Binary files /dev/null and b/webapi/requirements.txt differ
diff --git a/webapi/requirements_dev.txt b/webapi/requirements_dev.txt
new file mode 100644
index 0000000..b57e142
Binary files /dev/null and b/webapi/requirements_dev.txt differ
diff --git a/webapi/run_cardslot.py b/webapi/run_cardslot.py
new file mode 100644
index 0000000..a5e63cf
--- /dev/null
+++ b/webapi/run_cardslot.py
@@ -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())
diff --git a/webapi/run_loader.py b/webapi/run_loader.py
new file mode 100644
index 0000000..8dbc2f0
--- /dev/null
+++ b/webapi/run_loader.py
@@ -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))
diff --git a/webapi/run_metadata.py b/webapi/run_metadata.py
new file mode 100644
index 0000000..7f6f320
--- /dev/null
+++ b/webapi/run_metadata.py
@@ -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())
diff --git a/webapi/test.ini b/webapi/test.ini
new file mode 100644
index 0000000..8de6e04
--- /dev/null
+++ b/webapi/test.ini
@@ -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=
+