Initial commit

This commit is contained in:
IRBorisov 2024-06-07 19:50:21 +03:00
commit da0bfe21ec
52 changed files with 5271 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
__pycache__
~$*
*.spec
*.log
inputs
input.xlsx
output.csv
chromedriver.exe
Users.xlsx
webapi/\!Контент.xlsm
webapi/bin/
webapi/build/
webapi/*.csv
webapi/exporter.ini
webapi/exporter.exe
webapi/*.xlsm
webapi/docx/*.docx
test/
venv

39
VBAMake.txt Normal file
View File

@ -0,0 +1,39 @@
# == Properties Section ==
# configuration properties
# use .ini format to define properties
# mandatory properties: name, artifact_home, source_home
id = BRE
name = БРЭ
description = Интерфейс взаимодействия с Большой Российской Энциклопедией
artifact_home = BRE
source_home = BRE
install_home = D:\DEV\!WORK\output\BRE
%%
# === Build section ===
# Available commands:
# build LOCAL_MANIFEST
# copy LOCAL_SOURCE -> [LOCAL_ARTIFACT]
# save_as LOCAL_ARTIFACT -> LOCAL_ARTIFACT
# run LOCAL_SOURCE.bat
run webapi\pyinstaller_run.bat
build script\databaseManifest.txt
copy webapi\bin\exporter.exe -> exporter.exe
copy webapi\configs -> distr\configs
copy webapi\bin\exporter.exe -> distr\exporter.exe
copy webapi\bin\menu.exe -> distr\menu.exe
copy script\run_menu.bat -> distr\run_menu.bat
save_as !Контент.xlsm -> distr\!Контент.xlsm
%%
# === Install section ==
# Available commands:
# install LOCAL_ARTIFACT -> [INSTALL_PATH]
# add_template LOCAL_ARTIFACT -> [LOCAL_TEMPLATE]
# run LOCAL_ARTIFACT.bat <- [PARAMETERS]
# run APPLICATION <- [PARAMETERS]
# install !Контент.xlsm

32
script/LocalDevSetup.ps1 Normal file
View File

@ -0,0 +1,32 @@
# Create venv and install dependencies + imports
$webapi = Resolve-Path -Path "$PSScriptRoot\..\webapi"
$envPath = "$webapi\venv"
$python = "$envPath\Scripts\python.exe"
function LocalDevelopmentSetup() {
Set-Location $webapi
ClearPrevious
CreateEnv
InstallPips
}
function ClearPrevious() {
if (Test-Path -Path $envPath) {
Write-Host "Removing previous env: $envPath`n" -ForegroundColor DarkGreen
Remove-Item $envPath -Recurse -Force
}
}
function CreateEnv() {
Write-Host "Creating python env: $envPath`n" -ForegroundColor DarkGreen
& 'python' -m venv $envPath
}
function InstallPips() {
& $python -m pip install --upgrade pip
& $python -m pip install -r requirements_dev.txt
}
LocalDevelopmentSetup

View File

@ -0,0 +1,75 @@
# == Properties Section ==
# configuration properties
# use .ini format to define properties
# mandatory properties: name, artifact
name = !Контент.xlsm
artifact = !Контент.xlsm
%%
# === Imports Section ===
# Hierarchy of folders and files
# Use Tabulator to mark next level in hierarchy
# All folders are nested into SharedHome path
api
API_XLWrapper.cls
API_UserInteraction.cls
utility
ex_MSHook.bas
ex_VBA.bas
ex_DataPreparation.bas
excel
ex_Excel.bas
ui
CSE_ProgressBar.frm
CSE_ListSelector.frm
dev
DevTester.bas
%%
# === Source Code Section ==
# Hierarchy of folders and files
# Use Tabulator to mark next level in hierarchy
# All folders are nested into SourceHome path
src
DevHelper.bas
Declarations.bas
Main.bas
MainImpl.bas
z_UIRibbon.bas
z_UIMessages.bas
DB_Content.cls
DB_Workers.cls
InfoConfig.cls
IteratorAttribute.cls
IteratorContent.cls
IteratorCSVTasks.cls
IteratorCSVContent.cls
%%
# ===== UI Section =======
# Pairs of path to UI elements, use " -> " delimiter
# First component is a path relative to SourceHome\ui folders
# Second component is internal path inside project file
.rels -> _rels\.rels
customUI.xml -> customUI\customUI.xml
%%
# === References Section ===
# List dependencies in one of the formats
# global : GLOBAL_NAME
# guid : {REGISTERED_GUID}
# file : PATH_TO_LIBRARY
global : Scripting
global : MSForms
global : ADODB
global : IWshRuntimeLibrary

5
script/run_menu.bat Normal file
View File

@ -0,0 +1,5 @@
@echo off
menu.exe
pause

Binary file not shown.

126
src/DB_Content.cls Normal file
View File

@ -0,0 +1,126 @@
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "DB_Content"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
Private data_ As Excel.Worksheet
Private attributes_ As Excel.Worksheet
Public Function Init(iData As Excel.Worksheet, iAttributes As Excel.Worksheet)
Set data_ = iData
Set attributes_ = iAttributes
End Function
Public Function Attributes() As IteratorAttribute
Set Attributes = New IteratorAttribute
Call Attributes.Init(attributes_)
End Function
Public Function IBegin() As IteratorContent
Set IBegin = New IteratorContent
Call IBegin.Init(data_)
End Function
Public Function ILast() As IteratorContent
Set ILast = New IteratorContent
Call ILast.Init(data_)
Call ILast.GoLast
End Function
Public Function INew() As IteratorContent
Set INew = New IteratorContent
Call INew.Init(data_)
Call INew.GoLast
Call INew.Increment
End Function
Public Property Get Count() As Long
Count = ILast.row_ - IBegin.row_ + 1
End Property
Public Function FindTaskID(sID$) As IteratorContent
Dim iResult As IteratorContent: Set iResult = IBegin
If iResult.FindTaskID(sID) Then _
Set FindTaskID = iResult
End Function
Public Function FindContentName(sName$, sType$) As IteratorContent
Dim iResult As IteratorContent: Set iResult = IBegin
If iResult.FindContentName(sName, sType) Then _
Set FindContentName = iResult
End Function
Public Function EnsureDataVisible()
Call XLShowAllData(data_, bKeepColumns:=True)
Call XLShowAllData(attributes_)
End Function
Public Function ImportCSVTasks(iInput As IteratorCSVTasks)
Call iInput.GoFirst
Dim iOutput As IteratorContent
Do While Not iInput.IsDone
Set iOutput = FindPlaceFor(iInput.TaskID, iInput.ContentName, iInput.TaskType)
If Not iInput.IsCanceled Or iOutput.TaskType <> "" Then
Call iOutput.SyncCSVTasks(iInput)
End If
If CSE_ProgressBar.Visible Then _
Call CSE_ProgressBar.IncrementA
Call iInput.Increment
Loop
End Function
Public Function ImportCSVContent(iInput As IteratorCSVContent)
Call iInput.GoFirst
Dim iOutput As IteratorContent
Do While Not iInput.IsDone
Set iOutput = FindPlaceFor(iInput.TaskID)
Call iOutput.SyncCSVContent(iInput)
If CSE_ProgressBar.Visible Then _
Call CSE_ProgressBar.IncrementA
Call iInput.Increment
Loop
End Function
Public Function ImportDB(iData As DB_Content)
Dim iInput As IteratorContent: Set iInput = iData.IBegin()
Dim iOutput As IteratorContent
Do While Not iInput.IsDone
Set iOutput = FindPlaceFor(iInput.TaskID, iInput.ContentName, iInput.TaskType)
If Not iInput.IsCanceled Or iOutput.TaskType <> "" Then _
Call iOutput.SyncContent(iInput)
If CSE_ProgressBar.Visible Then _
Call CSE_ProgressBar.IncrementA
Call iInput.Increment
Loop
Call ImportAttributes(iData.Attributes)
End Function
' =======
Private Function FindPlaceFor(sID$, Optional sName$ = "", Optional sType$ = "") As IteratorContent
Dim iWhere As IteratorContent: Set iWhere = IBegin
If Not iWhere.FindTaskID(sID) Then _
If Not iWhere.FindContentName(sName, sType) Or iWhere.TaskID <> "" Then _
Call iWhere.GoEmpty
Set FindPlaceFor = iWhere
End Function
Private Function ImportAttributes(iInput As IteratorAttribute)
Dim iOutput As IteratorAttribute: Set iOutput = Attributes
Call iOutput.GoEmpty
Do While Not iInput.IsDone
Call iOutput.SyncWith(iInput)
Call iOutput.Increment
Call iInput.Increment
Loop
Call DeleteAttributeDuplicates
End Function
Private Function DeleteAttributeDuplicates()
End Function

28
src/DB_Workers.cls Normal file
View File

@ -0,0 +1,28 @@
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "DB_Workers"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
Private data_ As Excel.ListObject
Public Function Init(oTable As Excel.ListObject)
Set data_ = oTable
End Function
Public Function ExpandShortname(sShort$) As String
Dim dataRng As Excel.Range: Set dataRng = data_.Range
Dim nRow&
For nRow = 2 To dataRng.Rows.Count Step 1
If dataRng.Cells(nRow, 2) = sShort Then
ExpandShortname = dataRng.Cells(nRow, 1)
Exit Function
End If
Next nRow
ExpandShortname = sShort
End Function

82
src/Declarations.bas Normal file
View File

@ -0,0 +1,82 @@
Attribute VB_Name = "Declarations"
Option Private Module
Option Explicit
Public Const DATA_FIRST_ROW = 2
Public Const NO_CONTENT_PLACEHOLDER = "Íåò Êîíòåíòà"
Public Const NO_MEDIA_PLACEHOLDER = "Íåò Ìåäèà"
Public Const STATUS_CANCELED = "Îòìåíåíà"
Public Const BOOL_TEXT_YES = "Äà"
Public Const BOOL_TEXT_NO = "Íåò"
Public Const SHEET_CONTENT = "Êîíòåíò"
Public Const SHEET_OPTIONS = "Ñïðàâî÷íèêè"
Public Const SHEET_ATTRIBUTES = "Ïðèçíàêè"
Public Const SHEET_CONFIG = "Êîíôèã"
Public Const TABLE_WORKERS = "t_Person"
Public Const TABLE_MARKERS = "t_Markers"
Public Const TABLE_TAGS = "t_Tags"
Public Const URL_PREFIX_TASK = "https://rk.greatbook.ru/tasks/"
Public Const URL_PREFIX_CONTENT = "https://rk.greatbook.ru/widgets?link=task&id="
Public Const EXPORTER_CONFIG_FILE = "exporter.ini"
Public Const EXPORTER_EXECUTABLE = "exporter.exe"
Public Const PREFIX_IMMUTABLE = "Íåèçìåííûå "
Public Const SUFFIX_IMMUTABLE = " (áèáëèîãðàôèÿ+êîððåêòóðà+òðàíñêðèïöèÿ)"
Public Enum TUpdateStatus
T_UPD_UNDEF = 0
[_First] = 1
T_UPD_COMPLETE = 1
T_UPD_IGNORE = 2
T_UPD_AUTO = 3
T_UPD_ONCE = 4
T_UPD_ALWAYS = 5
[_Last] = 5
End Enum
Public Function InferContentFromTask(sTask$) As String
Dim sContent$: sContent = sTask
If sContent Like PREFIX_IMMUTABLE & "*" Then _
sContent = VBA.Right(sContent, VBA.Len(sContent) - VBA.Len(PREFIX_IMMUTABLE))
If sContent Like "*" & SUFFIX_IMMUTABLE Then _
sContent = VBA.Left(sContent, VBA.Len(sContent) - VBA.Len(SUFFIX_IMMUTABLE))
InferContentFromTask = sContent
End Function
Public Function UpdateStatusFromText(sText$) As TUpdateStatus
If sText = "çàâåðøåí" Then
UpdateStatusFromText = T_UPD_COMPLETE
ElseIf sText = "íèêîãäà" Then
UpdateStatusFromText = T_UPD_IGNORE
ElseIf sText = "àâòî" Then
UpdateStatusFromText = T_UPD_AUTO
ElseIf sText = "åäèíîæäû" Then
UpdateStatusFromText = T_UPD_ONCE
ElseIf sText = "âñåãäà" Then
UpdateStatusFromText = T_UPD_ALWAYS
Else
UpdateStatusFromText = T_UPD_UNDEF
End If
End Function
Public Function UpdateStatusToText(iStatus As TUpdateStatus) As String
Select Case iStatus
Case T_UPD_UNDEF: UpdateStatusToText = ""
Case T_UPD_COMPLETE: UpdateStatusToText = "çàâåðøåí"
Case T_UPD_IGNORE: UpdateStatusToText = "íèêîãäà"
Case T_UPD_AUTO: UpdateStatusToText = "àâòî"
Case T_UPD_ONCE: UpdateStatusToText = "åäèíîæäû"
Case T_UPD_ALWAYS: UpdateStatusToText = "âñåãäà"
Case Else: UpdateStatusToText = ""
End Select
End Function

20
src/DevHelper.bas Normal file
View File

@ -0,0 +1,20 @@
Attribute VB_Name = "DevHelper"
Option Explicit
Public Function Dev_PrepareSkeleton()
Call ClearData
End Function
Public Sub Dev_ManualRunTest()
Dim sSuite$: sSuite = "s_Database"
Dim sTest$: sTest = "t_RenameLawFile"
Dim sMsg$: sMsg = Dev_RunTestDebug(sSuite, sTest)
Debug.Print sMsg
Call MsgBox(sMsg)
End Sub
Public Function Dev_GetTestSuite(sName$) As Object
Select Case sName
' Case "s_ActiveStateExporter": Set Dev_GetTestSuite = New s_ActiveStateExporter
End Select
End Function

163
src/InfoConfig.cls Normal file
View File

@ -0,0 +1,163 @@
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "InfoConfig"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
Private Const S_CONFIG_KEY = 1
Private Const S_CONFIG_VALUE = 2
Private Const S_CONFIG_COMMENT = 3
Private Enum AppdataRows
[_First] = 2
R_APP_FILTER_TASK = 2
R_APP_FILTER_DEPARTMENT = 3
R_APP_FILTER_STATUS = 4
R_APP_FILTER_RESPONSIBLE = 5
R_APP_FILTER_SUPERVISOR = 6
R_APP_FILTER_EXECUTOR = 7
R_APP_FILTER_OBSERVER = 8
R_APP_FILTER_CREATED_BEGIN = 9
R_APP_FILTER_CREATED_END = 10
R_APP_FILTER_TARGET_BEGIN = 11
R_APP_FILTER_TARGET_END = 12
R_APP_OUTPUT = 13
R_APP_SCAN_TASKS = 14
R_APP_SCAN_CONTENT = 15
R_APP_OUTPUT_CONTENT = 16
R_APP_ACCESS_TOKEN = 17
[_Last] = 17
End Enum
Private Enum UserRows
[_First] = 19
R_USER_NAME = 19
R_USER_LOGIN = 20
R_USER_PASSWORD = 21
[_Last] = 21
End Enum
Private Enum OptionsRows
[_First] = 23
R_OPT_DEBUG = 23
R_OPT_TESTRUN = 24
R_OPT_TIMEOUT = 25
[_Last] = 25
End Enum
Private data_ As Excel.Worksheet
Private content_ As DB_Content
Public Function Init(iData As Excel.Worksheet, iContent As DB_Content)
Set data_ = iData
Set content_ = iContent
End Function
Public Function SetScanContent(bScanContent As Boolean)
data_.Cells(R_APP_SCAN_CONTENT, S_CONFIG_VALUE) = IIf(bScanContent, "true", "false")
End Function
Public Function SetScanTasks(bScanContent As Boolean)
data_.Cells(R_APP_SCAN_TASKS, S_CONFIG_VALUE) = IIf(bScanContent, "true", "false")
End Function
Public Function ConfigFilePath() As String
ConfigFilePath = data_.Parent.Path & "\" & EXPORTER_CONFIG_FILE
End Function
Public Function OutputFileTasks() As String
OutputFileTasks = data_.Parent.Path & "\" & data_.Cells(R_APP_OUTPUT, S_CONFIG_VALUE)
End Function
Public Function OutputFileContent() As String
OutputFileContent = data_.Parent.Path & "\" & data_.Cells(R_APP_OUTPUT_CONTENT, S_CONFIG_VALUE)
End Function
Public Function ScanContent() As Boolean
ScanContent = data_.Cells(R_APP_SCAN_CONTENT, S_CONFIG_VALUE)
End Function
Public Function ScanTasks() As Boolean
ScanTasks = data_.Cells(R_APP_SCAN_TASKS, S_CONFIG_VALUE)
End Function
Public Function CreateConfigFile()
Dim nRow&
Dim sValue$, sKey$
Dim iOut As New ADODB.Stream: iOut.Charset = "utf-8"
Call iOut.Open
Call iOut.WriteText("[AppData]", adWriteLine)
For nRow = AppdataRows.[_First] To AppdataRows.[_Last] Step 1
sKey = data_.Cells(nRow, S_CONFIG_KEY)
sValue = data_.Cells(nRow, S_CONFIG_VALUE)
Call iOut.WriteText(Fmt("{1}={2}", sKey, sValue), adWriteLine)
Next nRow
Call PrepareLists(iOut)
Call iOut.WriteText("[Options]", adWriteLine)
For nRow = OptionsRows.[_First] To OptionsRows.[_Last] Step 1
sKey = data_.Cells(nRow, S_CONFIG_KEY)
sValue = data_.Cells(nRow, S_CONFIG_VALUE)
Call iOut.WriteText(Fmt("{1}={2}", sKey, sValue), adWriteLine)
Next nRow
Call iOut.WriteText("[UserData]", adWriteLine)
For nRow = UserRows.[_First] To UserRows.[_Last] Step 1
sKey = data_.Cells(nRow, S_CONFIG_KEY)
sValue = data_.Cells(nRow, S_CONFIG_VALUE)
Call iOut.WriteText(Fmt("{1}={2}", sKey, sValue), adWriteLine)
Next nRow
Dim iOutNoBOM As New ADODB.Stream
iOutNoBOM.Type = adTypeBinary
Call iOutNoBOM.Open
iOut.Position = 3
Call iOut.CopyTo(iOutNoBOM)
Call iOutNoBOM.SaveToFile(ConfigFilePath, adSaveCreateOverWrite)
Call iOutNoBOM.Close
Call iOut.Close
End Function
Public Function DeleteConfigFile()
Call Kill(ConfigFilePath)
End Function
' ========
Private Function PrepareLists(iOut As ADODB.Stream) As String
Dim iExclude$: iExclude = ""
Dim iInclude$: iInclude = ""
Dim iContent As IteratorContent: Set iContent = content_.IBegin
Dim iStatus As TUpdateStatus
Do While Not iContent.IsDone
Call iContent.RecalculateStatus
If iContent.IsIgnored Then
If iExclude <> "" Then iExclude = iExclude & ";"
iExclude = iExclude & iContent.TaskID
End If
If iContent.NeedsUpdate Then
If iInclude <> "" Then iInclude = iInclude & ";"
iInclude = iInclude & iContent.TaskID
End If
Call iContent.Increment
Loop
Call iOut.WriteText(Fmt("{1}={2}", "ExcludeID", iExclude), adWriteLine)
Call iOut.WriteText(Fmt("{1}={2}", "IncludeID", iInclude), adWriteLine)
End Function

99
src/IteratorAttribute.cls Normal file
View File

@ -0,0 +1,99 @@
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "IteratorAttribute"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
' _A_ Attrtibutes structure
Private Enum AttributesStruct
[_First] = 1
S_A_CONTENT_NAME = 1
S_A_ATTRIBUTE = 2
S_A_VALUE = 3
S_A_INPUT_METHOD = 4
[_Last] = 4
End Enum
Public row_ As Long
Private data_ As Excel.Worksheet
Public Sub Init(target As Excel.Worksheet, Optional tRow& = DATA_FIRST_ROW)
Set data_ = target
row_ = tRow
End Sub
Public Function Increment(Optional inc& = 1)
If row_ + inc > 0 Then _
row_ = row_ + inc
End Function
Public Function GoFirst()
row_ = DATA_FIRST_ROW
End Function
Public Function GoLast()
row_ = GetLastRow
End Function
Public Function GoEmpty()
Call GoLast
Call Increment
End Function
Public Function IsDone() As Boolean
IsDone = row_ > GetLastRow
End Function
Public Function SyncWith(iInput As IteratorAttribute)
ContentName = iInput.ContentName
Attr = iInput.Attr
Value = iInput.Value
InputMethod = iInput.InputMethod
End Function
' ======== Property Get =========
Public Property Get ContentName() As String
ContentName = data_.Cells(row_, S_A_CONTENT_NAME)
End Property
Public Property Get Attr() As String
Attr = data_.Cells(row_, S_A_ATTRIBUTE)
End Property
Public Property Get Value() As String
Value = data_.Cells(row_, S_A_VALUE)
End Property
Public Property Get InputMethod() As String
InputMethod = data_.Cells(row_, S_A_INPUT_METHOD)
End Property
' ==== Property Let ====
Public Property Let ContentName(newVal$)
data_.Cells(row_, S_A_CONTENT_NAME) = newVal
End Property
Public Property Let Attr(newVal$)
data_.Cells(row_, S_A_ATTRIBUTE) = newVal
End Property
Public Property Let Value(newVal$)
data_.Cells(row_, S_A_VALUE) = newVal
End Property
Public Property Let InputMethod(newVal$)
data_.Cells(row_, S_A_INPUT_METHOD) = newVal
End Property
' =======
Private Function GetLastRow() As Long
GetLastRow = data_.Cells(data_.Rows.Count, 1).End(xlUp).Row
End Function

134
src/IteratorCSVContent.cls Normal file
View File

@ -0,0 +1,134 @@
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "IteratorCSVContent"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
' _I_ Input CSV structure
Private Enum InputStruct
[_First] = 1
S_I_TASK_ID = 1
S_I_BIBLIO_NAME = 2
S_I_CHANGE_SCORE = 3
S_I_DEPARTMENT = 4
S_I_RESPONSIBLE = 5
S_I_DEFINITION = 6
S_I_OBJECT_TYPE = 7
S_I_MARKERS = 8
S_I_TAGS = 9
S_I_SOURCE = 10
S_I_ELECTRON_BRE = 11
S_I_MAIN_PAGE = 12
S_I_IS_GENERAL = 13
S_I_ACTUALIZE_PERIOD = 14
S_I_AGE_RESTRICTION = 15
S_I_AUTHOR = 16
S_I_EDITOR = 17
[_Last] = 17
End Enum
Public row_ As Long
Private data_ As Excel.Worksheet
Public Sub Init(target As Excel.Worksheet, Optional tRow& = 1)
Set data_ = target
row_ = tRow
End Sub
Public Function Increment(Optional inc& = 1)
If row_ + inc > 0 Then _
row_ = row_ + inc
End Function
Public Function GoFirst()
row_ = 1
End Function
Public Function GoLast()
row_ = data_.Columns(S_I_TASK_ID).Find(vbNullString, LookAt:=xlWhole).Row - 1
End Function
Public Property Get CountRows() As Long
CountRows = data_.Columns(S_I_TASK_ID).Find(vbNullString, LookAt:=xlWhole).Row - 1
End Property
Public Function IsDone() As Boolean
IsDone = data_.Cells(row_, S_I_TASK_ID) = vbNullString
End Function
'===== Propertiy Get =====
Public Property Get TaskID() As String
TaskID = data_.Cells(row_, S_I_TASK_ID)
End Property
Public Property Get BiblioName() As String
BiblioName = data_.Cells(row_, S_I_BIBLIO_NAME)
End Property
Public Property Get ChangeScore() As String
ChangeScore = data_.Cells(row_, S_I_CHANGE_SCORE)
End Property
Public Property Get Department() As String
Department = data_.Cells(row_, S_I_DEPARTMENT)
End Property
Public Property Get Responsible() As String
Responsible = data_.Cells(row_, S_I_RESPONSIBLE)
End Property
Public Property Get Definition() As String
Definition = data_.Cells(row_, S_I_DEFINITION)
End Property
Public Property Get ObjectType() As String
ObjectType = data_.Cells(row_, S_I_OBJECT_TYPE)
End Property
Public Property Get Markers() As String
Markers = data_.Cells(row_, S_I_MARKERS)
End Property
Public Property Get Tags() As String
Tags = data_.Cells(row_, S_I_TAGS)
End Property
Public Property Get Source() As String
Source = data_.Cells(row_, S_I_SOURCE)
End Property
Public Property Get ElectronBre() As String
ElectronBre = data_.Cells(row_, S_I_ELECTRON_BRE)
End Property
Public Property Get MainPage() As String
MainPage = data_.Cells(row_, S_I_MAIN_PAGE)
End Property
Public Property Get IsGeneral() As String
IsGeneral = data_.Cells(row_, S_I_IS_GENERAL)
End Property
Public Property Get ActualizePeriod() As String
ActualizePeriod = data_.Cells(row_, S_I_ACTUALIZE_PERIOD)
End Property
Public Property Get AgeRestriction() As String
AgeRestriction = data_.Cells(row_, S_I_AGE_RESTRICTION)
End Property
Public Property Get Author() As String
Author = data_.Cells(row_, S_I_AUTHOR)
End Property
Public Property Get Editor() As String
Editor = data_.Cells(row_, S_I_EDITOR)
End Property

118
src/IteratorCSVTasks.cls Normal file
View File

@ -0,0 +1,118 @@
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "IteratorCSVTasks"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
' _I_ Input CSV structure
Private Enum InputStruct
[_First] = 1
S_I_TASK_TYPE = 1
S_I_STATUS = 2
S_I_CONTENT_NAME_DB = 3
S_I_SUPERVISOR = 4
S_I_EXECUTOR = 5
S_I_TARGET_DATE = 6
S_I_TASK_ID = 7
S_I_TASK_NAME = 8
S_I_PARENT_ID = 9
[_Last] = 9
End Enum
Public row_ As Long
Private data_ As Excel.Worksheet
Private workers_ As DB_Workers
Public Sub Init(target As Excel.Worksheet, dbWorkers As DB_Workers, Optional tRow& = 1)
Set data_ = target
row_ = tRow
Set workers_ = dbWorkers
End Sub
Public Function Increment(Optional inc& = 1)
If row_ + inc > 0 Then _
row_ = row_ + inc
End Function
Public Function GoFirst()
row_ = 1
End Function
Public Function GoLast()
row_ = data_.Columns(S_I_TASK_TYPE).Find(vbNullString, LookAt:=xlWhole).Row - 1
End Function
Public Property Get CountRows() As Long
CountRows = data_.Columns(S_I_TASK_TYPE).Find(vbNullString, LookAt:=xlWhole).Row - 1
End Property
Public Function IsDone() As Boolean
IsDone = data_.Cells(row_, S_I_TASK_TYPE) = vbNullString
End Function
Public Function IsCanceled() As Boolean
IsCanceled = Status = STATUS_CANCELED
End Function
Public Function HasContent() As Boolean
Dim sContent$: sContent = ContentNameDB
HasContent = sContent <> NO_CONTENT_PLACEHOLDER And sContent <> NO_MEDIA_PLACEHOLDER
End Function
'===== Propertiy Get =====
Public Property Get TaskType() As String
Dim sText$: sText = data_.Cells(row_, S_I_TASK_TYPE)
If sText = "ÑÒÀÒÜß ÈÇ ÝÂ ÁÐÝ" Then
TaskType = "Ñòàòüÿ èç ÝÂ ÁÐÝ"
Else
TaskType = CapitalizeFirstLetter(VBA.LCase(sText))
End If
End Property
Public Property Get Status() As String
Status = data_.Cells(row_, S_I_STATUS)
End Property
Public Property Get ContentName() As String
If Not HasContent Then
ContentName = InferContentFromTask(TaskName)
Else
ContentName = ContentNameDB
End If
End Property
Public Property Get ContentNameDB() As String
ContentNameDB = data_.Cells(row_, S_I_CONTENT_NAME_DB)
End Property
Public Property Get Supervisor() As String
Supervisor = workers_.ExpandShortname(data_.Cells(row_, S_I_SUPERVISOR))
End Property
Public Property Get Executor() As String
Executor = workers_.ExpandShortname(data_.Cells(row_, S_I_EXECUTOR))
End Property
Public Property Get TargetDate() As Long
TargetDate = VBA.CDate(data_.Cells(row_, S_I_TARGET_DATE))
End Property
Public Property Get TaskID() As String
TaskID = data_.Cells(row_, S_I_TASK_ID)
End Property
Public Property Get TaskName() As String
TaskName = data_.Cells(row_, S_I_TASK_NAME)
End Property
Public Property Get ParentID() As String
ParentID = data_.Cells(row_, S_I_PARENT_ID)
End Property

595
src/IteratorContent.cls Normal file
View File

@ -0,0 +1,595 @@
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "IteratorContent"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
' _C_ Content structure
Private Enum ContentStruct
[_First] = 1
S_C_TASK_TYPE = 1
S_C_STATUS = 2
S_C_CONTENT_NAME = 3
S_C_UPDATE_STATUS = 4
S_C_CHANGE_SCORE = 5
S_C_BIBLIO_NAME = 6
S_C_DEFINITION = 7
S_C_IS_IMMUTABLE = 8
S_C_OBJECT_TYPE = 9
S_C_MARKERS = 10
S_C_TAGS = 11
S_C_AUTHOR = 12
S_C_SUPERVISOR = 13
S_C_EXECUTOR = 14
S_C_EDITOR = 15
S_C_RESPONSIBLE = 16
S_C_DEPARTMENT = 17
S_C_TARGET_DATE = 18
S_C_SOURCE = 19
S_C_ELECTRON_BRE = 20
S_C_MAIN_PAGE = 21
S_C_IS_GENERAL = 22
S_C_ACTUALIZE_PERIOD = 23
S_C_AGE_RESTRICTION = 24
S_C_PRIORITY = 25
S_C_ARTICLE_TYPE = 26
S_C_DATE_EXCHANGE = 27
S_C_DATE_EES1 = 28
S_C_DATE_EX_TOOLS = 29
S_C_DATE_EES2 = 30
S_C_EXPERT = 31
S_C_CONTRACT = 32
S_C_COMMENT = 33
S_C_TASK_ID = 34
S_C_CONTENT_NAME_DB = 35
S_C_TASK_NAME = 36
S_C_PARENT_ID = 37
[_Last] = 37
End Enum
Public row_ As Long
Private data_ As Excel.Worksheet
Public Sub Init(target As Excel.Worksheet, Optional tRow& = DATA_FIRST_ROW)
Set data_ = target
row_ = tRow
End Sub
Public Function Increment(Optional inc& = 1)
If row_ + inc > 0 Then _
row_ = row_ + inc
End Function
Public Function GoFirst()
row_ = DATA_FIRST_ROW
End Function
Public Function GoLast()
row_ = GetLastRow
End Function
Public Function GoEmpty()
Call GoLast
Call Increment
End Function
Public Function IsDone() As Boolean
IsDone = row_ > GetLastRow
End Function
Public Function IsCanceled() As Boolean
IsCanceled = Status = STATUS_CANCELED
End Function
Public Function HasContent() As Boolean
Dim sContent$: sContent = ContentNameDB
HasContent = sContent <> NO_CONTENT_PLACEHOLDER And sContent <> NO_MEDIA_PLACEHOLDER
End Function
Public Function RemoveRow()
Call data_.Rows(row_).Delete
End Function
Public Function FindTaskID(sID$) As Boolean
If sID = "" Then _
Exit Function
Dim iFound As Excel.Range:
Set iFound = data_.Columns(S_C_TASK_ID).Find(sID, LookAt:=xlWhole)
FindTaskID = Not iFound Is Nothing
If FindTaskID Then _
row_ = iFound.Row
End Function
Public Function FindContentName(sName$, sType$) As Boolean
If sName = "" Then _
Exit Function
Dim iFound As Excel.Range:
Set iFound = data_.Columns(S_C_CONTENT_NAME).Find(sName, LookAt:=xlWhole)
If iFound Is Nothing Then _
Exit Function
If data_.Cells(iFound.Row, S_C_TASK_TYPE) <> sType Then _
Exit Function
row_ = iFound.Row
FindContentName = True
End Function
Public Function SyncCSVTasks(iInput As IteratorCSVTasks)
TaskType = iInput.TaskType
Status = iInput.Status
ContentName = iInput.ContentName
ContentNameDB = iInput.ContentNameDB
Supervisor = iInput.Supervisor
Executor = iInput.Executor
TargetDate = iInput.TargetDate
TaskID = iInput.TaskID
TaskName = iInput.TaskName
ParentID = iInput.ParentID
End Function
Public Function SyncCSVContent(iInput As IteratorCSVContent)
Call ValidateValue(S_C_BIBLIO_NAME, iInput.BiblioName)
Call ValidateValue(S_C_CHANGE_SCORE, iInput.ChangeScore)
Call ValidateValue(S_C_DEPARTMENT, iInput.Department)
Call ValidateValue(S_C_RESPONSIBLE, iInput.Responsible)
Call ValidateValue(S_C_EDITOR, iInput.Editor)
Call ValidateValue(S_C_DEFINITION, iInput.Definition)
Call ValidateValue(S_C_OBJECT_TYPE, iInput.ObjectType)
Call ValidateListValue(S_C_MARKERS, iInput.Markers)
Call ValidateListValue(S_C_TAGS, iInput.Tags)
Call ValidateValue(S_C_SOURCE, iInput.Source)
Call ValidateValue(S_C_ELECTRON_BRE, iInput.ElectronBre)
Call ValidateValue(S_C_MAIN_PAGE, iInput.MainPage)
Call ValidateValue(S_C_IS_GENERAL, iInput.IsGeneral)
Call ValidateValue(S_C_ACTUALIZE_PERIOD, iInput.ActualizePeriod)
Call ValidateValue(S_C_AGE_RESTRICTION, iInput.AgeRestriction)
Call ValidateValue(S_C_AUTHOR, iInput.Author)
If UpdateStatus = T_UPD_ONCE Then _
UpdateStatus = T_UPD_UNDEF
Call RecalculateStatus
End Function
Public Function SyncContent(iInput As IteratorContent)
TaskType = iInput.TaskType
Status = iInput.Status
ContentName = iInput.ContentName
Supervisor = iInput.Supervisor
Executor = iInput.Executor
TargetDate = iInput.TargetDate
IsImmutable = iInput.IsImmutable
IsMain = iInput.IsMain
IsGeneral = iInput.IsGeneral
IsBRE = iInput.IsBRE
UpdateStatus = iInput.UpdateStatus
If iInput.BiblioName <> "" Then BiblioName = iInput.BiblioName
If iInput.ChangeScore <> "" Then ChangeScore = iInput.ChangeScore
If iInput.Definition <> "" Then Definition = iInput.Definition
If iInput.ObjectType <> "" Then ObjectType = iInput.ObjectType
If iInput.Markers <> "" Then Markers = iInput.Markers
If iInput.Author <> "" Then Author = iInput.Author
If iInput.Tags <> "" Then Tags = iInput.Tags
If iInput.Editor <> "" Then Editor = iInput.Editor
If iInput.Responsible <> "" Then Responsible = iInput.Responsible
If iInput.Department <> "" Then Department = iInput.Department
If iInput.Source <> "" Then Source = iInput.Source
If iInput.ActualizePeriod <> "" Then ActualizePeriod = iInput.ActualizePeriod
If iInput.AgeRestriction <> "" Then AgeRestriction = iInput.AgeRestriction
If iInput.Priority <> "" Then Priority = iInput.Priority
If iInput.ArticleType <> "" Then ArticleType = iInput.ArticleType
If iInput.Expert <> "" Then Expert = iInput.Expert
If iInput.Contract <> "" Then Contract = iInput.Contract
If iInput.ContentNameDB <> "" Then ContentNameDB = iInput.ContentNameDB
If iInput.TaskID <> "" Then TaskID = iInput.TaskID
If iInput.TaskName <> "" Then TaskName = iInput.TaskName
If iInput.ParentID <> "" Then ParentID = iInput.ParentID
Call RecalculateStatus
End Function
Public Function HasBlanks() As Boolean
HasBlanks = True
If BiblioName = "" Then _
Exit Function
If Definition = "" Then _
Exit Function
If Markers = "" Then _
Exit Function
If ObjectType = "" Then _
Exit Function
If Supervisor = "" Then _
Exit Function
If Editor = "" Then _
Exit Function
HasBlanks = False
End Function
Public Function IsIgnored() As Boolean
Dim oldStatus As TUpdateStatus: oldStatus = UpdateStatus
IsIgnored = oldStatus = T_UPD_COMPLETE Or oldStatus = T_UPD_IGNORE
End Function
Public Function NeedsUpdate() As Boolean
Dim oldStatus As TUpdateStatus: oldStatus = UpdateStatus
NeedsUpdate = oldStatus = T_UPD_AUTO Or oldStatus = T_UPD_ONCE Or oldStatus = T_UPD_ALWAYS
End Function
Public Function RecalculateStatus() As TUpdateStatus
Dim oldStatus As TUpdateStatus: oldStatus = UpdateStatus
RecalculateStatus = oldStatus
If oldStatus = T_UPD_ONCE Or oldStatus = T_UPD_ALWAYS Or oldStatus = T_UPD_IGNORE Then _
Exit Function
If TaskID = "" Then
UpdateStatus = T_UPD_UNDEF
ElseIf ContentNameDB = "Íåò Êîíòåíòà" Or Status = "Îòìåíåíà" Then
UpdateStatus = T_UPD_UNDEF
ElseIf HasBlanks Then
UpdateStatus = T_UPD_AUTO
Else
UpdateStatus = T_UPD_COMPLETE
End If
RecalculateStatus = UpdateStatus
End Function
' ======== Property Get =========
Public Property Get TaskType() As String
TaskType = data_.Cells(row_, S_C_TASK_TYPE)
End Property
Public Property Get Status() As String
Status = data_.Cells(row_, S_C_STATUS)
End Property
Public Property Get ContentName() As String
ContentName = data_.Cells(row_, S_C_CONTENT_NAME)
End Property
Public Property Get UpdateStatus() As TUpdateStatus
UpdateStatus = UpdateStatusFromText(data_.Cells(row_, S_C_UPDATE_STATUS))
End Property
Public Property Get ChangeScore() As String
ChangeScore = data_.Cells(row_, S_C_CHANGE_SCORE)
End Property
Public Property Get BiblioName() As String
BiblioName = data_.Cells(row_, S_C_BIBLIO_NAME)
End Property
Public Property Get Definition() As String
Definition = data_.Cells(row_, S_C_DEFINITION)
End Property
Public Property Get IsImmutable() As Boolean
IsImmutable = data_.Cells(row_, S_C_IS_IMMUTABLE) = BOOL_TEXT_YES
End Property
Public Property Get ObjectType() As String
ObjectType = data_.Cells(row_, S_C_OBJECT_TYPE)
End Property
Public Property Get Markers() As String
Markers = data_.Cells(row_, S_C_MARKERS)
End Property
Public Property Get Tags() As String
Tags = data_.Cells(row_, S_C_TAGS)
End Property
Public Property Get Author() As String
Author = data_.Cells(row_, S_C_AUTHOR)
End Property
Public Property Get Supervisor() As String
Supervisor = data_.Cells(row_, S_C_SUPERVISOR)
End Property
Public Property Get Executor() As String
Executor = data_.Cells(row_, S_C_EXECUTOR)
End Property
Public Property Get Editor() As String
Editor = data_.Cells(row_, S_C_EDITOR)
End Property
Public Property Get Responsible() As String
Responsible = data_.Cells(row_, S_C_RESPONSIBLE)
End Property
Public Property Get Department() As String
Department = data_.Cells(row_, S_C_DEPARTMENT)
End Property
Public Property Get TargetDate() As String
TargetDate = data_.Cells(row_, S_C_TARGET_DATE)
End Property
Public Property Get Source() As String
Source = data_.Cells(row_, S_C_SOURCE)
End Property
Public Property Get IsBRE() As Boolean
IsBRE = data_.Cells(row_, S_C_ELECTRON_BRE) = BOOL_TEXT_YES
End Property
Public Property Get IsMain() As Boolean
IsMain = data_.Cells(row_, S_C_MAIN_PAGE) = BOOL_TEXT_YES
End Property
Public Property Get IsGeneral() As Boolean
IsGeneral = data_.Cells(row_, S_C_IS_GENERAL) = BOOL_TEXT_YES
End Property
Public Property Get ActualizePeriod() As String
ActualizePeriod = data_.Cells(row_, S_C_ACTUALIZE_PERIOD)
End Property
Public Property Get AgeRestriction() As String
AgeRestriction = data_.Cells(row_, S_C_AGE_RESTRICTION)
End Property
Public Property Get Priority() As String
Priority = data_.Cells(row_, S_C_PRIORITY)
End Property
Public Property Get ArticleType() As String
ArticleType = data_.Cells(row_, S_C_ARTICLE_TYPE)
End Property
Public Property Get DateExchange() As String
DateExchange = data_.Cells(row_, S_C_DATE_EXCHANGE)
End Property
Public Property Get DateEES1() As String
DateEES1 = data_.Cells(row_, S_C_DATE_EES1)
End Property
Public Property Get DateTools() As String
DateTools = data_.Cells(row_, S_C_DATE_EX_TOOLS)
End Property
Public Property Get DateEES2() As String
DateEES2 = data_.Cells(row_, S_C_DATE_EES2)
End Property
Public Property Get Expert() As String
Expert = data_.Cells(row_, S_C_EXPERT)
End Property
Public Property Get Contract() As String
Contract = data_.Cells(row_, S_C_CONTRACT)
End Property
Public Property Get Comment() As String
Comment = data_.Cells(row_, S_C_COMMENT)
End Property
Public Property Get TaskID() As String
TaskID = data_.Cells(row_, S_C_TASK_ID)
End Property
Public Property Get ContentNameDB() As String
ContentNameDB = data_.Cells(row_, S_C_CONTENT_NAME_DB)
End Property
Public Property Get TaskName() As String
TaskName = data_.Cells(row_, S_C_TASK_NAME)
End Property
Public Property Get ParentID() As String
ParentID = data_.Cells(row_, S_C_PARENT_ID)
End Property
' ==== Property Let ====
Public Property Let TaskID(newVal$)
Dim oldVal$: oldVal = data_.Cells(row_, S_C_TASK_ID)
data_.Cells(row_, S_C_TASK_ID) = newVal
If newVal <> "" And (oldVal <> newVal Or data_.Cells(row_, S_C_TASK_TYPE).Hyperlinks.Count = 0) Then
Call XLUpdateHyperlink(data_.Cells(row_, S_C_TASK_TYPE), URL_PREFIX_TASK & newVal)
If HasContent Then
Call XLUpdateHyperlink(data_.Cells(row_, S_C_CONTENT_NAME), URL_PREFIX_CONTENT & newVal)
Else
Call data_.Cells(row_, S_C_CONTENT_NAME).Hyperlinks.Delete
End If
End If
End Property
Public Property Let TaskType(newVal$)
data_.Cells(row_, S_C_TASK_TYPE) = newVal
End Property
Public Property Let Status(newVal$)
data_.Cells(row_, S_C_STATUS) = newVal
End Property
Public Property Let ContentName(newVal$)
data_.Cells(row_, S_C_CONTENT_NAME) = newVal
End Property
Public Property Let UpdateStatus(newVal As TUpdateStatus)
data_.Cells(row_, S_C_UPDATE_STATUS) = UpdateStatusToText(newVal)
End Property
Public Property Let ChangeScore(newVal$)
data_.Cells(row_, S_C_CHANGE_SCORE) = newVal
End Property
Public Property Let BiblioName(newVal$)
data_.Cells(row_, S_C_BIBLIO_NAME) = newVal
End Property
Public Property Let Definition(newVal$)
data_.Cells(row_, S_C_DEFINITION) = newVal
End Property
Public Property Let IsImmutable(newVal As Boolean)
data_.Cells(row_, S_C_IS_IMMUTABLE) = IIf(newVal, BOOL_TEXT_YES, BOOL_TEXT_NO)
End Property
Public Property Let ObjectType(newVal$)
data_.Cells(row_, S_C_OBJECT_TYPE) = newVal
End Property
Public Property Let Markers(newVal$)
data_.Cells(row_, S_C_MARKERS) = newVal
End Property
Public Property Let Tags(newVal$)
data_.Cells(row_, S_C_TAGS) = newVal
End Property
Public Property Let Author(newVal$)
data_.Cells(row_, S_C_AUTHOR) = newVal
End Property
Public Property Let Supervisor(newVal$)
data_.Cells(row_, S_C_SUPERVISOR) = newVal
End Property
Public Property Let Executor(newVal$)
data_.Cells(row_, S_C_EXECUTOR) = newVal
End Property
Public Property Let Editor(newVal$)
data_.Cells(row_, S_C_EDITOR) = newVal
End Property
Public Property Let Responsible(newVal$)
data_.Cells(row_, S_C_RESPONSIBLE) = newVal
End Property
Public Property Let Department(newVal$)
data_.Cells(row_, S_C_DEPARTMENT) = newVal
End Property
Public Property Let TargetDate(newVal$)
data_.Cells(row_, S_C_TARGET_DATE) = newVal
End Property
Public Property Let Source(newVal$)
data_.Cells(row_, S_C_SOURCE) = newVal
End Property
Public Property Let IsBRE(newVal As Boolean)
data_.Cells(row_, S_C_ELECTRON_BRE) = IIf(newVal, BOOL_TEXT_YES, BOOL_TEXT_NO)
End Property
Public Property Let IsMain(newVal As Boolean)
data_.Cells(row_, S_C_MAIN_PAGE) = IIf(newVal, BOOL_TEXT_YES, BOOL_TEXT_NO)
End Property
Public Property Let IsGeneral(newVal As Boolean)
data_.Cells(row_, S_C_IS_GENERAL) = IIf(newVal, BOOL_TEXT_YES, BOOL_TEXT_NO)
End Property
Public Property Let ActualizePeriod(newVal$)
data_.Cells(row_, S_C_ACTUALIZE_PERIOD) = newVal
End Property
Public Property Let AgeRestriction(newVal$)
data_.Cells(row_, S_C_AGE_RESTRICTION) = newVal
End Property
Public Property Let Priority(newVal$)
data_.Cells(row_, S_C_PRIORITY) = newVal
End Property
Public Property Let ArticleType(newVal$)
data_.Cells(row_, S_C_ARTICLE_TYPE) = newVal
End Property
Public Property Let DateExchange(newVal$)
data_.Cells(row_, S_C_DATE_EXCHANGE) = newVal
End Property
Public Property Let DateEES1(newVal$)
data_.Cells(row_, S_C_DATE_EES1) = newVal
End Property
Public Property Let DateTools(newVal$)
data_.Cells(row_, S_C_DATE_EX_TOOLS) = newVal
End Property
Public Property Let DateEES2(newVal$)
data_.Cells(row_, S_C_DATE_EES2) = newVal
End Property
Public Property Let Expert(newVal$)
data_.Cells(row_, S_C_EXPERT) = newVal
End Property
Public Property Let Contract(newVal$)
data_.Cells(row_, S_C_CONTRACT) = newVal
End Property
Public Property Let ContentNameDB(newVal$)
data_.Cells(row_, S_C_CONTENT_NAME_DB) = newVal
End Property
Public Property Let TaskName(newVal$)
data_.Cells(row_, S_C_TASK_NAME) = newVal
If data_.Cells(row_, S_C_IS_IMMUTABLE) = "" Then
If newVal Like "Íåèçìåííûå *" Then
data_.Cells(row_, S_C_IS_IMMUTABLE) = BOOL_TEXT_YES
Else
data_.Cells(row_, S_C_IS_IMMUTABLE) = BOOL_TEXT_NO
End If
End If
End Property
Public Property Let ParentID(newVal$)
data_.Cells(row_, S_C_PARENT_ID) = newVal
End Property
' =======
Private Function GetLastRow() As Long
GetLastRow = data_.Cells(data_.Rows.Count, 1).End(xlUp).Row
End Function
Private Function ColorCell(nColumn&, nColor&)
data_.Cells(row_, nColumn).Interior.Color = nColor
End Function
Private Function ValidateValue(nColumn&, portalValue$)
Dim sValue$: sValue = data_.Cells(row_, nColumn)
If sValue = portalValue Then _
Exit Function
If sValue = "" Then
data_.Cells(row_, nColumn) = portalValue
Exit Function
End If
If portalValue = "" Then
Call ColorCell(nColumn, RGB(142, 169, 219))
Else
Call ColorCell(nColumn, RGB(255, 151, 151))
End If
End Function
Private Function ValidateListValue(nColumn&, portalValue$)
Dim sValue$: sValue = data_.Cells(row_, nColumn)
If CheckListsEqual(sValue, portalValue) Then _
Exit Function
If sValue = "" Then
data_.Cells(row_, nColumn) = portalValue
Exit Function
End If
If portalValue = "" Then
Call ColorCell(nColumn, RGB(142, 169, 219))
Else
Call ColorCell(nColumn, RGB(255, 151, 151))
End If
End Function

152
src/Main.bas Normal file
View File

@ -0,0 +1,152 @@
Attribute VB_Name = "Main"
Option Explicit
Public Sub RunImportCSV()
Dim sFile$: sFile = UserInteraction.PromptFileFilter(ThisWorkbook.Path, _
sDescription:="Âûãðóçêà ïîðòàëà CSV", _
sFilter:="*.csv")
If sFile = vbNullString Then _
Exit Sub
If Not ProcessCSV(sFile) Then _
Exit Sub
Call UserInteraction.ShowMessage(IM_IMPORT_SUCCESS)
End Sub
Public Sub RunImportDB()
Dim sFile$: sFile = UserInteraction.PromptFileFilter(ThisWorkbook.Path, _
sDescription:="Òàáëèöà Excel", _
sFilter:="*.xlsx;*.xls;*.xlsm")
If sFile = vbNullString Then _
Exit Sub
Dim xlInput As New API_XLWrapper: Call xlInput.SetApplication(ThisWorkbook.Application)
If xlInput.OpenDocument(sFile, bReadOnly:=True) Is Nothing Then
Call UserInteraction.ShowMessage(EM_FILE_CANNOT_OPEN, sFile)
Exit Sub
End If
Call xlInput.PauseUI
Dim iInput As New DB_Content: Call iInput.Init(xlInput.Document.Sheets(SHEET_CONTENT), xlInput.Document.Worksheets(SHEET_ATTRIBUTES))
Call ImportDataFromDB(iInput, AccessContent)
Call xlInput.ResumeUI
Call xlInput.ReleaseDocument
Call UserInteraction.ShowMessage(IM_IMPORT_SUCCESS)
End Sub
Public Sub RunEditConfig()
Call ThisWorkbook.Worksheets(SHEET_CONFIG).Activate
End Sub
Public Sub RunUpdateTasks()
Dim iConfig As InfoConfig: Set iConfig = AccessConfig
Call iConfig.SetScanTasks(True)
Call iConfig.SetScanContent(False)
Call ExecuteUpdateRequest(iConfig)
End Sub
Public Sub RunUpdateContent()
Dim iConfig As InfoConfig: Set iConfig = AccessConfig
Call iConfig.SetScanTasks(False)
Call iConfig.SetScanContent(True)
Call ExecuteUpdateRequest(iConfig)
End Sub
Public Sub RunUpdatePortal()
Dim iConfig As InfoConfig: Set iConfig = AccessConfig
Call iConfig.SetScanTasks(True)
Call iConfig.SetScanContent(True)
Call ExecuteUpdateRequest(iConfig)
End Sub
Public Sub RunClearData()
Call ClearData
Call UserInteraction.ShowMessage(IM_DATA_DELETED)
End Sub
Public Sub RunUnstuck()
Dim uiWrap As New API_XLWrapper: Call uiWrap.SetDocument(ThisWorkbook)
Call uiWrap.ResumeUI
End Sub
Public Sub RunInputMarks()
Dim iTarget As Excel.Range: Set iTarget = Excel.Selection.Cells(1, 1)
Call CSE_ListSelector.Init(ThisWorkbook.Worksheets(SHEET_OPTIONS).ListObjects(TABLE_MARKERS))
Call CSE_ListSelector.Show
If CSE_ListSelector.isCanceled_ Then _
Exit Sub
iTarget = CSE_ListSelector.GetSelectedStr
Call Unload(CSE_ListSelector)
End Sub
Public Sub RunInputTags()
Dim iTarget As Excel.Range: Set iTarget = Excel.Selection.Cells(1, 1)
Call CSE_ListSelector.Init(ThisWorkbook.Worksheets(SHEET_OPTIONS).ListObjects(TABLE_TAGS))
Call CSE_ListSelector.Show
If CSE_ListSelector.isCanceled_ Then _
Exit Sub
iTarget = CSE_ListSelector.GetSelectedStr
Call Unload(CSE_ListSelector)
End Sub
' =======
Private Function ProcessCSV(sFile$) As Boolean
ProcessCSV = False
Dim dataIn As Excel.Worksheet: Set dataIn = ThisWorkbook.Worksheets.Add
With dataIn.QueryTables.Add(Connection:="TEXT;" & sFile, Destination:=dataIn.Cells(1, 1))
.TextFileParseType = xlDelimited
.TextFileCommaDelimiter = True
.TextFilePlatform = 65001 ' UTF-8
.Refresh
End With
Dim sID$: sID = dataIn.Cells(1, 1)
If sID <> "" Then
If VBA.Left(sID, 1) Like "[0-9a-f]" Then
Dim iContent As New IteratorCSVContent: Call iContent.Init(dataIn)
Call ImportContentFromCSV(iContent, AccessContent)
Else
Dim iTasks As New IteratorCSVTasks: Call iTasks.Init(dataIn, AccessWorkers)
Call ImportTasksFromCSV(iTasks, AccessContent)
End If
End If
Dim bAlerts As Boolean: bAlerts = Excel.Application.DisplayAlerts
Excel.Application.DisplayAlerts = False
Call dataIn.QueryTables(1).Delete
Call dataIn.Delete
Excel.Application.DisplayAlerts = bAlerts
ProcessCSV = True
End Function
Private Function ExecuteUpdateRequest(iConfig As InfoConfig)
Call iConfig.CreateConfigFile
Dim bScanPortal As Boolean: bScanPortal = PortalUpdate(iConfig)
' Call iConfig.DeleteConfigFile
If Not bScanPortal Then _
Exit Function
Dim bProcessTasks As Boolean: bProcessTasks = True
If iConfig.ScanTasks Then
Dim sFile$: sFile = iConfig.OutputFileTasks
bProcessTasks = ProcessCSV(sFile)
' Call Kill(sFile)
End If
Dim bProcessContent As Boolean: bProcessContent = True
If iConfig.ScanContent Then
sFile = iConfig.OutputFileContent
bProcessContent = ProcessCSV(sFile)
' Call Kill(sFile)
End If
If bProcessTasks And bProcessContent Then _
Call UserInteraction.ShowMessage(IM_IMPORT_SUCCESS)
End Function

143
src/MainImpl.bas Normal file
View File

@ -0,0 +1,143 @@
Attribute VB_Name = "MainImpl"
Option Private Module
Option Explicit
Public Function AccessContent() As DB_Content
Static s_Content As DB_Content
If s_Content Is Nothing Then
Set s_Content = New DB_Content
Call s_Content.Init(ThisWorkbook.Worksheets(SHEET_CONTENT), ThisWorkbook.Worksheets(SHEET_ATTRIBUTES))
End If
Set AccessContent = s_Content
End Function
Public Function AccessWorkers() As DB_Workers
Static s_Workerks As DB_Workers
If s_Workerks Is Nothing Then
Set s_Workerks = New DB_Workers
Call s_Workerks.Init(ThisWorkbook.Worksheets(SHEET_OPTIONS).ListObjects(TABLE_WORKERS))
End If
Set AccessWorkers = s_Workerks
End Function
Public Function AccessConfig() As InfoConfig
Set AccessConfig = New InfoConfig
Call AccessConfig.Init(ThisWorkbook.Worksheets(SHEET_CONFIG), AccessContent)
End Function
Public Function ClearData()
Call XLShowAllData(ThisWorkbook.Sheets(SHEET_CONTENT))
Call ThisWorkbook.Sheets(SHEET_CONTENT).UsedRange.Offset(1, 0).Rows.EntireRow.Delete
Call XLShowAllData(ThisWorkbook.Sheets(SHEET_ATTRIBUTES))
Call ThisWorkbook.Sheets(SHEET_ATTRIBUTES).UsedRange.Offset(1, 0).Rows.EntireRow.Delete
End Function
Public Function ImportTasksFromCSV(iInput As IteratorCSVTasks, iOutput As DB_Content)
Call CSE_ProgressBar.Init("Çàãðóçêà çàäà÷è èç CSV", maxVal:=iInput.CountRows)
Call CSE_ProgressBar.ShowModeless
Call iOutput.EnsureDataVisible
Call iOutput.ImportCSVTasks(iInput)
Call Unload(CSE_ProgressBar)
End Function
Public Function ImportContentFromCSV(iInput As IteratorCSVContent, iOutput As DB_Content)
Call CSE_ProgressBar.Init("Çàãðóçêà ìåòàäàííûõ èç CSV", maxVal:=iInput.CountRows)
Call CSE_ProgressBar.ShowModeless
Call iOutput.EnsureDataVisible
Call iOutput.ImportCSVContent(iInput)
Call Unload(CSE_ProgressBar)
End Function
Public Function ImportDataFromDB(iInput As DB_Content, iOutput As DB_Content)
Call CSE_ProgressBar.Init("Çàãðóçêà áàçû", maxVal:=iInput.Count)
Call CSE_ProgressBar.ShowModeless
Call iInput.EnsureDataVisible
Call iOutput.EnsureDataVisible
Call iOutput.ImportDB(iInput)
Call Unload(CSE_ProgressBar)
End Function
Public Function PortalUpdate(iConfig As InfoConfig) As Boolean
PortalUpdate = False
Dim sExec$: sExec = EXPORTER_EXECUTABLE & " " & EXPORTER_CONFIG_FILE
Dim fso As New Scripting.FileSystemObject
If Not fso.FileExists(ThisWorkbook.Path & "\" & EXPORTER_EXECUTABLE) Then
Call UserInteraction.ShowMessage(EM_MISSING_EXEC, EXPORTER_EXECUTABLE)
Exit Function
End If
Dim iShell As New WshShell
iShell.CurrentDirectory = ThisWorkbook.Path
On Error GoTo REPORT_EXEC
If iShell.Run(sExec, WaitOnReturn:=True) <> 0 Then
REPORT_EXEC:
Call UserInteraction.ShowMessage(EM_CANNOT_EXEC, sExec)
Exit Function
End If
On Error GoTo 0
Dim sOutput1$: sOutput1 = iConfig.OutputFileTasks
If Not fso.FileExists(sOutput1) Then
Call UserInteraction.ShowMessage(EM_MISSING_FILE, sExec)
Exit Function
End If
Dim sOutput2$: sOutput2 = iConfig.OutputFileContent
If iConfig.ScanContent And Not fso.FileExists(sOutput2) Then
Call UserInteraction.ShowMessage(EM_MISSING_FILE, sExec)
Exit Function
End If
PortalUpdate = True
End Function
Public Function CheckListsEqual(list1$, list2$) As Boolean
CheckListsEqual = False
Dim items1 As Variant: items1 = VBA.Split(list1, ";")
Dim items2 As Variant: items2 = VBA.Split(list2, ";")
If UBound(items1) <> UBound(items2) Then _
Exit Function
Dim it1 As Variant
Dim it2 As Variant
Dim flagExists As Boolean
For Each it1 In items1
flagExists = False
For Each it2 In items2
If it1 = it2 Then
flagExists = True
Exit For
End If
Next it2
If Not flagExists Then _
Exit Function
Next it1
For Each it2 In items2
flagExists = False
For Each it1 In items1
If it1 = it2 Then
flagExists = True
Exit For
End If
Next it1
If Not flagExists Then _
Exit Function
Next it2
CheckListsEqual = True
End Function

61
src/z_UIMessages.bas Normal file
View File

@ -0,0 +1,61 @@
Attribute VB_Name = "z_UIMessages"
' Ìîäóëü âûâîäà ñîîáùåíèé è âçàèìîäåéñòâèÿ ñ ïîëüçîâàòåëåì
Option Explicit
Public Enum MsgCode
MSG_OK = 0
EM_FILE_CANNOT_OPEN
EM_CANNOT_EXEC
EM_MISSING_FILE
EM_ITEM_EXISTS
EM_MISSING_EXEC
IM_IMPORT_SUCCESS
IM_DATA_DELETED
' QM_MERGE_WARNING
End Enum
Private g_UI As API_UserInteraction
Public Function UserInteraction() As API_UserInteraction
If g_UI Is Nothing Then _
Set g_UI = New API_UserInteraction
Set UserInteraction = g_UI
End Function
Public Function SetUserInteraction(newUI As API_UserInteraction)
Set g_UI = newUI
End Function
Public Function UIShowMessage(theCode As MsgCode, ParamArray params() As Variant)
Dim unwrapped As Variant: unwrapped = params
unwrapped = FixForwardedParams(unwrapped)
Select Case theCode
Case EM_FILE_CANNOT_OPEN: Call MsgBox(Fmt("Íå óäàëîñü îòêðûòü ôàéë {1}", unwrapped), vbExclamation)
Case EM_CANNOT_EXEC: Call MsgBox(Fmt("Íå óäàëîñü âûïîëíèòü êîìàíäó" & vbNewLine & """{1}""", unwrapped), vbExclamation)
Case EM_MISSING_FILE: Call MsgBox(Fmt("Íå íàéäåí ôàéë {1}", unwrapped), vbExclamation)
Case EM_ITEM_EXISTS: Call MsgBox(Fmt("Ýëåìåíò óæå ïðèñóòñòâóåò â ñïèñêå: {1}", unwrapped), vbExclamation)
Case EM_MISSING_EXEC: Call MsgBox(Fmt("Íå íàéäåí èñïîëíÿåìûé ôàéë Python: {1}", unwrapped), vbExclamation)
Case IM_IMPORT_SUCCESS: Call MsgBox("Èìïîðò çàâåðøåí óñïåøíî", vbInformation)
Case IM_DATA_DELETED: Call MsgBox("Äàííûå óäàëåíû", vbInformation)
Case Else: Call MsgBox("Íåâåðíûé êîä ñîîáùåíèÿ", vbCritical)
End Select
End Function
Public Function UIAskQuestion(theCode As MsgCode, ParamArray params() As Variant) As Boolean
Dim unwrapped As Variant: unwrapped = params
unwrapped = FixForwardedParams(unwrapped)
Dim answer&: answer = vbNo
Select Case theCode
' Case QM_DELETE_LAW: answer = MsgBox(Fmt("Âíèìàíèå! Âñå äàííûå äîêóìåíòà {1} áóäóò óäàëåíû!" & vbNewLine & "Ïðîäîëæèòü?", unwrapped), vbYesNo + vbQuestion)
Case Else: Call MsgBox("Íåâåðíûé êîä ñîîáùåíèÿ", vbCritical)
End Select
UIAskQuestion = answer = vbYes
End Function

20
src/z_UIRibbon.bas Normal file
View File

@ -0,0 +1,20 @@
Attribute VB_Name = "z_UIRibbon"
Option Explicit
Public Sub OnRibbonBtn(iControl As IRibbonControl)
Select Case iControl.ID
Case "ImportCSV": Call RunImportCSV
Case "ImportDB": Call RunImportDB
Case "EditConfig": Call RunEditConfig
Case "UpdateTasks": Call RunUpdateTasks
Case "UpdateContent": Call RunUpdateContent
Case "UpdatePortal": Call RunUpdatePortal
Case "InputMarks": Call RunInputMarks
Case "InputTags": Call RunInputTags
Case "Unstuck": Call RunUnstuck
Case "ClearData": Call RunClearData
End Select
End Sub

2
ui/.rels Normal file
View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/><Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/><Relationship Id="rId5" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId4" Type="http://schemas.microsoft.com/office/2006/relationships/ui/extensibility" Target="customUI/customUI.xml"/></Relationships>

66
ui/customUI.xml Normal file
View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" >
<ribbon>
<tabs>
<tab id="BRE" label="БРЭ">
<group id="Actions" label="Работа с данными">
<button id="ImportCSV" size="large"
label="Импорт CSV"
supertip="Импорт данных из выгрузки Портала"
imageMso="GetPowerQueryDataFromCsv"
onAction="OnRibbonBtn"/>
<button id="ImportDB" size="large"
label="Импорт базы"
supertip="Импорт данных из другой базы"
imageMso="FormExportToExcel"
onAction="OnRibbonBtn"/>
<button id="EditConfig" size="large"
label="Настройки импорта"
supertip="Настройка фильтров импорта данных с портала"
imageMso="GroupDocumentLibraryManage"
onAction="OnRibbonBtn"/>
<button id="UpdateTasks" size="large"
label="Запросить задачи"
supertip="Импорт данных о задачах с портала (5-10 минут)"
imageMso="BlogCategoriesRefresh"
onAction="OnRibbonBtn"/>
<button id="UpdateContent" size="large"
label="Запросить контент"
supertip="Импорт данных о контенте по задачам, отмеченным в Excel"
imageMso="AnimationDuration"
onAction="OnRibbonBtn"/>
<button id="UpdatePortal" size="large"
label="Обновить все"
supertip="Менеджер импорта данных из портала"
imageMso="AccessRefreshAllLists"
onAction="OnRibbonBtn"/>
</group>
<group id="Edit" label="Редактирование" >
<button id="InputMarks" size="large"
label="Словник"
supertip="Ввод списка из словника в текущую ячейку"
imageMso="ActiveXLabel"
onAction="OnRibbonBtn"/>
<button id="InputTags" size="large"
label="Метки"
supertip="Ввод списка из меток в текущую ячейку"
imageMso="DataGraphicIconSet"
onAction="OnRibbonBtn"/>
</group>
<group id="Debug" label="Отладка" >
<button id="Unstuck" size="large"
label="Восстановить"
supertip="Включить интерфейс, отключенный в результате некорректного завершения макроса"
imageMso="GroupListManage"
onAction="OnRibbonBtn"/>
<button id="ClearData" size="large"
label="Очистить данные"
supertip="Удалить ВСЕ данные из книги"
imageMso="Clear"
onAction="OnRibbonBtn"/>
</group>
</tab>
</tabs>
</ribbon>
</customUI>

634
webapi/.pylintrc Normal file
View File

@ -0,0 +1,634 @@
[MAIN]
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Clear in-memory caches upon conclusion of linting. Useful if running pylint
# in a server-like mode.
clear-cache-post-run=no
# Load and enable all available extensions. Use --list-extensions to see a list
# all available extensions.
#enable-all-extensions=
# In error mode, messages with a category besides ERROR or FATAL are
# suppressed, and no reports are done by default. Error mode is compatible with
# disabling specific errors.
#errors-only=
# Always return a 0 (non-error) status code, even if lint errors are found.
# This is primarily useful in continuous integration scripts.
#exit-zero=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-allow-list=pyconcept
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
# for backward compatibility.)
extension-pkg-whitelist=
# Return non-zero exit code if any of these messages/categories are detected,
# even if score is above --fail-under value. Syntax same as enable. Messages
# specified are enabled, while categories only check already-enabled messages.
fail-on=
# Specify a score threshold under which the program will exit with error.
fail-under=10
# Interpret the stdin as a python script, whose filename needs to be passed as
# the module_or_package argument.
#from-stdin=
# Files or directories to be skipped. They should be base names, not paths.
ignore=CVS
# Add files or directories matching the regular expressions patterns to the
# ignore-list. The regex matches against paths and can be in Posix or Windows
# format. Because '\\' represents the directory delimiter on Windows systems,
# it can't be used as an escape character.
ignore-paths=t_.*,.*migrations.*
# Files or directories matching the regular expression patterns are skipped.
# The regex matches against base names, not paths. The default value ignores
# Emacs file locks
ignore-patterns=t_.*?py
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use, and will cap the count on Windows to
# avoid hangs.
jobs=1
# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Minimum Python version to use for version dependent checks. Will default to
# the version used to run pylint.
py-version=3.9
# Discover python modules and packages in the file system subtree.
recursive=no
# Add paths to the list of the source roots. Supports globbing patterns. The
# source root is an absolute path or a path relative to the current working
# directory used to determine a package namespace for modules located under the
# source root.
source-roots=
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# In verbose mode, extra non-checker-related info will be displayed.
#verbose=
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style. If left empty, argument names will be checked with the set
# naming style.
#argument-rgx=
# Naming style matching correct attribute names.
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style. If left empty, attribute names will be checked with the set naming
# style.
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Bad variable names regexes, separated by a comma. If names match any regex,
# they will always be refused
bad-names-rgxs=
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style. If left empty, class attribute names will be checked
# with the set naming style.
#class-attribute-rgx=
# Naming style matching correct class constant names.
class-const-naming-style=UPPER_CASE
# Regular expression matching correct class constant names. Overrides class-
# const-naming-style. If left empty, class constant names will be checked with
# the set naming style.
#class-const-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style. If left empty, class names will be checked with the set naming style.
#class-rgx=
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style. If left empty, constant names will be checked with the set naming
# style.
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style. If left empty, function names will be checked with the set
# naming style.
#function-rgx=
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_
# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
good-names-rgxs=
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style. If left empty, inline iteration names will be checked
# with the set naming style.
#inlinevar-rgx=
# Naming style matching correct method names.
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style. If left empty, method names will be checked with the set naming style.
#method-rgx=
# Naming style matching correct module names.
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style. If left empty, module names will be checked with the set naming style.
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Regular expression matching correct type alias names. If left empty, type
# alias names will be checked with the set naming style.
#typealias-rgx=
# Regular expression matching correct type variable names. If left empty, type
# variable names will be checked with the set naming style.
#typevar-rgx=
# Naming style matching correct variable names.
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style. If left empty, variable names will be checked with the set
# naming style.
#variable-rgx=
[CLASSES]
# Warn about protected attribute access inside special methods
check-protected-access-in-special-methods=no
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp,
asyncSetUp,
__post_init__
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[DESIGN]
# List of regular expressions of class ancestor names to ignore when counting
# public methods (see R0903)
exclude-too-few-public-methods=
# List of qualified class names to ignore when counting class parents (see
# R0901)
ignored-parents=
# Maximum number of arguments for function / method.
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=12
# Maximum number of locals for function / method body.
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body.
max-returns=6
# Maximum number of statements in function / method body.
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[EXCEPTIONS]
# Exceptions that will emit a warning when caught.
overgeneral-exceptions=builtins.BaseException,builtins.Exception
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=100
# Maximum number of lines in a module.
max-module-lines=1000
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[IMPORTS]
# List of modules that can be imported at any level, not just the top level
# one.
allow-any-import-level=
# Allow explicit reexports by alias from a package __init__.
allow-reexport-from-package=no
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=
# Output a graph (.gv or any supported image format) of external dependencies
# to the given file (report RP0402 must not be disabled).
ext-import-graph=
# Output a graph (.gv or any supported image format) of all (i.e. internal and
# external) dependencies to the given file (report RP0402 must not be
# disabled).
import-graph=
# Output a graph (.gv or any supported image format) of internal dependencies
# to the given file (report RP0402 must not be disabled).
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Couples of modules and preferred modules, separated by a comma.
preferred-modules=
[LOGGING]
# The type of string formatting that logging methods do. `old` means using %
# formatting, `new` is for `{}` formatting.
logging-format-style=old
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
# UNDEFINED.
confidence=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then re-enable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=too-many-public-methods,
invalid-name,
no-else-break,
no-else-continue,
no-else-return,
no-member,
too-many-return-statements,
too-many-locals,
too-many-instance-attributes,
too-few-public-methods,
unused-argument,
missing-function-docstring,
attribute-defined-outside-init,
ungrouped-imports,
abstract-method
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member
[METHOD_ARGS]
# List of qualified names (i.e., library.method) which require a timeout
# parameter e.g. 'requests.api.get,requests.api.post'
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
max-line-length=120
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
# Regular expression of note tags to take in consideration.
notes-rgx=
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit,argparse.parse_error
[REPORTS]
# Python expression which should return a score less than or equal to 10. You
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
# 'convention', and 'info' which contain the number of messages in each
# category, as well as 'statement' which is the total number of statements
# analyzed. This score is used by the global evaluation report (RP0004).
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
#output-format=
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[SIMILARITIES]
# Comments are removed from the similarity computation
ignore-comments=yes
# Docstrings are removed from the similarity computation
ignore-docstrings=yes
# Imports are removed from the similarity computation
ignore-imports=yes
# Signatures are removed from the similarity computation
ignore-signatures=yes
# Minimum lines number of a similarity.
min-similarity-lines=4
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. No available dictionaries : You need to install
# both the python package and the system dependency for enchant to work..
spelling-dict=
# List of comma separated words that should be considered directives if they
# appear at the beginning of a comment and should not be checked.
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains the private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to the private dictionary (see the
# --spelling-private-dict-file option) instead of raising a message.
spelling-store-unknown-words=no
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=no
# This flag controls whether the implicit-str-concat should generate a warning
# on implicit string concatenation in sequences defined over several lines.
check-str-concat-over-line-jumps=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of symbolic message names to ignore for Mixin members.
ignored-checks-for-mixins=no-member,
not-async-context-manager,
not-context-manager,
attribute-defined-outside-init
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
# Regex pattern to define which classes are considered mixins.
mixin-class-rgx=.*[Mm]ixin
# List of decorators that change the signature of a decorated function.
signature-mutators=
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of names allowed to shadow builtins
allowed-redefined-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored.
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io

67
webapi/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,67 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Exporter",
"type": "python",
"request": "launch",
"program": "exporter.py",
"console": "integratedTerminal",
"justMyCode": true,
"args": [
"test.ini"
]
},
{
"name": "Menu",
"type": "python",
"request": "launch",
"program": "menu.py",
"console": "integratedTerminal",
"justMyCode": true
},
{
"name": "Add metadata",
"type": "python",
"request": "launch",
"program": "run_metadata.py",
"console": "integratedTerminal",
"justMyCode": true,
"args": [
"test.ini",
"input.xlsx",
"breAccessK3y"
]
},
{
"name": "Add cardslots",
"type": "python",
"request": "launch",
"program": "run_cardslot.py",
"console": "integratedTerminal",
"justMyCode": true,
"args": [
"test.ini",
"input.xlsx",
"breAccessK3y"
]
},
{
"name": "Existence check",
"type": "python",
"request": "launch",
"program": "run_cardslot.py",
"console": "integratedTerminal",
"justMyCode": true,
"args": [
"test.ini",
"input.xlsx",
"breAccessK3y",
"-c"
]
}
]
}

3
webapi/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"python.analysis.typeCheckingMode": "off"
}

View File

@ -0,0 +1,14 @@
[AppData]
# Feel free to modify these parameters
SlovnikExecutor=Фунтов Евгений Евгеньевич
FilterTask=Статья;Микропонятие;Актуализация статьи из ЭВ БРЭ;Медиаконтент;Сюжет
FilterDepartment=Редакция технологий и техники;Редакция энергетики, промышленности
FilterSupervisor=
FilterExecutor=
FilterStatus=
FilterObserver=
# Папка для раскрытых текстов
DocxFolder=./docx

4
webapi/configs/!Menu.ini Normal file
View File

@ -0,0 +1,4 @@
[Launch]
Output=output.csv
Config=portal.ini
User=iborisov

View File

@ -0,0 +1,20 @@
[Options]
# Warning! Do not change this parameters unless you are sure what they influence
# TestRun=false to enable modifications in portal
TestRun=false
# Debug=false will disable exceptions
Debug=false
# Duration in seconds for login to be considered valid
LoginTimeout=900
# Duration in seconds to wait for data to be transmitted after pressing OK
WaitData=10
# Duration in seconds to wait for change of status to transmit to server
WaitStatus=20
# Ammount of times to try creating task after FAILED attempt
CardslotRetries=2

View File

@ -0,0 +1,5 @@
[UserData]
User=Костюк Алексей Владимирович
Input=!Контент.xlsm
UserLogin=a.kostyuk
Password=gAAAAABidSZD_kif_iE_2rjp_zF8o-VsHudEW7UZtT4TfTWr1D77V1q1edtoNp_4wWxjUyATu0KhU2SFIrqnvxCuMdZsLxuApvxqJuUvenDT7xEm2vkoffE=

View File

@ -0,0 +1,5 @@
[UserData]
User=Марзоева Анжелика Владиславовна
Input=!Контент.xlsm
UserLogin=a.marzoeva
Password=gAAAAABjKxsXA3Cp9T4XKQ0zK0d3-3s1KEbtmEgK23mUIZSEgkJiD0LlTyoYz6S8xO4R_wBrPep4bNGcdzA88yAu-EQdaAnftjMSLHPyBDI-UzvWLexC_ro=

View File

@ -0,0 +1,5 @@
[UserData]
User=Никитин Алексей Валерьевич
Input=!Контент.xlsm
UserLogin=a.nikitin
Password=gAAAAABiMOMOoBGkME4YvhxuTYnipry3CKak2jfAMTHcxKP8Uhx_biFk2eBfPwIrcm--NTx4DoYogT_Bk70TAU52717R6A9ixHn4ES2qHHD5NnclOxv9Qdg=

View File

@ -0,0 +1,5 @@
[UserData]
User=Борисов Иван Романович
Input=!Контент.xlsm
UserLogin=i.borisov
Password=gAAAAABlI6fdFAeVM8UhkPYJaOP0pVhE6_GCF91EyY8kaFEyrgkr4SAm3lm4INIXPEQowAlvBr3M9uNtVwjqlGWIAiJPoKNeAg5sDOOP5lpZLYxy4HgdheY=

48
webapi/exporter.py Normal file
View File

@ -0,0 +1,48 @@
'''Data exporter from BRE used mainly for automation'''
import os
import sys
import logging
from datetime import datetime
import portal as bre
def _extract_password(token: str) -> str:
return bre.decrypt(token, '3EJalkimf91-muM') # hard-coded password meant to obfuscate master password on call
if __name__ == '__main__':
import argparse
if not os.path.exists("logs/"):
os.makedirs("logs/")
logging.basicConfig(
filename=datetime.now().strftime('logs/%Y%m%d_%H%M export.log'), encoding='utf-8', level=logging.INFO,
format='%(asctime)s %(levelname)s %(funcName)s: %(message)s',
datefmt='%Y%m%d_%H:%M:%S'
)
logging.getLogger().addHandler(logging.StreamHandler())
parser = argparse.ArgumentParser(description='BRE automation parameters')
parser.add_argument('filename')
parser.add_argument('config', help='Configuration INI file')
args = parser.parse_args(sys.argv)
config = bre.read_config(args.config)
config['AppData']['Password'] = _extract_password(config['AppData']['AccessToken'])
portal = bre.PortalAPI(config)
if not portal.validate(config['AppData']['Password']):
sys.exit(-1)
if not portal.set_output_tasks(config['AppData']['Output']):
sys.exit(-1)
if config['AppData']['ScanContent'] and not portal.set_output_content(config['AppData']['OutputContent']):
sys.exit(-1)
try:
exit_code: int = portal.export_tasks()
except SystemExit as e:
exit_code = e
except: # pylint: disable=bare-except
logging.exception("message")
raise
del portal
sys.exit(exit_code)

203
webapi/menu.py Normal file
View File

@ -0,0 +1,203 @@
'''Main menu for BRE'''
import sys
import os
import logging
from datetime import datetime
from enum import IntEnum, unique
import portal as bre
_CONFIG_FOLDER = 'configs/'
_CONFIG_MAIN = '!Menu.ini'
_CONFIG_APPDATA = '!AppData.ini'
_CONFIG_OPTIONS = '!Options.ini'
_SYSTEM_CONFIGS = [_CONFIG_MAIN, _CONFIG_APPDATA, _CONFIG_OPTIONS]
_TEXT_MAIN_MENU = '''Системное меню [{}]
0. Выбрать пользователя
1. Шифровать пароль пользователя
2. Проверить наличие контента
3. Завести карт-слоты
4. Ввести метаданные
5. Обновить метаданные
6. Загрузить тексты
8. Выгрузить список задач
9. Завершить работу
'''
'''Pairs of flags to indicate if actions needs input / output setup'''
_ACCTIONS_REQUIREMENTS= [
(False, False), # 0
(False, False), # 1
(True, True), # 2
(True, True), # 3
(True, False), # 4
(True, False), # 5
(True, False), # 6
(False, False), # 7
(False, True), # 8
(False, False), # 9
]
@unique
class MenuAction(IntEnum):
'''Действие в главном меню'''
choose_user = 0
encrypt_password = 1
check_existence = 2
import_cards = 3
import_meta = 4
update_meta = 5
load_content_text = 6
export_tasks = 8
exit = 9
def needs_input(self) -> bool:
'''Get text label for field'''
return _ACCTIONS_REQUIREMENTS[self.value][0]
def needs_output(self) -> bool:
'''Get text label for field'''
return _ACCTIONS_REQUIREMENTS[self.value][1]
def _list_configs() -> list[str]:
filenames = next(os.walk(_CONFIG_FOLDER), (None, None, []))[2]
return [item.split('.')[0] for item in filenames if item not in _SYSTEM_CONFIGS]
class Menu:
'''Portal menu UI'''
def __init__(self):
self._config = bre.read_config(_CONFIG_FOLDER + _CONFIG_MAIN)
self._password = ''
def __del__(self):
pass
def start(self):
'''Entry point'''
password = input('Введите пароль: ')
if not self._unlock(password):
print('Пароль не прошел проверку')
sys.exit()
self._main_loop()
def _unlock(self, password: str) -> bool:
if not bre.validate_password(password):
return False
self._password = password
return True
def _main_loop(self):
while True:
os.system('cls')
print(_TEXT_MAIN_MENU.format(self._config['Launch']['User']))
action = MenuAction(int(input('Введите число: ')))
self._process_action(action)
def _process_action(self, action: MenuAction):
if action == MenuAction.exit:
sys.exit(0)
elif action == MenuAction.choose_user:
self._change_user()
return
elif action == MenuAction.encrypt_password:
self._encrypt()
return
portal = self._setup_action(action)
if portal is None:
return
if action == MenuAction.import_meta:
result = portal.import_meta() == 0
elif action == MenuAction.update_meta:
result = portal.update_meta() == 0
elif action == MenuAction.export_tasks:
result = portal.export_tasks() == 0
elif action == MenuAction.check_existence:
result = portal.check_existence() == 0
elif action == MenuAction.import_cards:
result = portal.import_cardslots() == 0
elif action == MenuAction.load_content_text:
result = portal.load_texts() == 0
del portal
if result:
print('Действие завершено успешно')
else:
print('Действие не выполнено')
print('Введите любой символ для возврата в основное меню')
input()
def _setup_action(self, action: MenuAction) -> bre.PortalAPI:
user_config = self._prepare_config()
portal = bre.PortalAPI(user_config)
if not portal.validate(user_config['AppData']['Password']):
print('Portal API invalid...')
input()
return None
if action.needs_input() and not portal.set_input(user_config['UserData']['Input']):
print('Input init failed...')
input()
return None
if action.needs_output() and not portal.set_output_tasks(self._config['Launch']['Output']):
print('Output init failed...')
input()
return None
return portal
def _change_user(self):
os.system('cls')
print('Текущий пользователь: {}'.format(self._config['Launch']['User']))
print('0. Вернуться в основное меню')
user_configs = _list_configs()
for idx, name in enumerate(user_configs):
print('{}. {}'.format(idx + 1, name))
action = int(input('Введите число: '))
if action == 0:
return
self._config['Launch']['User'] = user_configs[action - 1]
def _encrypt(self):
os.system('cls')
user = input('Введите логин пользователя: ')
password = input('Введите пароль пользователя: ')
cypher = bre.encrypt_user(user, password, self._password)
print('Шифрограмма пароля: {}\nСкопируйте этот текст в конфиг-файл пользователя\n'.format(cypher))
input('Ввдеите любой символ для возврата в основное меню\n')
def _prepare_config(self) -> bre.Config:
file_name = self._config['Launch']['Config']
output = open(file_name, 'w', encoding='UTF-8')
inputs = [_CONFIG_APPDATA, _CONFIG_OPTIONS, self._config['Launch']['User'] + '.ini']
for input_name in inputs:
with open(_CONFIG_FOLDER + input_name, 'r', encoding='UTF-8') as infile:
output.write(infile.read())
output.write('\n')
output.close()
config = bre.read_config(file_name)
os.remove(file_name)
config['AppData']['Password'] = self._password
return config
if __name__ == '__main__':
if not os.path.exists("logs/"):
os.makedirs("logs/")
logging.basicConfig(
filename=datetime.now().strftime('logs/%Y%m%d_%H%M menu.log'), encoding='utf-8', level=logging.INFO,
format='%(asctime)s %(levelname)s %(funcName)s: %(message)s',
datefmt='%Y%m%d_%H:%M:%S'
)
logging.getLogger().addHandler(logging.StreamHandler())
try:
Menu().start()
except SystemExit as e:
sys.exit(e)
except: # pylint: disable=bare-except
logging.exception("message")
raise

25
webapi/portal/__init__.py Normal file
View File

@ -0,0 +1,25 @@
''' -*- coding: utf-8 -*-
requires pandas, openpyxl, selenium, rsa
'''
from selenium.webdriver.chrome.options import Options
from .portal import PortalAPI
from .selenium_wrapper import WebBrowser
from .electron_loading import GBDownloader
from .uploader import GreatbookUploader
from .info_models import FieldType, FilterType
from .data_reader import DataReader, ContentIterator
from .bre_browser_options import (
get_browser_options
)
from .config import (
Config, read_config
)
from .crypto import (
validate_password, encrypt, decrypt, encrypt_user, decrypt_user
)

View File

@ -0,0 +1,17 @@
'''BRE browser options'''
from selenium.webdriver.chrome.options import Options
def get_browser_options(show_window: bool) -> Options:
'''Prepare browser options for BRE portal'''
options = Options()
if not show_window:
options.add_argument('--headless')
options.add_argument("--start-maximized")
options.add_experimental_option('excludeSwitches', ['enable-logging', 'enable-automation'])
prefs = {"credentials_enable_service": False, "profile.password_manager_enabled": False}
options.add_experimental_option("prefs", prefs)
return options

12
webapi/portal/config.py Normal file
View File

@ -0,0 +1,12 @@
'''Configuration module'''
from configparser import ConfigParser as Config
def read_config(file_name: str) -> Config:
'''Read config from a file'''
config_file = open(file_name, 'r', encoding='utf8')
config = Config()
config.read_file(config_file)
config_file.close()
return config

53
webapi/portal/crypto.py Normal file
View File

@ -0,0 +1,53 @@
'''Cyptographic module'''
import base64
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
def _get_crypto(password: str) -> Fernet:
_CRYPTO16_SALT = b'\xac\xaa\xc7\xae\x99\xb1\x7fO\x01\xc6\x94<R$\xf7?'
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=_CRYPTO16_SALT,
iterations=390000,
)
key = base64.urlsafe_b64encode(kdf.derive(password.encode('UTF-8')))
return Fernet(key)
def validate_password(password: str) -> bool:
'''Validate password using test message'''
_VERIFICATION_MESSAGE = 'Hello BRE'
_VERIFICATION_CRYPTO = \
'gAAAAABiMGnQn96MZkBpBE9qZRJfZ91-muMLzxMnydwcXt3ZaG6zjRt576E1waelYKxhGMazRSYwmslHpqxpgtIMSDbQSuE6_A=='
try:
keyphrase = decrypt(_VERIFICATION_CRYPTO, password)
return keyphrase == _VERIFICATION_MESSAGE
except InvalidToken:
return False
def encrypt(message: str, password: str) -> str:
'''Encrypt message using key locked by password'''
crypto = _get_crypto(password)
return crypto.encrypt(message.encode('UTF-8')).decode('UTF-8')
def encrypt_user(user_name: str, user_password: str, crypto_passwrod: str) -> str:
'''Encrypt user password using key locked by crypto password'''
return encrypt(user_name + user_password, crypto_passwrod)
def decrypt(ciphertext: str, password: str):
'''Decrypt ciphertext using key locked by password'''
crypto = _get_crypto(password)
return crypto.decrypt(ciphertext.encode('UTF-8')).decode('UTF-8')
def decrypt_user(user_name: str, ciphertext: str, crypto_passwrod: str) -> str:
'''Decrypt user password using key locked by crypto password'''
text = decrypt(ciphertext, crypto_passwrod)
return text[len(user_name):]

View File

@ -0,0 +1,196 @@
'''Reading data from Excel spreadsheets'''
from enum import IntEnum, unique
from datetime import datetime
import pandas
from .info_models import FieldType, InputMethod, text_to_method
_ContentFields = [
FieldType.task_type,
FieldType.status,
FieldType.content_name,
FieldType.skip,
FieldType.change_score,
FieldType.biblio_name,
FieldType.definition,
FieldType.is_immutable,
FieldType.object_type,
FieldType.markers,
FieldType.tags,
FieldType.author,
FieldType.supervisor,
FieldType.executor,
FieldType.task_manager,
FieldType.responsible,
FieldType.department,
FieldType.date_target,
FieldType.source,
FieldType.electron_bre,
FieldType.main_page,
FieldType.is_general,
FieldType.actualize_period,
FieldType.age_restriction,
FieldType.priority,
FieldType.article_type,
FieldType.date_exchange,
FieldType.date_ees1,
FieldType.date_ex_tools,
FieldType.date_ees2,
FieldType.expert,
FieldType.contract,
FieldType.comment,
FieldType.task_id,
FieldType.content_name_db,
FieldType.task_name
]
@unique
class _ContentColumns(IntEnum):
task_type = 0
status = 1
content_name = 2
change_score = 4
biblio_name = 5
definition = 6
is_immutable = 7
object_type = 8
markers = 9
tags = 10
author = 11
supervisor = 12
executor = 13
task_manager = 14
responsible = 15
department = 16
date_target = 17
source = 18
electron_bre = 19
main_page = 20
is_general = 21
actualize_period = 22
age_restriction = 23
priority = 24
article_type = 25
date_exchange = 26
date_ees1 = 27
date_ex_tools = 28
date_ees2 = 29
expert = 30
contract = 31
comment = 32
task_id = 33
content_name_db = 34
task_name = 35
def to_field(self) -> FieldType:
'''Transform metadata column to FieldType'''
return _ContentFields[self.value]
def _get_task_name(content_name: str, is_immutable: bool) -> str:
UNMUTABLE_TEMPLATE = 'Неизменные {} (библиография+корректура+транскрипция)'
if not is_immutable:
return content_name
else:
return UNMUTABLE_TEMPLATE.format(content_name)
def _drop_from_nan(target: pandas.DataFrame) -> pandas.DataFrame:
rows_with_nan = [index for index, row in target.iterrows() if pandas.isna(row.iloc[0])]
if len(rows_with_nan) > 0:
return target[:rows_with_nan[0]]
else:
return target
class ContentIterator:
'''Iterates over metadata sheet rows'''
def __init__(self, data: pandas.DataFrame):
self._data = data
self._row = 0
self._count = len(self._data.index)
def __del__(self):
pass
def is_done(self) -> bool:
'''Indicates end of iteration'''
return self._row >= self._count
def next(self) -> bool:
'''Iteration'''
if self.is_done():
return False
self._row = self._row + 1
return True
def read_row(self) -> dict:
'''Data access'''
data = {}
for column in _ContentColumns:
if not pandas.isna(self._data.iat[self._row, column]):
field = column.to_field()
value = self._data.iat[self._row, column]
if field.input_method() == InputMethod.combo_dialog or \
field.input_method() == InputMethod.combo_dialog_simple_list:
data[field] = list(filter(None, [element.strip() for element in value.split(';')]))
elif isinstance(value, str):
data[field] = value.strip()
elif isinstance(value, pandas.Timestamp):
data[field] = value.strftime('%d.%m.%Y')
elif isinstance(value, datetime):
data[field] = value.strftime('%d.%m.%Y')
else:
data[field] = value
if FieldType.is_immutable in data:
data[FieldType.is_immutable] = data[FieldType.is_immutable] == 'Да'
if FieldType.electron_bre in data:
data[FieldType.electron_bre] = data[FieldType.electron_bre] == 'Да'
if FieldType.main_page in data:
data[FieldType.main_page] = data[FieldType.main_page] == 'Да'
if FieldType.is_general in data:
data[FieldType.is_general] = data[FieldType.is_general] == 'Да'
if FieldType.content_name_db not in data:
data[FieldType.content_name_db] = data[FieldType.content_name]
if FieldType.task_name not in data:
is_immutable = FieldType.is_immutable in data and data[FieldType.is_immutable]
data[FieldType.task_name] = _get_task_name(data[FieldType.content_name], is_immutable)
if FieldType.department in data:
data[FieldType.department] = 'Редакция ' + data[FieldType.department]
data[FieldType.editorial] = data[FieldType.department]
if FieldType.article_type in data:
data[FieldType.article_type][0] = data[FieldType.article_type][0] + ' статья'
return data
class DataReader:
'''BRE data reader for Excel'''
_SHEET_CONTENT = 'Контент'
_SHEET_ATTRIBUTES = 'Признаки'
def __init__(self):
self._xls = None
self._content = None
self._attributes = None
def load(self, input_file: str) -> bool:
'''Load file'''
try:
self._xls = pandas.ExcelFile(input_file)
self._content = _drop_from_nan(pandas.read_excel(self._xls, DataReader._SHEET_CONTENT))
self._attributes = _drop_from_nan(pandas.read_excel(self._xls, DataReader._SHEET_ATTRIBUTES))
except (FileNotFoundError, ValueError):
return False
return True
def get_content(self) -> ContentIterator:
'''Return iterator for cards'''
return ContentIterator(self._content)
def get_attributes_for(self, content_name) -> list:
'''Return attributes list for specific content'''
filtered = self._attributes.loc[self._attributes['Название контента'] == content_name]
return [(row[1], text_to_method(row[3]), row[2]) for index, row in filtered.iterrows()]

75
webapi/portal/document.py Normal file
View File

@ -0,0 +1,75 @@
'''Processing docx files'''
import os
import subprocess
import docx
from docx.shared import Pt
def _strip_stress_(text: str) -> str:
''' Удаляет с текста ударения text: текст '''
a = bytes(text, encoding='utf8').split(b"\xcc\x81")
res = ""
for i in a:
res += str(i, encoding='utf-8')
return res
class DocxTextProcessor:
'''Document processor class'''
def __init__(self, file_name=None) -> None:
'''
file_name: название файла для считывания
Конструктор автоматически создаёт Документ и парсит в текст, разделяя литературу и авторов
'''
self._docx = None
self.text = ''
self.authors = ''
self.bibliography = ''
self._name = ''
if file_name is not None:
self.process_document(file_name)
def process_document(self, file_name: str) -> bool:
'''Document processing'''
self._docx = docx.Document(file_name)
self._name = file_name.split('/')[-1].split('.docx')[0]
try:
self.text = self.get_text__()
except: # pylint: disable=bare-except
return False
return True
def get_text__(self):
''' Получает текст - самый стрёмный метод '''
res = ''
f = self._docx
name = self._name
is_bib_part = False
for p in f.paragraphs:
if p.runs[0].font.size < Pt(15):
for run in p.runs:
if name == _strip_stress_(run.text):
res += (run.text[:1] + run.text[1:].lower())
continue
if "Лит" in run.text or is_bib_part:
is_bib_part = True
self.bibliography += run.text
continue
res += (run.text)
res += '\n'
test = _strip_stress_(res[:150]).lower()
if test.find(name.lower().split('/')[-1]) > 2:
self.authors = res[:test.find(name.lower()) - 1]
res = res[test.find(name.lower()):]
return res
def process_typograph(self):
''' Обработка типографом '''
with open('test', 'wb+') as file:
file.write(bytes(self.text, encoding='utf-8'))
cmd = 'node process_typograph.js'
subprocess.run(cmd, capture_output=True, check=True) # mind about try
with open('temp.log', 'r+', encoding='utf8') as file:
self.text = file.read()
os.remove("test")
os.remove("temp.log")

View File

@ -0,0 +1,171 @@
'''
автоматическая загруза с сайта посредством selenium
с использованием браузера Chrome (требуется драйвер)
'''
import csv
from selenium.webdriver.common.by import By
from .selenium_wrapper import WebBrowser
_LOGIN_PAGE = 'https://bigenc.ru/user/login'
_ARTICLE_PAGE = 'https://bigenc.ru/user/content/articles'
class GBDownloader:
'''Downloader functionality'''
def __init__(self, browser: WebBrowser, save_path: str, append: bool, suffix_seed: int):
mode = 'a' if append else 'w'
suffix = ('00' + str(suffix_seed))[-3:]
self.browser = browser
self.save_path = save_path
self._articles = open('{}\\!out_bd_{}.csv'.format(save_path, suffix), mode, newline='')
self._references = open('{}\\!ref_bd_{}.csv'.format(save_path, suffix), mode, newline='')
self._images = open('{}\\!img_bd_{}.csv'.format(save_path, suffix), mode, newline='')
self._bibliography = open('{}\\!bib_bd_{}.csv'.format(save_path, suffix), mode, newline='')
self.writer_out = csv.writer(self._articles)
self.writer_ref = csv.writer(self._references)
self.writer_img = csv.writer(self._images)
self.writer_bib = csv.writer(self._bibliography)
if not append:
self._create_headers()
def __del__(self):
self._articles.close()
self._references.close()
self._images.close()
self._bibliography.close()
def login(self, login: str, password: str) -> bool:
'''Login to electron version of BRE'''
self.browser.driver.get(_LOGIN_PAGE)
if not self.browser.wait_presence('page-login-form'):
return False
self.browser.driver.find_element(By.NAME, '__login').send_keys(login)
self.browser.driver.find_element(By.NAME, '__password').send_keys(password)
self.browser.driver.find_element(By.TAG_NAME, 'button').click()
return self.browser.wait_presence('userMenu')
def scan(self, start: str, stop: str, max_items: int) -> str:
'''Scan element from BRE'''
next_id = start
for i in range(max_items):
print('{}-{}'.format(i, next_id))
next_id = self._process_article(next_id, i)
self._flush()
if next_id == '':
print("Stopped with no next")
break
if next_id == stop:
print("Stopped at " + stop + " and worked " + str(i + 1) + "articles")
break
return next_id
def _flush(self):
self._articles.flush()
self._references.flush()
self._images.flush()
self._bibliography.flush()
def _create_headers(self):
self.writer_out.writerow([
'Indx', 'ArticleId', 'BlackWord', 'subWord', 'Rubrika', 'Year', 'Slovnik', 'Author(s)',
'KolZnak', 'Status', 'Version', 'Litr', 'Soch', 'KolImg', 'KolCnt'
])
# ИД слова статьи + ИД статьи ссылаемого слова + другое слово + ссылка
self.writer_ref.writerow(['WordId', 'RefWordId', 'RefWord', 'RefURL'])
# ИД слова + ИД ссылки на иллюстрацию
self.writer_img.writerow(['WordId', 'ImgId'])
# ИД слова + ИД библиоссылки + тип библиографии + текст библиографии
self.writer_bib.writerow(['WordId', 'BibId', 'BibType', 'BibText'])
def _process_article(self, article_id: str, index: int) -> str:
self.browser.driver.get('{}/{}'.format(_ARTICLE_PAGE, article_id))
if not self.browser.wait_presence('userMenu'):
return ''
self._download_rtf()
image_count = self._extract_images(article_id)
links_count = self._extract_links(article_id)
literature, sochinenie = self._extract_biblio(article_id)
self._extract_article_info(article_id, index, image_count, links_count, literature, sochinenie)
ref = self.browser.by_xpath('//a[.="следующая >>"]').get_attribute('href')
return ref.split('/')[-1]
def _download_rtf(self):
self.browser.by_xpath('//a[@title="Rich Text Format"]').click()
return True
def _extract_article_info(self, article_id: str, index: int, images: int, links: int, literature: int, soch: int):
black_word = self.browser.by_xpath('//h1[@name="title"]').text
sub_word = self.browser.by_xpath('//h2[@name="subtitle"]').text
section_name = self.browser.by_xpath('//span[@name="section"]').text # Рубрика сайта
markers = self.browser.by_xpath('//span[@name="markers"]').text # Словник
authors = self.browser.by_xpath_m('//p[@class="Автор type-"]') # Автор // !!два значения или не одного
author = '' if len(authors) == 0 else authors[0].text
char_num = self.browser.by_xpath('//span[@class="art_num_chars"]').text # Количество знаков
status = self.browser.by_xpath('//span[@class="status-text"]').text # Статус ("опубликовано" и др.)
status = status.replace('Статус: ', '')
version = self.browser.by_xpath('//td[@id="art-cat"]').text # Версия статьи ("Исходная" и др.)
year = self.browser.by_xpath('//span[@class="year"]').text
self.writer_out.writerow([
index, article_id, black_word, sub_word, section_name, year, markers, author,
char_num, status, version, literature, soch, images, links
])
def _extract_images(self, article_id: str) -> int:
# Гиперссылка на иллюстрации (src="")
images = self.browser.by_xpath_m('//img')
for img in images:
source = img.get_attribute('src')
self.writer_img.writerow([article_id, source])
return len(images)
def _extract_links(self, article_id: str) -> int:
links = self.browser.by_xpath_m('//a[@class="processed-link"]')
for link in links:
self.writer_ref.writerow([
article_id, link.get_attribute('data-art'),
link.get_attribute('data-word'),
link.get_attribute('href')
])
return len(links)
def _extract_biblio(self, article_id: str):
# Библиография // (по куску текста) + удалить '&shy;' (\xad) заменить '&nbsp;' на пробел Возможно, подстрока '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

View File

@ -0,0 +1,160 @@
'''Data models for BRE portal'''
from enum import Enum, IntEnum, unique
@unique
class InputMethod(Enum):
'''Метод ввода данных на форме'''
read_only = -1
combo_box = 0
text_input = 1
text_area = 2
date_picker = 3
combo_dialog = 4
combo_dialog_simple_list = 5 # no search required to select from list
bool_toggle = 6
def text_to_method(text: str) -> InputMethod:
'''Input method from text transformation'''
if text == 'Текстовое поле':
return InputMethod.text_input
if text == 'Тумблер':
return InputMethod.bool_toggle
if text == 'Выпад. Cписок':
return InputMethod.combo_box
if text == 'Диалог (с выбором)':
return InputMethod.combo_dialog
return InputMethod.read_only
_FIELD_DATA = [
('Тип задачи', InputMethod.combo_box),
('Статус', InputMethod.read_only),
('Название задачи', InputMethod.text_input),
('Название контента', InputMethod.text_input),
('Характер изменения текста', InputMethod.combo_dialog_simple_list),
('Название контента для библиографической записи', InputMethod.text_input),
('Краткая дефиниция', InputMethod.text_input),
('Неизменный', InputMethod.read_only),
('Тип объекта', InputMethod.combo_dialog),
('Тематический словник', InputMethod.combo_dialog),
('Метки', InputMethod.combo_dialog),
('Курирующий редактор', InputMethod.combo_box),
('Исполнитель', InputMethod.combo_box),
('Ответственный за задачу', InputMethod.combo_box),
('Ответственный', InputMethod.combo_box),
('Ответственное подразделение', InputMethod.combo_box),
('Ответственная редакция', InputMethod.combo_box),
('Срок исполнения', InputMethod.date_picker),
('Источник термина', InputMethod.combo_dialog),
('Текст из ЭВ БРЭ', InputMethod.bool_toggle),
('Отображать на главной', InputMethod.bool_toggle),
('Обобщающая статья', InputMethod.bool_toggle),
('Период актуализации', InputMethod.combo_dialog_simple_list),
('Возрастное ограничение', InputMethod.combo_dialog_simple_list),
('Приоритет', InputMethod.combo_box),
('Тип статьи', InputMethod.combo_dialog_simple_list),
('Биржа', InputMethod.date_picker),
('Срок сдачи в ЕЭС 1', InputMethod.date_picker),
('Биржа инструментов', InputMethod.date_picker),
('Срок сдачи в ЕЭС 2', InputMethod.date_picker),
('Эксперт', InputMethod.combo_box),
('Номер договора', InputMethod.combo_box),
('Комментарий', InputMethod.text_area),
('Автор (на портале)', InputMethod.combo_box),
('ID задачи', InputMethod.read_only),
('Название контента в базе', InputMethod.read_only),
]
@unique
class FieldType(IntEnum):
'''Поля ввода'''
task_type = 0
status = 1
task_name = 2
content_name = 3
change_score = 4
biblio_name = 5
definition = 6
is_immutable = 7
object_type = 8
markers = 9
tags = 10
supervisor = 11
executor = 12
task_manager = 13
responsible = 14
department = 15
editorial = 16
date_target = 17
source = 18
electron_bre = 19
main_page = 20
is_general = 21
actualize_period = 22
age_restriction = 23
priority = 24
article_type = 25
date_exchange = 26
date_ees1 = 27
date_ex_tools = 28
date_ees2 = 29
expert = 30
contract = 31
comment = 32
author = 33
task_id = 34
content_name_db = 35
skip = -1
def to_label(self) -> str:
'''Get text label for input field'''
return _FIELD_DATA[self.value][0]
def input_method(self) -> InputMethod:
'''Get input method for field'''
return _FIELD_DATA[self.value][1]
_FILTER_DATA = [
('FilterTask', 'Тип задачи'),
('FilterDepartment', 'Ответственное подразделение'),
('FilterStatus', 'Статус задачи'),
('FilterResponsible', 'Ответственный за задачу'),
('FilterSupervisor', 'Курирующий редактор'),
('FilterExecutor', 'Исполнитель'),
('FilterObserver', 'Наблюдатель'),
('FilterDateCreatedBegin', 'Дата создания'),
('FilterDateCreatedEnd', 'Дата создания'),
('FilterDateTargetBegin', 'Срок исполнения'),
('FilterDateTargetEnd', 'Срок исполнения')
]
@unique
class FilterType(IntEnum):
'''Параметры фильтрации'''
task_type = 0
department = 1
status = 2
responsible = 3
supervisor = 4
executor = 5
observer = 6
created_begin = 7
created_end = 8
target_begin = 9
target_end = 10
def to_config(self) -> str:
'''Get input method for field'''
return _FILTER_DATA[self.value][0]
def to_label(self) -> str:
'''Get input method for field'''
return _FILTER_DATA[self.value][1]

347
webapi/portal/portal.py Normal file
View File

@ -0,0 +1,347 @@
'''BRE Portal API'''
import csv
import time
import logging
import warnings
from colorama import init as color_init
from colorama import Fore, Style
from .selenium_wrapper import WebBrowser
from .config import Config
from .info_models import FieldType, FilterType
from .uploader import GreatbookUploader
from .data_reader import DataReader
from .bre_browser_options import get_browser_options
from .crypto import validate_password
from .document import DocxTextProcessor
warnings.filterwarnings('ignore', category=UserWarning, module='openpyxl')
def _log_start():
logging.info(
'Start time is ' + Style.BRIGHT + Fore.GREEN + '%s' + Style.RESET_ALL,
time.strftime('%H:%M:%S')
)
def _log_end():
logging.info(
'Done ... end time is ' + Style.BRIGHT + Fore.GREEN + '%s' + Style.RESET_ALL,
time.strftime('%H:%M:%S')
)
def _chunks(lst, n: int):
"""Yield successive n-sized chunks from lst."""
for i in range(0, len(lst), n):
yield lst[i:i + n]
def _format_for(status: str):
if status in ['OK', 'NOT EXISTS']:
return Style.BRIGHT + Fore.GREEN
if status in ['EXCEPTION', 'FAIL', 'EXISTS', 'NO TASK']:
return Style.BRIGHT + Fore.RED
if status in ['ONLY TASK']:
return Style.BRIGHT + Fore.BLUE
return ''
class PortalAPI:
'''Main entrpoint to Portal'''
def __init__(self, config: Config):
color_init(autoreset=True)
chrome = WebBrowser()
chrome_options = get_browser_options(show_window=True)
chrome.start_chrome(chrome_options)
self.config = config
self._browser = chrome
self._debug = config['Options'].getboolean('Debug')
self._loader = GreatbookUploader(chrome, config)
self._reader = None
self._output_tasks = None
self._writer_tasks = None
self._output_content = None
self._writer_content = None
self._document_processor = None
def __del__(self):
pass
def validate(self, password: str) -> bool:
'''Validate API status'''
if not validate_password(password):
return False
if not self._loader.login():
return False
return True
def set_input(self, input_file: str) -> bool:
'''Initialize input file'''
self._reader = DataReader()
if not self._reader.load(input_file):
logging.error('Failed to access %s', input_file)
return False
return True
def set_output_tasks(self, output_file: str) -> bool:
'''Initialize output file'''
self._output_tasks = open(output_file, 'w', newline='', encoding='utf-8')
self._writer_tasks = csv.writer(self._output_tasks)
return True
def set_output_content(self, output_file: str) -> bool:
'''Initialize output file'''
self._output_content = open(output_file, 'w', newline='', encoding='utf-8')
self._writer_content = csv.writer(self._output_content)
return True
def check_existence(self) -> int:
'''Check existence of card-slots from Excel input'''
_log_start()
self._writer_tasks.writerow(['Слово', 'Статус', 'Текст', 'Идентификатор'])
content_it = self._reader.get_content()
while not content_it.is_done():
data = content_it.read_row()
(content, status, has_text, task_id) = self._process_existence(data)
self._write_task(content, status, has_text, task_id)
content_it.next()
self._output_tasks.close()
_log_end()
return 0
def import_cardslots(self) -> int:
'''Check existence of card-slots from Excel input'''
_log_start()
self._writer_tasks.writerow(['Слово', 'Статус', 'Текст', 'Идентификатор'])
content_it = self._reader.get_content()
while not content_it.is_done():
data = content_it.read_row()
attempts = 0
while attempts <= self.config['Options'].getint('CardslotRetries') + 1:
if attempts > 0:
logging.info('Retrying after failed attempt # %d...', attempts)
(content, status, has_text, task_id) = self._process_cardslots(data)
self._write_task(content, status, has_text, task_id)
if status != 'FAILED':
break
attempts += 1
content_it.next()
self._output_tasks.close()
_log_end()
return 0
def import_meta(self) -> int:
'''Import content metadata'''
_log_start()
content_it = self._reader.get_content()
while not content_it.is_done():
data = content_it.read_row()
content = data[FieldType.content_name_db]
attributes = self._reader.get_attributes_for(content)
status = self._process_metadata(data, attributes)
logging.info('%s ... ' + _format_for(status) + '[%s]' + Style.RESET_ALL, content, status)
content_it.next()
_log_end()
return 0
def update_meta(self) -> int:
'''Update content metadata'''
_log_start()
content_it = self._reader.get_content()
while not content_it.is_done():
data = content_it.read_row()
content = data[FieldType.content_name_db]
attributes = self._reader.get_attributes_for(content)
status = self._update_metadata(data, attributes)
logging.info('%s ... ' + _format_for(status) + '[%s]' + Style.RESET_ALL, content, status)
content_it.next()
_log_end()
return 0
def load_texts(self) -> int:
'''Load content text'''
_log_start()
self._document_processor = DocxTextProcessor()
content_it = self._reader.get_content()
while not content_it.is_done():
data = content_it.read_row()
content = data[FieldType.content_name]
filename = self.config['AppData']['DocxFolder'] + '/' + content.upper() + '.docx'
if not self._document_processor.process_document(filename):
status = 'FAIL'
else:
authors = self._document_processor.authors
text = self._document_processor.text
bibliography = self._document_processor.bibliography
status = self._load_content_text(content, authors, text, bibliography)
content_it.next()
logging.info('%s ... ' + _format_for(status) + '[%s]' + Style.RESET_ALL, content, status)
_log_end()
return 0
def export_tasks(self) -> int:
'''Update content metadata'''
_log_start()
scanTasks = self.config['AppData']['ScanTasks'] == 'true'
scanContent = self.config['AppData']['ScanContent'] == 'true'
excluded = self.config['AppData']['ExcludeID'].split(';')
if '' in excluded:
excluded.remove('')
included = self.config['AppData']['IncludeID'].split(';')
if '' in included:
included.remove('')
logging.info('Excluded tasks: %s', len(excluded))
logging.info('Included tasks: %s', len(included))
logging.info('Scan tasks: %s', scanTasks)
logging.info('Scan content: %s', scanContent)
if scanTasks:
logging.info('Loading tasks data...')
filters = []
for filter_id in FilterType:
filters.append([s.strip() for s in self.config['AppData'][filter_id.to_config()].split(';')])
data = self._loader.get_tasks_data(filters)
logging.info('Loaded %s tasks', len(data))
self._writer_tasks.writerows(data)
self._output_tasks.close()
if not scanContent:
_log_end()
return 0
tasks_with_content = [
item[6] for item in
filter(
lambda x: x[0] in ['МИКРОПОНЯТИЕ', 'СТАТЬЯ', 'АКТУАЛИЗАЦИЯ СТАТЬИ ИЗ ЭВ БРЭ', 'СЮЖЕТ']
and x[1] not in ['Отменена']
and x[2] not in ['Нет Контента', 'Нет Медиа']
and x[6] not in excluded
and x[6] not in included,
data
)
]
included = included + tasks_with_content
chunks = list(_chunks(included, 50))
logging.info('Scanning %s content in %s bundles', len(included), len(chunks))
for index, tasks_bundle in enumerate(chunks): # Split in 50 bunches to ensure login is valid
logging.info('%s: Processing bundle %s / %s', time.strftime('%H:%M:%S'), index + 1, len(chunks))
try:
content = self._loader.get_tasks_content(tasks_bundle)
self._writer_content.writerows(content)
self._output_content.flush()
except: # pylint: disable=bare-except
logging.info('EXCEPTION during processing! Skipping bundle %s', index + 1)
logging.info('\n'.join(tasks_bundle))
self._output_content.close()
_log_end()
return 0
def _write_task(self, content: str, status: str, has_text: bool, task_id: str):
has_text_str = 'Да' if has_text else 'Нет'
# pylint: disable=logging-not-lazy
logging.info(
'%s ... ' + _format_for(status) + '[%s]' + Style.RESET_ALL + ' ... [%s] ... %s',
content, status, has_text_str, task_id
)
self._writer_tasks.writerow([content, status, has_text_str, task_id])
self._output_tasks.flush()
def _process_existence(self, data: dict):
content = data[FieldType.content_name_db]
has_text = False
try:
task_id = self._loader.find_task_id(data[FieldType.task_name])
if self._loader.content_exists(content):
status = 'EXISTS'
has_text = self._loader.content_has_text(content)
else:
status = 'ONLY TASK' if task_id != '' else 'NOT EXISTS'
except: # pylint: disable=bare-except
if not self._debug:
logging.exception('Got exception...')
task_id = ''
status = 'EXCEPTION'
else:
raise
if content == 'Нет Контента':
content = data[FieldType.task_name]
return (content, status, has_text, task_id)
def _process_cardslots(self, data: dict):
content = data[FieldType.content_name_db]
has_text = False
try:
task_id = self._loader.find_task_id(data[FieldType.task_name])
if self._loader.content_exists(content):
status = 'EXISTS'
has_text = self._loader.content_has_text(content)
elif task_id != '':
status = 'ONLY TASK'
else:
task_id = self._loader.create_task(data)
status = 'OK' if task_id != '' else 'FAILED'
except: # pylint: disable=bare-except
if not self._debug:
logging.exception('Got exception...')
status = 'EXCEPTION'
task_id = ''
else:
raise
return (content, status, has_text, task_id)
def _process_metadata(self, data: dict, attributes: list) -> str:
try:
task_id = self._loader.find_task_id(data[FieldType.task_name])
if task_id == '':
return 'NO TASK'
if self._loader.fill_metadata(task_id, data, attributes):
return 'OK'
else:
return 'FAIL'
except: # pylint: disable=bare-except
if not self._debug:
logging.exception('Got exception...')
return 'EXCEPTION'
else:
raise
def _update_metadata(self, data: dict, attributes: list) -> str:
try:
task_id = self._loader.find_task_id(data[FieldType.task_name])
if task_id == '':
return 'NO TASK'
if self._loader.update_metadata(task_id, data, attributes):
return 'OK'
else:
return 'FAIL'
except: # pylint: disable=bare-except
if not self._debug:
logging.exception('Got exception...')
return 'EXCEPTION'
else:
raise
def _load_content_text(self, content: str, authors, text: str, bibliography: str) -> str:
try:
if self._loader.load_content(content, authors, text, bibliography):
return 'OK'
else:
return 'FAIL'
except: # pylint: disable=bare-except
if not self._debug:
logging.exception('Got exception...')
return 'EXCEPTION'
else:
raise

View File

@ -0,0 +1,19 @@
const Typograph = require('./typograf/build/typograf.js')
const tp = new Typograph({locale : ['ru' , 'en-US']})
tp.enableRule('common/nbsp/replaceNbsp');
const fs = require('fs');
var file = '';
file = fs.readFile('test', 'utf-8', function(err,data){
if (!err) {
fs.writeFile('temp.log', tp.execute(data), (err) => {
if(err){
console.log(err);
}
})
} else {
console.log(err);
}})

View File

@ -0,0 +1,97 @@
'''Selenium wrapper'''
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import TimeoutException, WebDriverException
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.remote.webelement import WebElement
_LOAD_TIMEOUT = 10 # seconds
_AJAX_TIMEOUT = 10 # seconds
def _ajax_complete(driver) -> bool:
try:
return driver.execute_script("return jQuery.active") == 0
except WebDriverException:
return True
class WebBrowser:
'''Wrapper for selenium webdriver'''
def __init__(self):
self.driver = None
def __del__(self):
self.stop_chrome()
def start_chrome(self, chrome_options):
'''Start Google Chrome driver'''
self.driver = webdriver.Chrome(options=chrome_options)
def stop_chrome(self):
'''Stop driver'''
if self.driver:
self.driver.quit()
def by_xpath(self, xpath: str) -> WebElement:
'''Return web element found by xpath'''
return self.driver.find_element(By.XPATH, xpath)
def by_xpath_m(self, xpath: str) -> list[WebElement]:
'''Return multimple web elements found by xpath'''
return self.driver.find_elements(By.XPATH, xpath)
def wait_presence(self, elem_id: str, by_what: str = By.ID) -> bool:
'''Wait for presence of specific element'''
try:
element_present = EC.presence_of_element_located((by_what, elem_id))
WebDriverWait(self.driver, _LOAD_TIMEOUT).until(element_present)
return True
except TimeoutException:
return False
def wait_visibility(self, elem_id: str, by_what: str = By.ID) -> bool:
'''Wait for presence of specific element'''
try:
element_visible = EC.visibility_of_element_located((by_what, elem_id))
WebDriverWait(self.driver, _LOAD_TIMEOUT).until(element_visible)
return True
except TimeoutException:
return False
def wait_clickable(self, elem_id: str, by_what: str = By.ID) -> bool:
'''Wait for presence of specific element'''
try:
element_clickable = EC.element_to_be_clickable((by_what, elem_id))
WebDriverWait(self.driver, _LOAD_TIMEOUT).until(element_clickable)
return True
except TimeoutException:
return False
def wait_ajax(self):
'''Wait for ajax to finish'''
wait_token = WebDriverWait(self.driver, _AJAX_TIMEOUT)
wait_token.until(_ajax_complete)
wait_token.until(lambda driver: driver.execute_script('return document.readyState') == 'complete')
# WebDriverWait wait = new WebDriverWait(getWebDriver(), 10);
# WebElement element = wait.until(ExpectedConditions.visibilityOf(element));
# jQuery doesnt work for some reason so we just wait implicitly
time.sleep(1)
def send_esc(self):
'''Send Escape key to browser'''
webdriver.ActionChains(self.driver).send_keys(Keys.ESCAPE).perform()
def send_enter(self):
'''Send Enter key to browser'''
webdriver.ActionChains(self.driver).send_keys(Keys.ENTER).perform()
def send_tab(self):
'''Send Tab key to browser'''
webdriver.ActionChains(self.driver).send_keys(Keys.TAB).perform()

946
webapi/portal/uploader.py Normal file
View File

@ -0,0 +1,946 @@
"""Uploading data to rk.greatbook.ru"""
import time
import logging
import random
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.common.exceptions import (
NoSuchElementException,
InvalidElementStateException,
ElementNotInteractableException
)
from selenium.webdriver.remote.webelement import WebElement
from .selenium_wrapper import WebBrowser
from .info_models import InputMethod, FieldType, FilterType
from .config import Config
from .crypto import decrypt_user
def _replace_comma_separators(value: str) -> str:
start = 0
while True:
pos = value.find(", ", start)
if pos == -1 or pos + 2 >= len(value):
return value
if value[pos + 2].isupper():
value = ';'.join([value[:pos], value[pos + 2:]])
start = pos + 1
class GreatbookUploader:
"""Entry point into portal data input automation"""
def __init__(self, browser: WebBrowser, config: Config):
self._impl = _UploaderImpl(browser, config)
self._login_timeout = config['Options'].getint('LoginTimeout')
self._time_login = 0
def __del__(self):
pass
def login(self) -> bool:
'''Login into Portal'''
if not self._impl.login():
return False
self._time_login = time.time()
return True
def logout(self):
'''Logout from current user'''
self._impl.logout()
def content_exists(self, content: str) -> bool:
'''Check if content name is taken'''
self._ensure_login()
return self._impl.content_exists(content)
def content_has_text(self, content: str) -> bool:
'''Warning! Requires to run content_exists first'''
return self._impl.content_has_text(content)
def create_task(self, data: dict) -> bool:
'''Create task in system. Returns empty string on error'''
self._ensure_login()
return self._impl.create_task(data)
def find_task_id(self, task_name: str) -> str:
'''Find task ID or empty string if not found'''
self._ensure_login()
return self._impl.find_task_id(task_name)
def set_task_status(self, task_id: str, new_status: str, status_label: str) -> bool:
'''Change task status. Can fail (return false) if status is unavailable'''
self._ensure_login()
return self._impl.set_task_status(task_id, new_status, status_label)
def fill_metadata(self, task_id: str, data: dict, attributes: list) -> bool:
'''Create metadata. Also changes status for new content'''
self._ensure_login()
return self._impl.fill_metadata(task_id, data, attributes)
def update_metadata(self, task_id: str, data: dict, attributes: list) -> bool:
'''Update metadata'''
self._ensure_login()
return self._impl.update_metadata(task_id, data, attributes)
def get_tasks_data(self, filters: list[list]) -> list:
'''Export data for tasks matching task filter and department filter'''
self._ensure_login()
return self._impl.get_tasks_data(filters)
def get_tasks_content(self, task_ids: list[str]) -> list:
'''Export content for specific task ids'''
self._ensure_login()
return self._impl.get_tasks_content(task_ids)
def _ensure_login(self):
if time.time() - self._time_login < self._login_timeout:
return
logging.info('Relogging at %s', time.strftime('%H:%M:%S'))
self.logout()
self.login()
def load_content(self, content: str, authors: str, text: str, bibliography: str) -> bool:
'''Loads content'''
self._ensure_login()
return self._impl.load_content(content, authors, text, bibliography)
class _UploaderImpl:
"""Implementation of portal data input automation"""
_CONTEXT_CONTENT = 'CRUD EDITOR'
_CONTEXT_SEARCH = 'SEARCH'
_CONTENT_OMITTED = 'Заполните обязательный признак'
_XP_PREFIX_DIALOG = '//span[contains(text(),"{0}")]/parent::*/parent::*/descendant::'
_XP_PREFIX_CONTENT = '//section[@class="article-CRUD__widget"]/descendant::'
_XP_NOTIFICATION_UPDATE_OK = '//*[contains(text(),"{0} успешно отредактирован")]'
_PAGE_PORTAL = 'https://rk.greatbook.ru'
_PAGE_LOGIN = _PAGE_PORTAL
_PAGE_TASK = _PAGE_PORTAL + '/tasks/kanban/all'
_PAGE_CONTENT = _PAGE_PORTAL + '/glossary/all'
_PAGE_TASK_ID = _PAGE_PORTAL + '/tasks/{0}'
_PAGE_CONTENT_BY_TASK = _PAGE_PORTAL + '/widgets?link=task&id={0}'
_XP_LOGIN_TEST = 'app-login__right-container'
_XP_MAIN_ADD_BUTTON = '//button[@class="el-tooltip button-input-add primary"]'
_XP_LIST_ITEM_CONTAINS = '//li[contains(text(),"{0}")]'
_XP_SPAN_CONTAINS = '//span[contains(text(),"{0}")]'
_XP_SPAN_EXACT_MATCH = '//span[text()=" {0} " or text()="{0}"]'
_XP_BUTTON_CONTAINS = '//button[contains(text(),"{0}")]'
_XP_TABLE_ROW = '//div[@class="table__row"]'
_XP_TABLE_CELL = '(descendant::div[@class="table__row-cell sortable"])[{0}]/child::*/child::*'
_XP_TABLE_ROW_EXPAND = 'ancestor::div[@class="table__row-content"]/child::div[@class="table__row-expand"]'
_XP_NOTIFICATION_CLOSE = '//button[@class="notifications-layout__item-close"]'
_XP_NEW_TASK_BTN = '//div[@class="sidebar-tasks"]/button'
_XP_SPAN_STATUS = '//span[@class="task-form__row-status"]'
_XP_LOGIN_USER = '//input[@placeholder="Имя пользователя"]'
_XP_LOGIN_PW = '//input[@placeholder="Пароль"]'
_XP_LOGIN_BTN = '//button[@type="button"]'
_XP_USER_PROFILE = '//div[@class="el-tooltip top-bar__user-profile"]'
_XP_USER_EXIT = '//div[contains(text(),"Выйти")]'
_XP_TASKS_NEXT_BTN = '//i[@class="el-icon el-icon-arrow-right"]'
_XP_TASKS_PAGER = '//ul[@class="el-pager"]'
_XP_TASK_SELECT_STEP = '//span[text()="Выбрать шаг"]/parent::button'
_XP_TASK_CREATE_SUB = '//button[contains(text(),"Создать подзадачу")]'
_XP_TASK_DATA = '//label[text()=" {0} " or text()="{0}"]/following-sibling::div/descendant::span'
_XP_TASK_INPUT = '//label[text()=" {0} " or text()="{0}"]/following-sibling::div/descendant::input'
_XP_TASK_CONTENT_URL = '//a[text()="{0}"]'
_XP_TASK_RADIO_BTNS = '//span[@class="el-radio__inner"]'
_XP_TASK_PAGINATION_CONTROL = '//span[@class="el-pagination__sizes"]/descendant::input'
_XP_INPUT_ACCEPT = 'ancestor::div[@class="deferred-form-input__edit edit-mode"]/descendant::button[@class="button-with-icon-24x24 button-toggle"]'
_XP_DIALOG_LABEL = _XP_PREFIX_DIALOG + 'label[text()=" {1} " or text()="{1}"]'
_XP_DIALOG_INPUT = _XP_DIALOG_LABEL + '/following-sibling::div/descendant::input'
_XP_DIALOG_COMBO = _XP_DIALOG_LABEL + '/following-sibling::div/child::div/child::div'
_XP_DIALOG_TEXT_AREA = _XP_DIALOG_LABEL + '/following-sibling::div/descendant::textarea'
_XP_DIALOG_SWITCH = _XP_PREFIX_DIALOG + 'div[@id="{1}"]'
_XP_DIALOG_BTN_SELECT = '//button[contains(text(),"Выбрать")]'
_XP_DIALOG_BTN_CLEAN = '(//span[@class="tasks-filters-dialog__footer"]/button)[2]'
_XP_DIALOG_BTN_OK = '(//span[@class="tasks-filters-dialog__footer"]/button)[3]'
_XP_DIALOG_BTN_PRIMARY = _XP_PREFIX_DIALOG + 'button[@class="button-dialog button-type-primary"]'
_XP_DIALOG_BTN_CANCEL = _XP_PREFIX_DIALOG + 'button[@aria-label="Close"]'
_XP_CONTENT_LABEL = _XP_PREFIX_CONTENT + 'label[text()=" {0} " or text()="{0}"]'
_XP_CONTENT_INPUT = _XP_CONTENT_LABEL + '/following-sibling::div/descendant::input'
_XP_CONTENT_COMBO = _XP_CONTENT_LABEL + '/following-sibling::div/child::div/child::div'
_XP_CONTENT_TEXT_AREA = _XP_CONTENT_LABEL + '/following-sibling::div/descendant::textarea'
_XP_CONTENT_SWITCH = _XP_PREFIX_CONTENT + 'div[@id="{0}"]'
_XP_CONTENT_BTN_PRIMARY = _XP_PREFIX_CONTENT + 'button[@class="button-dialog button-type-primary"]'
_XP_CONTENT_BTN_CANCEL = _XP_PREFIX_CONTENT + 'button[@class="button-dialog button-type-primary"]'
_XP_CONTENT_BTN_EDIT = '//button[text()="Редактировать" or text()=" Редактировать "]'
_XP_CONTENT_IS_EDITING = '//*[contains(text(),"атрибуты и признаки")]'
_XP_CONTENT_CHAPTER = '//p[contains(text(), "Раздел")]'
_XP_CONTENT_VIEW_LABEL = '//span[@class="view-section__label" and text()="{0}:"]'
_XP_CONTENT_VIEW_VALUE = _XP_CONTENT_VIEW_LABEL + '/following-sibling::span/span'
_XP_CONTENT_TITLE = '//div[@class="top-bar__title-col flex"]/h2[text()="Редактор"]'
_XP_SEARCH_INPUT = '//label[contains(text(),"{0}")]/following-sibling::div/descendant::input'
_XP_SCROLLER_ITEM = '//div[text()=" {0} " or text()="{0}" or text()=" {0} "]'
_XP_SCROLLER_CHECKBOX = 'preceding-sibling::div[@class="tree__node-checkbox"]/label[@class="el-checkbox"]'
_XP_LIST_ITEM = '//li[.="{0}" or .=" {0} "]'
_XP_RELATIVE_BTN_CLOSE = 'descendant::button[@class="catalogs-block__close icon-close-tags"]'
_XP_RELATIVE_BTN_PLUS = 'following-sibling::button[@class="button-input-add"]'
_XP_RELATIVE_OUTPUT_END = 'following-sibling::input[@placeholder="Конец"]'
_XP_SEARCH_QUERY = '//input[@placeholder="Поиск"]'
_XP_SEARCH_BAR = '//div[@class="el-input el-input-group el-input-group--append el-input--suffix"]/input'
_XP_SEARCH_BTN = '//button[@slot="append"]'
_XP_SEARCH_OPTIONS = '//button[@class="tasks-filters__search-btn button-graphic"]'
_XP_SEARCH_NO_RESULTS = '//div[@class="search-list__no-results"]'
_XP_TASKS_VIEW_LIST = '(//button[@class="el-tooltip button-graphic"])[3]'
_XP_SEARCH_MAIN_HREF = '(//span[@class="parent-placeholder el-tooltip"]/parent::*/parent::*/preceding-sibling::*)[5]/descendant::a'
_XP_DIV_TAB = '//p[contains(text(),"{0}")]/parent::*/parent::*'
_XP_CONTENT_BTN_EDIT_TEXT = '//button[contains(text(),"Редактировать раздел")]'
_XP_EDITOR_TEXT_AREA = '//*[@class="ProseMirror"]'
def __init__(self, browser: WebBrowser, config: Config):
self.browser = browser
self._config = config
self._output_context = ''
self._test_run = self._config['Options'].getboolean('TestRun')
def __del__(self):
pass
def login(self) -> bool:
'''Login into Portal'''
self.browser.driver.get(self._PAGE_LOGIN)
if not self.browser.wait_presence(self._XP_LOGIN_TEST, By.CLASS_NAME):
return False
user = self._config['UserData']['UserLogin']
password = decrypt_user(user, self._config['UserData']['Password'], self._config['AppData']['Password'])
self.browser.by_xpath(self._XP_LOGIN_USER).send_keys(user)
self.browser.by_xpath(self._XP_LOGIN_PW).send_keys(password)
self.browser.by_xpath(self._XP_LOGIN_BTN).click()
return self._wait_loading()
def logout(self):
'''Logout from current user'''
try:
self.browser.by_xpath(self._XP_USER_PROFILE).click()
self.browser.by_xpath(self._XP_USER_EXIT).click()
self.browser.wait_ajax()
except NoSuchElementException:
pass
def content_exists(self, content: str) -> bool:
'''Check if content name is taken'''
self._load_page(self._PAGE_CONTENT)
search = self.browser.by_xpath(self._XP_SEARCH_BAR)
search.clear()
search.send_keys(content)
search.send_keys(Keys.RETURN)
self.browser.wait_ajax()
results = self.browser.by_xpath_m(self._XP_SPAN_EXACT_MATCH.format(content))
return len(results) > 0
def content_has_text(self, content: str) -> bool:
'''Check if found content has attached text'''
content_cell = self.browser.by_xpath(self._XP_SPAN_EXACT_MATCH.format(content))
return len(content_cell.find_elements(By.XPATH, self._XP_TABLE_ROW_EXPAND)) > 0
def create_task(self, data: dict) -> bool:
'''Create task in system. Returns empty string on error'''
if not self._open_new_task_form():
return ''
self._output_context = 'Создание задачи'
for field in FieldType:
if field in data:
if field in data:
self._output_field(field, data[field])
self._finalize_output()
self._wait_dialog_done()
return self.find_task_id(data[FieldType.task_name])
def find_task_id(self, task_name: str) -> str:
'''Find task ID or empty string if not found'''
self._load_page(self._PAGE_TASK)
self._list_view()
self._search_task_name(task_name)
for row in self.browser.by_xpath_m(self._XP_TABLE_ROW):
found_name = row.find_element(By.XPATH, self._XP_TABLE_CELL.format(3)).text
if found_name != task_name:
continue
parent = row.find_element(By.XPATH, self._XP_TABLE_CELL.format(4)).text
if parent != 'Нет':
continue
status = row.find_element(By.XPATH, self._XP_TABLE_CELL.format(10)).text
if status == 'Отменена':
continue
reference = row.find_element(By.XPATH, self._XP_TABLE_CELL.format(3))
return reference.get_attribute('href').split('/')[-1]
return ''
def set_task_status(self, task_id: str, new_status: str, status_label: str) -> bool:
'''Change task status. Can fail (return false) if status is unavailable'''
if task_id != '':
self._load_page(self._PAGE_TASK_ID.format(task_id))
self.browser.by_xpath(self._XP_TASK_SELECT_STEP).click()
self.browser.wait_ajax()
status_element = self.browser.by_xpath_m(self._XP_LIST_ITEM_CONTAINS.format(new_status))
if len(status_element) == 0:
return False
if not self._test_run:
status_element[0].click()
time.sleep(self._config['Options'].getint('WaitStatus'))
self.browser.driver.refresh()
self._wait_loading()
self._ensure_no_notifications()
status = self._get_task_status()
if status != status_label:
return False
return True
def fill_metadata(self, task_id: str, data: dict, attributes: list) -> bool:
'''Create metadata. Also changes status for new content'''
if not self._load_task_page(task_id, data[FieldType.task_name]):
return False
if self._has_content():
return False
is_article = data[FieldType.task_type] in ['Статья', 'Актуализация статьи из ЭВ БРЭ']
status = self._get_task_status()
if status == 'Новая':
status = 'Заполнение метаданных статьи' if is_article else 'Написание контента'
status_label = status
if not self.set_task_status('', status, status_label):
return False
if not self._create_content(data, attributes) or not self._wait_loading():
return False
if not self._has_bibliography():
self._create_bibliography_task()
return self._progress_metedata(is_article)
def update_metadata(self, task_id: str, data: dict, attributes: list) -> bool:
'''Update metadata'''
if not self._load_task_page(task_id, data[FieldType.task_name]):
return False
content = self.browser.by_xpath_m(self._XP_TASK_CONTENT_URL.format(data[FieldType.content_name_db]))
if len(content) != 1:
return False
self._load_page(content[0].get_attribute('href'))
return self._update_content(data, attributes)
def get_tasks_data(self, filters: list[list]) -> list:
'''Export data for tasks matching task filter and department filter'''
self._load_page(self._PAGE_TASK)
self._list_view()
self.browser.by_xpath(self._XP_TASK_PAGINATION_CONTROL).click()
self.browser.wait_ajax()
self.browser.by_xpath(self._XP_SPAN_CONTAINS.format('50 на странице')).click()
self.browser.wait_ajax()
if not self._search_all_tasks(filters):
return []
else:
return self._read_all_tasks()
def get_tasks_content(self, task_ids: list[str]) -> list[list[str]]:
'''Export content for specific task ids'''
result = []
for task_id in task_ids:
try:
self._load_page(self._PAGE_CONTENT_BY_TASK.format(task_id))
self.browser.wait_ajax()
title = self.browser.by_xpath_m(self._XP_CONTENT_TITLE)
if len(title) == 0:
continue
result.append(self._read_content(task_id))
self._load_page(self._PAGE_TASK_ID.format(task_id))
self.browser.wait_presence(self._XP_TASK_DATA.format(FieldType.task_manager.to_label()), By.XPATH)
editor = self.browser.by_xpath(self._XP_TASK_DATA.format(FieldType.task_manager.to_label())).text
result[-1].append(editor)
except Exception as e:
logging.info('Exception while reading content %s\n%s', task_id, e)
return result
def load_content(self, content: str, authors: str, text: str, bibliography: str) -> bool:
'''Update metadata
if not self._load_task_page(task_id, data[FieldType.task_name]):
return False
content = self.browser.by_xpath_m(self._XP_TASK_CONTENT_URL.format(data[FieldType.content_name_db]))
if len(content) != 1:
return False
self._load_page(content[0].get_attribute('href'))
return self._update_content(data, attributes)'''
self._load_page(self._PAGE_CONTENT)
search = self.browser.by_xpath(self._XP_SEARCH_BAR)
search.clear()
search.send_keys(content)
search.send_keys(Keys.RETURN)
self._wait_loading()
results = self.browser.by_xpath_m(self._XP_SPAN_EXACT_MATCH.format(content))
if len(results) != 1:
return False
results[0].click()
if not self._wait_loading():
return False
self.browser.wait_ajax()
main_tab = self.browser.by_xpath(self._XP_DIV_TAB.format('Раздел'))
main_tab.click()
self.browser.wait_ajax()
if len(authors) != 0:
self.browser.by_xpath(self._XP_CONTENT_BTN_EDIT).click()
self._wait_loading()
self._add_attrbute("Автор (на портале)")
self._output_field(FieldType.author, authors)
self.browser.by_xpath(self._XP_BUTTON_CONTAINS.format("Сохранить")).click() # test method
else:
self.browser.by_xpath(self._XP_CONTENT_BTN_EDIT).click()
self._wait_loading()
# field = self.browser.by_xpath('//label[text()=" Аффилированные организации " or text()="Аффилированные организации"]/parent::*//span[contains(text(),"Добавьте")]')
# if len(field) != 0:
# self._output_field(FieldType.affiliated_organizations, "Редакция технологий и техники")
self._add_attrbute("Аффилированные организации")
self._output_item("Аффилированные организации", InputMethod.combo_dialog, ["Редакция технологий и техники"]) # remove feature
self.browser.by_xpath(self._XP_BUTTON_CONTAINS.format("Сохранить")).click() # test method
self.browser.by_xpath(self._XP_BUTTON_CONTAINS.format("Отменить")).click() # test method
# parse data_reader module
# разобрать на методы заведение авторов, библиографии, текст
if text != '':
self.browser.by_xpath(self._XP_CONTENT_BTN_EDIT_TEXT).click()
self.browser.wait_ajax()
textarea = self.browser.by_xpath(self._XP_EDITOR_TEXT_AREA)
textarea.click()
textarea.send_keys(text)
# self.browser.by_xpath(self._XP_BUTTON_CONTAINS.format("Сохранить")).click()
return True
def _load_page(self, page_url: str):
self.browser.driver.get(page_url)
if not self._wait_loading():
raise BaseException(f"Cannot load page {page_url}")
if not self._login_required():
return
if not self.login():
raise BaseException("Cannot login")
self.browser.driver.get(page_url)
if not self._wait_loading():
raise BaseException(f"Cannot load page {page_url}")
def _login_required(self) -> bool:
return len(self.browser.driver.find_elements(By.CLASS_NAME, self._XP_LOGIN_TEST)) != 0
def _has_content(self) -> bool:
return len(self.browser.by_xpath_m(self._XP_BUTTON_CONTAINS.format('+ Создать'))) == 0
def _is_content_editing(self) -> bool:
elems = self.browser.by_xpath_m(self._XP_CONTENT_IS_EDITING)
return len(elems) > 0
def _has_bibliography(self):
return len(self.browser.by_xpath_m(self._XP_SPAN_CONTAINS.format('Раздел библиография'))) != 0
def _get_task_status(self) -> str:
return self.browser.by_xpath(self._XP_SPAN_STATUS).text
def _wait_dialog_done(self):
time.sleep(self._config['Options'].getint('WaitData'))
def _wait_loading(self) -> bool:
if not self.browser.wait_presence(self._XP_MAIN_ADD_BUTTON, By.XPATH):
return False
self.browser.wait_ajax()
return True
def _ensure_no_notifications(self):
while True:
close_btns = self.browser.by_xpath_m(self._XP_NOTIFICATION_CLOSE)
if len(close_btns) == 0:
return
try:
close_btns[0].click()
except ElementNotInteractableException:
pass
self.browser.wait_ajax()
def _list_view(self):
self.browser.by_xpath(self._XP_TASKS_VIEW_LIST).click()
self.browser.wait_ajax()
def _load_task_page(self, task_id: str, task_name: str) -> bool:
self._load_page(self._PAGE_TASK_ID.format(task_id))
task_header = self.browser.by_xpath('//h3').text
return task_header == task_name
def _search_task_name(self, task_name: str):
search = self.browser.by_xpath(self._XP_SEARCH_BAR)
search.clear()
search.click()
search.send_keys(task_name)
options = self.browser.by_xpath(self._XP_SEARCH_OPTIONS)
options.click()
self.browser.wait_ajax()
self.browser.wait_presence(self._XP_DIALOG_BTN_CLEAN, By.XPATH)
self.browser.by_xpath(self._XP_DIALOG_BTN_CLEAN).click()
self.browser.wait_ajax()
self.browser.by_xpath(self._XP_SPAN_CONTAINS.format('Точное совпадение')).click()
self.browser.by_xpath(self._XP_DIALOG_BTN_OK).click()
self.browser.wait_ajax()
def _search_all_tasks(self, filters: list[list]):
search = self.browser.by_xpath(self._XP_SEARCH_BAR)
search.clear()
options = self.browser.by_xpath(self._XP_SEARCH_OPTIONS)
options.click()
self.browser.wait_ajax()
self.browser.wait_presence(self._XP_DIALOG_BTN_CLEAN, By.XPATH)
self.browser.by_xpath(self._XP_DIALOG_BTN_CLEAN).click()
self.browser.wait_ajax()
self._output_context = self._CONTEXT_SEARCH
for filter_id in range(FilterType.task_type, FilterType.observer):
label = FilterType(filter_id).to_label()
for value in filters[filter_id]:
if value != '':
self._output_combobox(label, value)
if filters[FilterType.created_begin] != '':
self._output_date_range(FilterType.created_begin.to_label(), filters[FilterType.created_begin], filters[FilterType.created_end])
if filters[FilterType.target_begin] != '':
self._output_date_range(FilterType.target_begin.to_label(), filters[FilterType.target_begin], filters[FilterType.target_end])
self.browser.by_xpath(self._XP_DIALOG_BTN_OK).click()
self.browser.wait_ajax()
no_result = self.browser.by_xpath(self._XP_SEARCH_NO_RESULTS)
return not no_result.is_displayed()
def _read_all_tasks(self) -> list:
result = []
next_btn = self.browser.by_xpath(self._XP_TASKS_NEXT_BTN)
count_pages = int(self.browser.by_xpath(self._XP_TASKS_PAGER).find_elements(By.XPATH, 'descendant::li')[-1].text)
for _ in range(count_pages):
result += self._scan_tasks_table()
next_btn.click()
self.browser.wait_ajax()
time.sleep(random.randint(0, 100)/100)
self._ensure_no_notifications()
return result
def _scan_tasks_table(self) -> list:
result = []
for row in self.browser.by_xpath_m(self._XP_TABLE_ROW):
task_type = self._read_cell(row, 2).text
task_name = self._read_cell(row, 3).text
task_id = self._read_cell(row, 3).get_attribute('href').split('/')[-1]
parent = self._read_cell(row, 4)
if parent.text != 'Нет':
parent_id = parent.find_element(By.XPATH, 'descendant::a').get_attribute('href').split('/')[-1]
else:
parent_id = 'Нет'
content_name = self._read_cell(row, 6).text
target_date = self._read_cell(row, 7).text
supervisor = self._read_cell(row, 8).text
worker = self._read_cell(row, 9).text
status = self._read_cell(row, 10).text
result.append([task_type, status, content_name, supervisor, worker, target_date, task_id, task_name, parent_id])
return result
def _read_content(self, task_id: str) -> list:
editorial = self._read_field(FieldType.editorial)
if editorial.startswith('Редакция '):
editorial = editorial[len('Редакция '):]
result = [
task_id,
self._read_field(FieldType.biblio_name),
self._read_field(FieldType.change_score),
editorial,
self._read_field(FieldType.responsible),
self._read_field(FieldType.definition),
self._read_field(FieldType.object_type),
self._read_field(FieldType.markers),
self._read_field(FieldType.tags),
self._read_field(FieldType.source),
self._read_field(FieldType.electron_bre),
self._read_field(FieldType.main_page),
self._read_field(FieldType.is_general),
self._read_field(FieldType.actualize_period),
self._read_field(FieldType.age_restriction)
]
chapters = self.browser.by_xpath_m(self._XP_CONTENT_CHAPTER)
if len(chapters) == 0:
author = ''
else:
chapters[0].click()
self.browser.wait_ajax()
author = self._read_field(FieldType.author)
result.append(author)
return result
def _create_content(self, data: dict, attributes: list) -> bool:
self._ensure_no_notifications()
if not self.browser.wait_clickable(self._XP_BUTTON_CONTAINS.format('+ Создать'), By.XPATH):
return False
self.browser.by_xpath(self._XP_BUTTON_CONTAINS.format('+ Создать')).click()
self.browser.wait_ajax()
self._output_context = 'Создать контент'
self._output_content_data(data, attributes)
self._wait_dialog_done()
if self._test_run:
return True
return self._has_content()
def _update_content(self, data: dict, attributes: list) -> bool:
''' updates meta-data '''
btn_edit = self.browser.by_xpath_m(self._XP_CONTENT_BTN_EDIT)
if len(btn_edit) != 1:
logging.warning('Не доступна кнопка Редактировать на странице контента')
return False
btn_edit[0].click()
if not self._wait_loading() or not self._is_content_editing():
return False
self._output_context = self._CONTEXT_CONTENT
self._output_content_data(data, attributes)
notifications = self.browser.by_xpath_m(self._XP_NOTIFICATION_UPDATE_OK.format(data[FieldType.content_name_db]))
return len(notifications) > 0
def _create_bibliography_task(self):
self._ensure_no_notifications()
executor = self.browser.by_xpath_m(self._XP_TASK_DATA.format(FieldType.executor.to_label()))[0].text
supervisor = self.browser.by_xpath_m(self._XP_TASK_DATA.format(FieldType.supervisor.to_label()))[0].text
department = self.browser.by_xpath_m(self._XP_TASK_DATA.format(FieldType.department.to_label()))[0].text
date_target = self.browser.by_xpath_m(self._XP_TASK_DATA.format(FieldType.date_target.to_label()))[0].text
self._output_context = 'Создание подзадачи'
self.browser.by_xpath(self._XP_TASK_CREATE_SUB).click()
self.browser.by_xpath_m(self._XP_TASK_RADIO_BTNS)[0].click()
self.browser.by_xpath(self._XP_DIALOG_BTN_PRIMARY.format(self._output_context)).click()
self.browser.wait_ajax()
self._output_field(FieldType.task_type, 'Раздел библиография')
self._output_field(FieldType.executor, executor)
self._output_field(FieldType.supervisor, supervisor)
self._output_field(FieldType.department, department)
self._output_field(FieldType.date_target, date_target)
self._finalize_output()
self._wait_dialog_done()
def _progress_metedata(self, is_article: bool) -> bool:
if is_article:
if not self.set_task_status('', 'В службу', 'Ожидание проверки службой "Словник"'):
return False
new_executor = self._config['AppData']['SlovnikExecutor']
else:
if not self.set_task_status('', 'Заведующему редакцией', 'Проверка Заведующим редакцией'):
return False
new_executor = self.browser.by_xpath_m(self._XP_TASK_DATA.format(FieldType.supervisor.to_label()))[0].text
self._ensure_no_notifications()
self._output_task_combo(FieldType.executor.to_label(), new_executor)
return True
def _read_cell(self, row: WebElement, column: int) -> WebElement:
return row.find_element(By.XPATH, self._XP_TABLE_CELL.format(column))
def _read_field(self, field: FieldType) -> str:
label = field.to_label()
input_method = field.input_method()
return self._read_item(label, input_method)
def _read_item(self, field_label: str, input_method: InputMethod) -> str:
xpath = self._XP_CONTENT_VIEW_VALUE.format(field_label)
retries = 3
value = ''
while True:
elems = self.browser.by_xpath_m(xpath)
if len(elems) == 0:
return ''
value = elems[0].text
if (self._validate_input(value) or retries == 0):
break
retries = retries - 1
time.sleep(0.5)
value = value.replace('\xad', '#')
value = value.replace('\xd7', '#')
if value == self._CONTENT_OMITTED:
return ''
if input_method in [InputMethod.combo_box, InputMethod.combo_dialog, InputMethod.combo_dialog_simple_list]:
value = _replace_comma_separators(value)
return value
def _validate_input(self, value: str) -> bool:
if value == '...':
return False
if value == 'Загрузка...':
return False
return True
def _open_new_task_form(self) -> bool:
self._load_page(self._PAGE_TASK)
self.browser.by_xpath(self._XP_NEW_TASK_BTN).click()
return self.browser.wait_presence(self._XP_DIALOG_BTN_PRIMARY.format('Создание задачи'), By.XPATH)
def _output_content_data(self, data: dict, attributes: list):
for field in FieldType:
if field in data:
self._output_field(field, data[field])
self.browser.wait_ajax()
for attr in attributes:
self._output_attribute(attr[0], attr[1], attr[2])
self.browser.wait_ajax()
self._finalize_output()
self.browser.wait_ajax()
def _finalize_output(self):
self._ensure_no_notifications()
btn_path = self._xpath_finalize_btn(self._output_context)
self.browser.by_xpath(btn_path).click()
self.browser.wait_ajax()
def _output_field(self, field: FieldType, value):
label = field.to_label()
input_method = field.input_method()
self._output_item(label, input_method, value)
def _output_attribute(self, name: str, input_method: InputMethod, value):
if not self._test_output_exists(name):
self._add_attrbute(name)
self._output_item(name, input_method, value)
def _add_attrbute(self, name: str):
self._ensure_no_notifications()
self.browser.by_xpath(self._XP_BUTTON_CONTAINS.format('Добавить признак')).click()
self.browser.wait_ajax()
self._select_dialog_item(name)
def _output_item(self, field_label: str, input_method: InputMethod, value):
if value == '':
return
if input_method == InputMethod.read_only:
return
if not self._test_output_exists(field_label):
return
if input_method == InputMethod.combo_box:
self._output_combobox(field_label, value)
elif input_method == InputMethod.text_input:
self._output_text_input(field_label, value)
elif input_method == InputMethod.text_area:
self._output_text_area(field_label, value)
elif input_method == InputMethod.date_picker:
self._output_date(field_label, value)
elif input_method == InputMethod.bool_toggle:
self._output_bool(field_label, value)
elif input_method == InputMethod.combo_dialog_simple_list:
self._output_dialog(field_label, value, False)
elif input_method == InputMethod.combo_dialog:
if not isinstance(value, list):
raise "List argument expected. Given: %s" % type(value)
self._output_dialog(field_label, value, True)
def _test_output_exists(self, field_label: str) -> bool:
xpath = self._xpath_label(field_label, self._output_context)
elems = self.browser.by_xpath_m(xpath)
return len(elems) > 0
def _output_bool(self, field_label: str, value: bool):
bool_switch = self.browser.by_xpath(self._xpath_switch(field_label, self._output_context))
current_value = 'is-checked' in bool_switch.get_attribute('class')
if current_value != value:
bool_switch.click()
def _output_text_input(self, field_label: str, value: str):
self._output(self._xpath_input(field_label, self._output_context), value)
def _output_text_area(self, field_label: str, value: str):
self._output(self._xpath_textarea(field_label, self._output_context), value)
def _output_combobox(self, field_label: str, value: str):
if not self._output(self._xpath_input(field_label, self._output_context), value):
return
self.browser.wait_ajax()
list_item = self._find_active_list(value)
list_item.click()
self.browser.wait_ajax()
self.browser.send_esc()
def _output_date_range(self, field_label: str, value1: str, value2: str):
element = self.browser.by_xpath(self._xpath_input(field_label, self._output_context))
element.click()
element.send_keys(value1)
element = element.find_element(By.XPATH, self._XP_RELATIVE_OUTPUT_END)
element.click()
element.send_keys(value2)
self.browser.wait_ajax()
self.browser.send_enter()
def _output_task_combo(self, label: str, value: str):
element = self.browser.by_xpath_m(self._XP_TASK_DATA.format(label))[0]
element.click()
self.browser.wait_ajax()
element = self.browser.by_xpath_m(self._XP_TASK_INPUT.format(label))[0]
element.send_keys(value)
self.browser.wait_ajax()
list_item = self._find_active_list(value)
list_item.click()
self.browser.wait_ajax()
accept_btn = element.find_element(By.XPATH, self._XP_INPUT_ACCEPT)
if not self._test_run:
accept_btn.click()
self.browser.wait_ajax()
def _output_date(self, field_label: str, value: str):
self._output(self._xpath_input(field_label, self._output_context), value)
def _output_dialog(self, field_label, values: list[str], search_flag=True):
self._ensure_no_notifications()
item = self.browser.by_xpath(self._xpath_combo(field_label, self._output_context))
remove_btns = item.find_elements(By.XPATH, self._XP_RELATIVE_BTN_CLOSE)
for existing_item in remove_btns:
existing_item.click()
btn_plus = item.find_element(By.XPATH, self._XP_RELATIVE_BTN_PLUS)
for value in values:
btn_plus.click()
self.browser.wait_ajax()
self._select_dialog_item(value, search_flag)
def _output(self, xpath: str, value: str) -> bool:
element = self.browser.by_xpath(xpath)
element.click()
try:
element.clear()
except InvalidElementStateException:
pass
element.send_keys(value)
return True
def _find_active_list(self, value: str):
list_items = self.browser.by_xpath_m(self._XP_LIST_ITEM.format(value))
count_active = sum(1 for x in list_items if x.is_displayed())
if count_active > 1:
logging.info('%s multiple list results', value)
for list_item in list_items:
if list_item.is_displayed():
return list_item
logging.info('Cannot find list item: %s', value)
return None
def _find_active_scroller(self, value: str):
list_items = self.browser.by_xpath_m(self._XP_SCROLLER_ITEM.format(value))
count_active = sum(1 for x in list_items if x.is_displayed())
if count_active == 0:
logging.warning('Cannot find checkbox list item: %s', value)
raise NoSuchElementException
if count_active > 1:
logging.info('%s multiple list results', value)
for list_item in list_items:
checkbox = list_item.find_elements(By.XPATH, self._XP_SCROLLER_CHECKBOX)
if len(checkbox) > 0 and list_item.is_displayed():
return list_item
return None
def _select_dialog_item(self, value: str, search_flag=True):
if search_flag:
search = self.browser.by_xpath(self._XP_SEARCH_QUERY)
search.send_keys(value)
search.send_keys(Keys.ENTER)
self.browser.wait_ajax()
time.sleep(1) # additional wait for list to load
list_item = self._find_active_scroller(value)
list_item.click()
self.browser.by_xpath(self._XP_DIALOG_BTN_SELECT).click()
self.browser.wait_ajax()
def _xpath_label(self, field_label: str, context: str) -> str:
if context == self._CONTEXT_CONTENT:
return self._XP_CONTENT_LABEL.format(field_label)
else:
return self._XP_DIALOG_LABEL.format(context, field_label)
def _xpath_input(self, field_label: str, context: str) -> str:
if context == self._CONTEXT_CONTENT:
return self._XP_CONTENT_INPUT.format(field_label)
elif context == self._CONTEXT_SEARCH:
return self._XP_SEARCH_INPUT.format(field_label)
else:
return self._XP_DIALOG_INPUT.format(context, field_label)
def _xpath_textarea(self, field_label: str, context: str) -> str:
if context == self._CONTEXT_CONTENT:
return self._XP_CONTENT_TEXT_AREA.format(field_label)
else:
return self._XP_DIALOG_TEXT_AREA.format(context, field_label)
def _xpath_combo(self, field_label: str, context: str) -> str:
if context == self._CONTEXT_CONTENT:
return self._XP_CONTENT_COMBO.format(field_label)
else:
return self._XP_DIALOG_COMBO.format(context, field_label)
def _xpath_switch(self, field_label: str, context: str) -> str:
if context == self._CONTEXT_CONTENT:
return self._XP_CONTENT_SWITCH.format(field_label)
else:
return self._XP_DIALOG_SWITCH.format(context, field_label)
def _xpath_finalize_btn(self, context: str) -> str:
if not self._test_run:
if context == self._CONTEXT_CONTENT:
return self._XP_CONTENT_BTN_PRIMARY
else:
return self._XP_DIALOG_BTN_PRIMARY.format(context)
else:
if context == self._CONTEXT_CONTENT:
return self._XP_CONTENT_BTN_CANCEL
else:
return self._XP_DIALOG_BTN_CANCEL.format(context)

View File

@ -0,0 +1,15 @@
@echo off
venv\Scripts\pyinstaller --clean --onefile --distpath ./bin --hidden-import pandas._libs.tslibs.timedeltas, --hidden-import pandas._libs.tslibs.base menu.py
del menu.spec
rmdir /S /Q build
rmdir /S /Q __pycache__
venv\Scripts\pyinstaller --clean --onefile --distpath ./bin --hidden-import pandas._libs.tslibs.timedeltas, --hidden-import pandas._libs.tslibs.base exporter.py
del exporter.spec
rmdir /S /Q build
rmdir /S /Q __pycache__
exit

BIN
webapi/requirements.txt Normal file

Binary file not shown.

BIN
webapi/requirements_dev.txt Normal file

Binary file not shown.

33
webapi/run_cardslot.py Normal file
View File

@ -0,0 +1,33 @@
'''Cardslot manipulation'''
import sys
import logging
import portal as bre
DEFAULT_OUTPUT = 'output.csv'
if __name__ == '__main__':
import argparse
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
parser = argparse.ArgumentParser(description='BRE automation parameters')
parser.add_argument('filename')
parser.add_argument('config', help='Configuration INI file')
parser.add_argument('input', help='Input xlsx file')
parser.add_argument('password', help='Password to access encrypted user data')
parser.add_argument('-check', '-c', action='store_true', help='Optional flag to only check existense')
args = parser.parse_args(sys.argv)
config = bre.read_config(args.config)
config['AppData']['Password'] = args.password
portal = bre.PortalAPI(config)
if not portal.set_input(args.input):
sys.exit()
if not portal.set_output_tasks(DEFAULT_OUTPUT):
sys.exit()
if args.check:
sys.exit(portal.check_existence())
else:
sys.exit(portal.import_cardslots())

56
webapi/run_loader.py Normal file
View File

@ -0,0 +1,56 @@
'''Run electron BRE loading'''
import sys
import time
import portal
START_ROUND = 1
NUMBER_OF_ROUNDS = 10
NUMBER_OF_ARTICLES = 10
CONTINUE_FLAG = False
START_ID = '5052657'
STOP_ID = ''
SAVE_TO_PATH = 'PATH_TO_SAVE_FOLDER'
LOGIN_NAME = 'LOGIN'
LOGIN_PW = 'PASSWORD'
# MAIN START
curr_id = START_ID
print("Start time is", time.strftime("%H") + ":" + time.strftime("%M"))
chrome_options = portal.Options()
chrome_options.add_experimental_option("prefs", {"download.default_directory": SAVE_TO_PATH, "download.prompt_for_download": False, })
chrome_options.add_argument('--headless')
chrome = portal.WebBrowser()
for round_number in range(NUMBER_OF_ROUNDS):
if curr_id == '':
sys.exit(0)
print("*********************************")
print("** round #" + str(round_number + START_ROUND) + " of " + str(START_ROUND + NUMBER_OF_ROUNDS - 1))
print("** start time is", time.strftime("%H") + ":" + time.strftime("%M"))
print("*********************************")
chrome.start_chrome(chrome_options)
processor = portal.GBDownloader(chrome, SAVE_TO_PATH, CONTINUE_FLAG, suffix_seed = round_number + START_ROUND)
if not processor.login(LOGIN_NAME, LOGIN_PW):
sys.exit(0)
curr_id = processor.scan(curr_id, STOP_ID, NUMBER_OF_ARTICLES)
time.sleep(2)
del processor
chrome.stop_chrome()
time.sleep(3)
print("** end time is", time.strftime("%H") + ":" + time.strftime("%M"))
print("")
print("next start article ID = " + curr_id)
print("next START_ROUND = " + str(START_ROUND + NUMBER_OF_ROUNDS))

26
webapi/run_metadata.py Normal file
View File

@ -0,0 +1,26 @@
'''Metadata manipulations'''
import sys
import logging
import portal as bre
if __name__ == '__main__':
import argparse
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
parser = argparse.ArgumentParser(description='BRE automation parameters')
parser.add_argument('filename')
parser.add_argument('config', help='Configuration INI file')
parser.add_argument('input', help ='Input xlsx file')
parser.add_argument('password', help='Password to access encrypted user data')
args = parser.parse_args(sys.argv)
config = bre.read_config(args.config)
config['AppData']['Password'] = args.password
portal = bre.PortalAPI(config)
if not portal.set_input(args.input):
sys.exit()
sys.exit(portal.import_meta())

29
webapi/test.ini Normal file
View File

@ -0,0 +1,29 @@
[AppData]
FilterTask=Актуализация статьи из ЭВ БРЭ;Микропонятие
FilterDepartment=Редакция технологий и техники;Редакция энергетики, промышленности
FilterStatus=
FilterResponsible=
FilterSupervisor=
FilterExecutor=
FilterObserver=
FilterDateCreatedBegin=01.01.2023
FilterDateCreatedEnd=01.02.2024
FilterDateTargetBegin=01.01.2023
FilterDateTargetEnd=01.01.2024
Output=output_tasks.csv
ScanContent=true
ScanTasks=false
ExcludeID=1234
IncludeID=60143e42-8d2a-4d96-8e5f-5217b5d824c6
OutputContent=output_content.csv
AccessToken=gAAAAABiY8Q8m1Ef6gd_UO3KIVjvSshJWGWaSGZQ8uqYe-Yn2eGH4kYQcPGxZ4g7iKt4w0L4-YFDEkLp9V6PkJHrB6kx0qb3hA==
[Options]
Debug=false
TestRun=false
LoginTimeout=900
[UserData]
User=Борисов Иван Романович
UserLogin=i.borisov
Password=gAAAAABlI6fdFAeVM8UhkPYJaOP0pVhE6_GCF91EyY8kaFEyrgkr4SAm3lm4INIXPEQowAlvBr3M9uNtVwjqlGWIAiJPoKNeAg5sDOOP5lpZLYxy4HgdheY=