Compare commits
20 Commits
f5cf4c32da
...
prod
| Author | SHA1 | Date | |
|---|---|---|---|
| 013c9c5a15 | |||
| b3e1a5c88c | |||
| 8e757afe39 | |||
| 5100c5d17c | |||
| 81d33bebef | |||
| f7ed20de0e | |||
| 3500d433ea | |||
| c713b47d58 | |||
| ddd0ffbcb0 | |||
| 36a8548562 | |||
| 995b539a67 | |||
| 2515294c56 | |||
| 88d43124f7 | |||
| 56db68768b | |||
| b9e248c02e | |||
| a73c714d88 | |||
| 25e53aa84e | |||
| ae0290145a | |||
| d21b3b3214 | |||
| 008b2e57f2 |
@@ -7,19 +7,41 @@ on:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: [docker:host]
|
||||
runs-on: [docker, host]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Prepare workspace
|
||||
run: |
|
||||
rm -rf /tmp/olaper || true
|
||||
mkdir -p /tmp/olaper
|
||||
cd /tmp/olaper
|
||||
apk update && apk add openssh docker-cli
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
ssh-keyscan -p 2222 10.25.100.250 >> ~/.ssh/known_hosts
|
||||
git clone --branch prod ssh://git@10.25.100.250:2222/serty/olaper.git .
|
||||
|
||||
- name: Build Docker image
|
||||
run: |
|
||||
cd /tmp/olaper
|
||||
docker build -t olaper:latest .
|
||||
|
||||
- name: Stop old container (if running)
|
||||
- name: Create volume (if not exists)
|
||||
run: |
|
||||
if [ "$(docker ps -q -f name=olaper)" ]; then
|
||||
docker stop olaper && docker rm olaper
|
||||
docker volume create olaper_data || true
|
||||
|
||||
- name: Stop and remove old containers
|
||||
run: |
|
||||
# Stop and remove container named olaper if exists
|
||||
docker stop olaper || true
|
||||
docker rm olaper || true
|
||||
|
||||
# Stop and remove any container using port 5005
|
||||
PORT=5005
|
||||
CONTAINER_IDS=$(docker ps -q --filter "publish=$PORT")
|
||||
if [ -n "$CONTAINER_IDS" ]; then
|
||||
echo "Stopping containers using port $PORT..."
|
||||
docker stop $CONTAINER_IDS
|
||||
docker rm $CONTAINER_IDS
|
||||
fi
|
||||
|
||||
- name: Run new container
|
||||
@@ -30,4 +52,8 @@ jobs:
|
||||
-p 5005:5005 \
|
||||
-e SECRET_KEY=${{ secrets.SECRET_KEY }} \
|
||||
-e ENCRYPTION_KEY=${{ secrets.ENCRYPTION_KEY }} \
|
||||
-v olaper_data:/opt/olaper/data \
|
||||
olaper:latest
|
||||
|
||||
- name: Cleanup
|
||||
run: rm -rf /tmp/olaper || true
|
||||
@@ -39,6 +39,7 @@ jobs:
|
||||
docker run -d \
|
||||
--name olaper_test \
|
||||
-p 5050:5005 \
|
||||
-v /home/master/olaper-debug/data:/opt/olaper/data \
|
||||
-e SECRET_KEY=${{ secrets.SECRET_KEY }} \
|
||||
-e ENCRYPTION_KEY=${{ secrets.ENCRYPTION_KEY }} \
|
||||
olaper:test
|
||||
|
||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
93
routes.py
93
routes.py
@@ -328,32 +328,103 @@ def render_olap():
|
||||
if req_module.login():
|
||||
result = req_module.take_olap(json_body)
|
||||
|
||||
# --- НАЧАЛО НОВОЙ УЛУЧШЕННОЙ ЛОГИКИ ОБРАБОТКИ ДАННЫХ ---
|
||||
|
||||
if 'data' not in result or not isinstance(result['data'], list):
|
||||
flash(_('Error: Unexpected response format from RMS for report "%(name)s".', name=preset.get('name', report_id)), 'error')
|
||||
current_app.logger.error(f"Unexpected API response for report {report_id} ('{preset.get('name')}'). Response: {result}")
|
||||
return redirect(url_for('.index'))
|
||||
|
||||
data_to_insert = []
|
||||
if result['data']:
|
||||
headers = list(result['data'][0].keys())
|
||||
data_to_insert.append(headers)
|
||||
for item in result['data']:
|
||||
data_to_insert.append([item.get(h) for h in headers])
|
||||
report_data = result['data']
|
||||
|
||||
# Если отчет пуст, очищаем лист и уведомляем пользователя.
|
||||
if not report_data:
|
||||
gs_client.clear_and_write_data(sheet_title, [])
|
||||
flash(_('Report "%(name)s" returned no data for the selected period. Sheet "%(sheet)s" has been cleared.', name=preset.get('name', report_id), sheet=sheet_title), 'warning')
|
||||
return redirect(url_for('.index'))
|
||||
|
||||
# Здесь будет храниться наш итоговый "плоский" список словарей
|
||||
processed_data = []
|
||||
|
||||
# Проверяем структуру отчета: сводный (pivoted) или простой (flat)
|
||||
first_item = report_data[0]
|
||||
is_pivoted = isinstance(first_item, dict) and 'row' in first_item and 'cells' in first_item
|
||||
|
||||
if is_pivoted:
|
||||
current_app.logger.info(f"Processing a pivoted report: {preset.get('name', report_id)}")
|
||||
# "Разворачиваем" (unpivot) данные в плоский список словарей
|
||||
for row_item in report_data:
|
||||
row_values = row_item.get('row', {})
|
||||
cells = row_item.get('cells', [])
|
||||
if not cells:
|
||||
# Обрабатываем строки, у которых может не быть данных в ячейках
|
||||
processed_data.append(row_values.copy())
|
||||
else:
|
||||
for cell in cells:
|
||||
new_flat_row = row_values.copy()
|
||||
new_flat_row.update(cell.get('col', {}))
|
||||
new_flat_row.update(cell.get('values', {}))
|
||||
processed_data.append(new_flat_row)
|
||||
else:
|
||||
current_app.logger.info(f"Processing a simple flat report: {preset.get('name', report_id)}")
|
||||
# Данные уже в виде плоского списка, просто присваиваем
|
||||
processed_data = [item for item in report_data if isinstance(item, dict)]
|
||||
|
||||
# --- Универсальное формирование заголовков и данных ---
|
||||
|
||||
# 1. Собираем все уникальные ключи из всех строк для гарантии целостности.
|
||||
all_keys = set()
|
||||
for row in processed_data:
|
||||
all_keys.update(row.keys())
|
||||
|
||||
# 2. Создаем упорядоченный список заголовков для лучшей читаемости.
|
||||
# Используем поля из пресета для определения логического порядка.
|
||||
row_group_fields = preset.get('groupByRowFields', [])
|
||||
col_group_fields = preset.get('groupByColFields', [])
|
||||
agg_fields = preset.get('aggregateFields', [])
|
||||
|
||||
ordered_headers = []
|
||||
# Сначала добавляем известные поля из пресета в логической последовательности.
|
||||
for field in row_group_fields + col_group_fields + agg_fields:
|
||||
if field in all_keys:
|
||||
ordered_headers.append(field)
|
||||
all_keys.remove(field)
|
||||
# Добавляем любые другие (неожиданные) поля, отсортировав их по алфавиту.
|
||||
ordered_headers.extend(sorted(list(all_keys)))
|
||||
|
||||
# 3. Собираем итоговый список списков для Google Sheets, приводя все значения к строкам.
|
||||
data_to_insert = [ordered_headers]
|
||||
for row in processed_data:
|
||||
row_data = []
|
||||
for header in ordered_headers:
|
||||
value_str = str(row.get(header, ''))
|
||||
if value_str.startswith(('=', '+', '-', '@')):
|
||||
row_data.append("'" + value_str)
|
||||
else:
|
||||
row_data.append(value_str)
|
||||
# Преобразуем None в пустую строку, а все остальное в строковое представление.
|
||||
# Это предотвращает потенциальные ошибки типов со стороны Google Sheets API.
|
||||
data_to_insert.append(row_data)
|
||||
|
||||
|
||||
gs_client.clear_and_write_data(sheet_title, data_to_insert)
|
||||
|
||||
if len(data_to_insert) > 1:
|
||||
flash(_('Report "%(name)s" data successfully written to sheet "%(sheet)s".', name=preset.get('name', report_id), sheet=sheet_title), 'success')
|
||||
else:
|
||||
flash(_('Report "%(name)s" returned no data for the selected period. Sheet "%(sheet)s" has been cleared.', name=preset.get('name', report_id), sheet=sheet_title), 'warning')
|
||||
rows_count = len(data_to_insert) - 1
|
||||
flash(_('Report "%(name)s" data (%(rows)s rows) successfully written to sheet "%(sheet)s".',
|
||||
name=preset.get('name', report_id),
|
||||
rows=rows_count,
|
||||
sheet=sheet_title), 'success')
|
||||
else:
|
||||
flash(_('Error authorizing on RMS server when trying to get a report.'), 'error')
|
||||
|
||||
except ValueError as ve:
|
||||
flash(_('Data Error: %(error)s', error=str(ve)), 'error')
|
||||
except gspread.exceptions.APIError as api_err:
|
||||
flash(_('Google API Error accessing sheet "%(sheet)s". Check service account permissions.', sheet=sheet_title), 'error')
|
||||
flash(_('Google API Error accessing sheet "%(sheet)s". Check service account permissions.', sheet=sheet_title or _('Unknown')), 'error')
|
||||
current_app.logger.error(f"Google API Error for sheet '{sheet_title}': {api_err}", exc_info=True)
|
||||
except Exception as e:
|
||||
flash(_('An unexpected error occurred: %(error)s', error=str(e)), 'error')
|
||||
current_app.logger.error(f"Unexpected error in render_olap: {e}", exc_info=True)
|
||||
finally:
|
||||
if req_module and req_module.token:
|
||||
req_module.logout()
|
||||
|
||||
Reference in New Issue
Block a user