работа с публикацией

This commit is contained in:
2025-12-20 12:19:33 +03:00
parent e83647042f
commit b1e99277d1
16 changed files with 1344 additions and 165 deletions
BIN
View File
Binary file not shown.
+175 -77
View File
@@ -1,11 +1,14 @@
from flask import Flask, request, jsonify from flask import Flask, request, jsonify, render_template
from config import Config from config import Config
from db import VkAPI, db, MedodsAPI, ApiEndpoint from db import PostScheduler, UsersMedods, VkAPI, VkPost, db, MedodsAPI, ApiEndpoint
from scheduler import start_scheduler from medods_handler import updateMedodsUsers
from scheduler import enable_publish_job, get_scheduler_status, start_scheduler
from http_client import send_request from http_client import send_request
import logging import logging
import os import os
from vk_handler import handle_vk_post
app = Flask(__name__) app = Flask(__name__)
app.config.from_object(Config) app.config.from_object(Config)
@@ -24,7 +27,13 @@ logging.basicConfig(
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from flask import render_template
@app.before_request
def init():
db.create_all()
start_scheduler()
enable_publish_job()
logger.info("Приложение запущено")
@app.route("/") @app.route("/")
@@ -47,7 +56,7 @@ def medods():
return render_template("medods.html", data=data) return render_template("medods.html", data=data)
@app.route("/vk") @app.route("/vk", methods=["GET"])
def vk(): def vk():
vkDB = VkAPI.query.first() vkDB = VkAPI.query.first()
data = {} data = {}
@@ -56,63 +65,85 @@ def vk():
return render_template("vk.html", data=data) return render_template("vk.html", data=data)
@app.route("/posts") @app.route("/posts", methods=["GET"])
def posts(): def posts():
return render_template("posts.html") medodsUsers = UsersMedods.query.all()
if len(medodsUsers) > 0:
medodsUsers = [user.toDict() for user in medodsUsers]
vkPost = VkPost.query.first()
if vkPost:
vkPost = vkPost.toDict()
schedulerStatus = get_scheduler_status()
schedulerSettings = PostScheduler.query.first()
if schedulerSettings:
schedulerSettings = schedulerSettings.toDict()
return render_template(
"posts.html",
data={
"medodsUsers": medodsUsers,
"vkPost": vkPost,
"schedulerStatus": schedulerStatus,
"schedulerSettings": schedulerSettings,
},
)
@app.before_request @app.route("/api/medods", methods=["POST"])
def init(): def api_medods():
db.create_all() try:
start_scheduler() data = request.json
logger.info("Приложение запущено") apiKey = data.get("apiKey", None)
url = data.get("url", None)
if url is not None:
logger.info("Получен url")
try:
medodsRecord = MedodsAPI.query.first()
medodsRecord.url = url
db.session.commit()
logger.info("Обновлен url")
except Exception:
db.session.merge(MedodsAPI(url=url))
db.session.commit()
logger.info("Добавлен url")
if apiKey:
logger.info("Получены ключи")
try:
medodsRecord = MedodsAPI.query.first()
medodsRecord.identity = apiKey["identity"]
medodsRecord.secretKey = apiKey["secretKey"]
db.session.commit()
logger.info("Обновлены ключи")
except Exception:
db.session.merge(
MedodsAPI(
identity=apiKey["identity"], secretKey=apiKey["secretKey"]
)
)
db.session.commit()
logger.info("Добавлены ключи")
return jsonify({"ok": True})
except Exception as e:
logger.error(f"Ошибка при обновлении ключей: {e}")
return jsonify({"ok": False}), 500
@app.route("/settings/medods", methods=["POST"]) @app.route("/api/requests", methods=["GET", "POST", "PATCH", "DELETE"])
def medods_url(): def api_requests():
data = request.json
logger.info(data)
apiKey = data.get("apiKey", None)
url = data.get("url", None)
if url is not None:
logger.info("Получен url")
try:
medodsRecord = MedodsAPI.query.first()
medodsRecord.url = url
db.session.commit()
logger.info("Обновлен url")
except Exception:
db.session.merge(MedodsAPI(url=url))
db.session.commit()
logger.info("Добавлен url")
if apiKey:
logger.info("Получены ключи")
try:
medodsRecord = MedodsAPI.query.first()
medodsRecord.identity = apiKey["identity"]
medodsRecord.secretKey = apiKey["secretKey"]
db.session.commit()
logger.info("Обновлены ключи")
except Exception:
db.session.merge(
MedodsAPI(identity=apiKey["identity"], secretKey=apiKey["secretKey"])
)
db.session.commit()
logger.info("Добавлены ключи")
return jsonify({"ok": True})
# return jsonify({"ok": False}), 400
@app.route("/settings/requests", methods=["GET", "POST", "PATCH", "DELETE"])
def get_requests():
requestData = ( requestData = (
request.json if request.method in ["POST", "PATCH", "DELETE"] else None request.json if request.method in ["POST", "PATCH", "DELETE"] else None
) )
match request.method: match request.method:
case "DELETE": case "DELETE":
try:
db.session.execute(
db.delete(ApiEndpoint).where(ApiEndpoint.id == requestData["id"])
)
db.session.commit()
except Exception as e:
logger.error(f"Ошибка при удалении запроса {requestData['id']}: {e}")
logger.info("Удален запрос") logger.info("Удален запрос")
logger.info(requestData)
return jsonify({"status": "ok"}) return jsonify({"status": "ok"})
case "POST": case "POST":
logger.info("Добавлен/обновлен запрос") logger.info("Добавлен/обновлен запрос")
@@ -145,28 +176,33 @@ def get_requests():
case "PATCH": case "PATCH":
logger.info("Выполнен запрос") logger.info("Выполнен запрос")
requestParams = ApiEndpoint.query.filter_by(id=requestData["id"]).first()
medodsDB = MedodsAPI.query.first()
baseUrl = medodsDB.url
response = send_request(
requestParams.method,
f"{baseUrl}{requestParams.url_path}",
requestParams.payload,
requestParams.query_params,
)
exitData = {}
try: try:
exitData = response.json() requestParams = ApiEndpoint.query.filter_by(
except: id=requestData["id"]
exitData = response.text ).first()
return jsonify(exitData) medodsDB = MedodsAPI.query.first()
baseUrl = medodsDB.url
response = send_request(
requestParams.method,
f"{baseUrl}{requestParams.url_path}",
requestParams.payload,
requestParams.query_params,
)
exitData = {}
try:
exitData = response.json()
except:
exitData = response.text
return jsonify(exitData)
except Exception as e:
logger.error(f"Ошибка при выполнении запроса: {e}")
return jsonify({"status": "error"}), 500
case "GET": case "GET":
logger.info("Получен список запросов") logger.info("Получен список запросов")
requestsDB = ApiEndpoint.query.all() requestsDB = ApiEndpoint.query.all()
requestsList = [r.toDict() for r in requestsDB] requestsList = [r.toDict() for r in requestsDB]
logger.info(requestsList)
return jsonify( return jsonify(
{ {
"status": "ok", "status": "ok",
@@ -179,10 +215,9 @@ def get_requests():
return jsonify({"status": "error"}), 405 return jsonify({"status": "error"}), 405
@app.route("/settings/vk", methods=["POST"]) @app.route("/api/vk", methods=["POST"])
def settings_vk(): def api_vk():
requestData = request.json requestData = request.json
logger.info(requestData)
if "id" in requestData: if "id" in requestData:
logger.info("Обновлен запрос") logger.info("Обновлен запрос")
try: try:
@@ -217,14 +252,77 @@ def settings_vk():
return jsonify({"status": "ok"}) return jsonify({"status": "ok"})
@app.route("/request", methods=["POST"]) @app.route("/api/posts", methods=["POST", "GET"])
def make_request(): def api_posts():
data = request.json match request.method:
response = send_request( case "POST":
data["method"], data["url"], data.get("payload"), data.get("headers") requestData = request.json
) logger.info("Настройки публикации и расписания")
return jsonify({"status": response.status_code}) vkPostData = requestData.get("vkPostData", None)
if vkPostData:
selectedUsers = vkPostData.get("selectedUsers", None)
static_text = vkPostData.get("static_text", None)
full_name = vkPostData.get("full_name", None)
logger.info("Обновление настроек публикации")
try:
vkPost = VkPost.query.first()
if vkPost:
if selectedUsers:
vkPost.selected_users = selectedUsers
if static_text:
vkPost.static_text = static_text
if full_name is not None:
vkPost.full_name = full_name
else:
db.session.merge(VkPost(**vkPostData))
db.session.commit()
except Exception as e:
logger.error(f"Ошибка при обновлении настроек публикации: {e}")
schedulerData = requestData.get("schedulerData", None)
if schedulerData:
logger.info("Обновление расписания публикации")
try:
scheduler = PostScheduler.query.first()
startTime = schedulerData.get("startTime", None)
endTime = schedulerData.get("endTime", None)
interval_minutes = schedulerData.get("interval_minutes", None)
enabled = schedulerData.get("enabled", None)
if startTime:
scheduler.start_hour = int(startTime)
if endTime:
scheduler.end_hour = int(endTime)
if interval_minutes:
scheduler.interval_minutes = int(interval_minutes)
if enabled is not None:
scheduler.enabled = enabled
db.session.commit()
except Exception as e:
logger.error(f"Ошибка при обновлении расписания публикации: {e}")
return jsonify({"status": "ok"})
case "GET":
queryParams = request.args.to_dict()
action = queryParams.get("action", None)
if action:
match action:
case "update_users":
logger.info("Обновить список пользователей")
result = updateMedodsUsers()
return jsonify({"ok": result})
case "handle_posts":
logger.info("Выполнить публикацию")
handle_vk_post()
return jsonify({"ok": True})
case _:
logger.error("Неверный метод запроса")
return jsonify({"status": "error"}), 405
return jsonify({"ok": False, "status": "error", "message": "no action"})
case _:
logger.error("Неверный метод запроса")
return jsonify({"status": "error"}), 405
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=5000) app.run(debug=True)
# app.run(debug=True, host="0.0.0.0", port=80)
-3
View File
@@ -9,6 +9,3 @@ class Config:
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
LOG_FILE = os.path.join(BASE_DIR, "logs/app.log") LOG_FILE = os.path.join(BASE_DIR, "logs/app.log")
VK_GROUP_TOKEN = "GROUP_ACCESS_TOKEN"
VK_GROUP_ID = 123456789
+75
View File
@@ -65,3 +65,78 @@ class VkAPI(db.Model):
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M:%S"), "created_at": self.created_at.strftime("%Y-%m-%d %H:%M:%S"),
"updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M:%S"), "updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M:%S"),
} }
class UsersMedods(db.Model):
__tablename__ = "users_medods"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Text)
short_name = db.Column(db.Text)
sex = db.Column(db.Text)
step = db.Column(db.Integer)
specialties = db.Column(db.JSON)
created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
def toDict(self):
return {
"id": self.id,
"name": self.name,
"shortName": self.short_name,
"sex": self.sex,
"step": self.step,
"specialties": self.specialties,
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M:%S"),
"updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M:%S"),
}
class VkPost(db.Model):
__tablename__ = "vk_post"
id = db.Column(db.Integer, primary_key=True)
dynamic_text = db.Column(db.Text, nullable=True)
static_text = db.Column(db.Text)
selected_users = db.Column(db.JSON)
full_name = db.Column(db.Boolean, default=True)
post_id = db.Column(db.Integer, nullable=True)
publish_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
def toDict(self):
return {
"id": self.id,
"dynamic_text": self.dynamic_text,
"static_text": self.static_text,
"selected_users": self.selected_users,
"full_name": self.full_name,
"post_id": self.post_id,
"publish_at": self.publish_at,
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M:%S"),
"updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M:%S"),
}
class PostScheduler(db.Model):
__tablename__ = "post_scheduler"
id = db.Column(db.Integer, primary_key=True)
start_hour = db.Column(db.Integer)
end_hour = db.Column(db.Integer)
interval_minutes = db.Column(db.Integer)
enabled = db.Column(db.Boolean)
created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
def toDict(self):
return {
"id": self.id,
"start_hour": self.start_hour,
"end_hour": self.end_hour,
"interval_minutes": self.interval_minutes,
"enabled": self.enabled,
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M:%S"),
"updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M:%S"),
}
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
# Получаем текущую дату и время
DATETIME=$(date "+%Y-%m-%d %H:%M:%S")
# Проверка статуса
git status
# Добавляем все изменения
git add .
# Коммит с автокомментарием
git commit -m "Обновление БД ${DATETIME}"
# Отправляем в удалённый репозиторий
git push
+1 -4
View File
@@ -10,7 +10,6 @@ def send_request(method, url, json_data=None, params=None):
bearer_token = generate_token() bearer_token = generate_token()
if bearer_token: if bearer_token:
headers["Authorization"] = f"Bearer {bearer_token}" headers["Authorization"] = f"Bearer {bearer_token}"
logger.info(headers)
try: try:
response = requests.request( response = requests.request(
@@ -20,8 +19,6 @@ def send_request(method, url, json_data=None, params=None):
logger.error(f"Ошибка при выполнении запроса: {e}") logger.error(f"Ошибка при выполнении запроса: {e}")
return return
logger.info(response.status_code) logger.debug(f"Статус код: {response.status_code}")
logger.info(response.headers)
# logger.info(response.text)
return response return response
+177
View File
@@ -0,0 +1,177 @@
import datetime
from db import ApiEndpoint, MedodsAPI, UsersMedods, VkPost, db
from http_client import send_request
def updateMedodsUsers() -> bool:
from app import logger
try:
requestParams = ApiEndpoint.query.filter_by(title="Список докторов").first()
if not requestParams:
return False
response = send_request(
requestParams.method,
requestParams.url_path,
params=requestParams.query_params,
)
if not response:
return False
usersFromDB = []
for user in response.json():
if user["availabilityForOnlineRecording"] != "available":
continue
userDict = {
"id": user["id"],
"name": f"{user['surname']} {user['name']} {user['secondName']}",
"short_name": f"{user['surname']} {user['name'][:1]}. {user['secondName'][:1]}.",
"sex": user["sex"],
"step": user["appointmentDuration"],
"specialties": [spec["title"] for spec in user["specialties"]],
}
usersFromDB.append(userDict)
actualUsersIds = [user["id"] for user in usersFromDB]
allExistingUsers = UsersMedods.query.all()
for user in allExistingUsers:
if user.id not in actualUsersIds:
logger.info(f"Удален доктор {user.name} {user.surname}")
db.session.delete(user)
db.session.commit()
for user in usersFromDB:
existingUser = UsersMedods.query.filter_by(id=user["id"]).first()
if existingUser:
changes = False
if existingUser.name != user["name"]:
existingUser.name = user["name"]
existingUser.short_name = user["short_name"]
changes = True
if existingUser.step != user["step"]:
existingUser.step = user["step"]
changes = True
if existingUser.specialties != user["specialties"]:
existingUser.specialties = user["specialties"]
changes = True
if changes:
logger.info(f"Обновлен доктор {user['name']} {user['surname']}")
db.session.commit()
else:
newUser = UsersMedods(
id=user["id"],
name=user["name"],
short_name=user["short_name"],
sex=user["sex"],
step=user["step"],
specialties=user["specialties"],
)
db.session.add(newUser)
db.session.commit()
logger.info(f"Добавлен доктор {user['name']} {user['surname']}")
return True
except Exception as e:
logger.error(f"Ошибка при обновлении списка докторов: {e}")
return False
def getFreeSlots(vkPost) -> dict:
from app import logger
try:
if not vkPost:
logger.error("Информация для размещения поста не найдена")
return {}
selectedUsersIds = vkPost.selected_users
selectedUsers = UsersMedods.query.filter(
UsersMedods.id.in_(selectedUsersIds)
).all()
requestParams = ApiEndpoint.query.filter_by(
title="Свободные записи на дату"
).first()
medodsApi = MedodsAPI.query.first()
if not requestParams or len(selectedUsers) == 0 or not medodsApi:
logger.error("Ошибка получения необходимых параметров")
return {}
userParams = [{str(user.id): {"step": user.step}} for user in selectedUsers]
tomorrow = datetime.date.today() + datetime.timedelta(days=1)
the_day_after_tomorrow = tomorrow + datetime.timedelta(days=1)
startDate = tomorrow.strftime("%Y-%m-%d")
endDate = the_day_after_tomorrow.strftime("%Y-%m-%d")
json = requestParams.payload
json["startDate"] = startDate
json["endDate"] = endDate
json["userParams"] = userParams
url = medodsApi.url + requestParams.url_path
response = send_request(
requestParams.method,
url,
json,
)
if not response:
return {}
slotsDataFull = response.json()
if len(slotsDataFull.keys()) == 0:
return {}
firstKey = list(slotsDataFull.keys())[0]
slotsData = {"date": firstKey, "slots": slotsDataFull.get(firstKey)}
return slotsData
except Exception as e:
logger.error(f"Ошибка при получении свободных приемов: {e}")
return {}
def setDynamicText():
from app import logger
try:
vkPost = VkPost.query.first()
if not vkPost:
logger.error("Информация для размещения поста не найдена")
return vkPost
freeSlots = getFreeSlots(vkPost)
if len(freeSlots.keys()) == 0:
logger.error("Нет свободных приемов")
return vkPost
userIds = [int(key) for key in freeSlots["slots"].keys()]
usersMedods = UsersMedods.query.filter(UsersMedods.id.in_(userIds)).all()
if len(usersMedods) == 0:
logger.error("Не найдены доктора с свободными приемами")
return vkPost
usersMedods.sort(key=lambda x: x.name)
users = [user.toDict() for user in usersMedods]
dateText = (
freeSlots["date"][8:]
+ "."
+ freeSlots["date"][5:7]
+ "."
+ freeSlots["date"][:4]
)
dynamicText = f"📌 Свободная запись на 📅 {dateText}:\n\n"
for user in users:
sex_icon = "👨‍⚕️" if user["sex"] == "male" else "👩‍⚕️"
slots = freeSlots["slots"][str(user["id"])]
name = user["name"] if vkPost.full_name else user["shortName"]
dynamicText += f"{sex_icon} {name} ({', '.join(user['specialties'])}):\n"
dynamicText += f"🕒 {', '.join(slots)}\n\n"
vkPost.dynamic_text = dynamicText
db.session.commit()
return vkPost
except Exception as e:
logger.error(f"Ошибка при обновлении списка докторов: {e}")
return
+61 -3
View File
@@ -1,7 +1,65 @@
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from db import PostScheduler
# ===== Scheduler =====
scheduler: BackgroundScheduler | None = None
JOB_ID = "vk_publish_job"
def start_scheduler(): def start_scheduler():
scheduler = BackgroundScheduler() global scheduler
# scheduler.add_job(publish_vk_posts, "interval", seconds=30) if scheduler is None:
scheduler.start() scheduler = BackgroundScheduler()
scheduler.start()
# ===== Добавление задачи =====
def enable_publish_job():
"""
Включает выполнение handle_vk_post
"""
from vk_handler import handle_vk_post
scheduleData = PostScheduler.query.first()
if not scheduleData or not scheduleData.enabled:
return
start_hour = scheduleData.start_hour
end_hour = scheduleData.end_hour
interval_minutes = scheduleData.interval_minutes
trigger = CronTrigger(
hour=f"{start_hour}-{end_hour - 1}", minute=f"*/{interval_minutes}"
)
scheduler.add_job(handle_vk_post, trigger=trigger, id=JOB_ID, replace_existing=True)
# ===== Отключение задачи =====
def disable_publish_job():
if scheduler and scheduler.get_job(JOB_ID):
scheduler.remove_job(JOB_ID)
def get_scheduler_status() -> dict:
scheduler_running = bool(scheduler and scheduler.running)
job = scheduler.get_job(JOB_ID) if scheduler_running else None
status = {
"scheduler": scheduler_running,
"vk_publish_job": job is not None,
"next_run_time": None,
}
if job:
status["next_run_time"] = (
job.next_run_time.strftime("%Y-%m-%d %H:%M:%S")
if job.next_run_time
else None
)
return status
+177
View File
@@ -0,0 +1,177 @@
/* Карточки */
.card {
border-radius: 10px;
border: 1px solid #e9ecef;
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
}
.card-header {
background-color: rgba(0, 0, 0, 0.02);
border-bottom: 1px solid #e9ecef;
padding: 1rem 1.25rem;
}
/* Таблица сотрудников */
.table-responsive {
max-height: 500px;
overflow-y: auto;
}
.table {
margin-bottom: 0;
}
.table thead th {
position: sticky;
top: 0;
background-color: #f8f9fa;
z-index: 10;
font-weight: 600;
text-transform: uppercase;
font-size: 0.85rem;
color: #6c757d;
border-bottom: 2px solid #dee2e6;
}
.table tbody tr {
transition: all 0.2s ease;
}
.table tbody tr:hover {
background-color: rgba(13, 110, 253, 0.05);
}
.table tbody tr.selected {
background-color: rgba(13, 110, 253, 0.1);
}
/* Чекбоксы */
.form-check-input {
width: 1.2em;
height: 1.2em;
cursor: pointer;
}
.form-check-input:checked {
background-color: #0d6efd;
border-color: #0d6efd;
}
/* Бейджи */
.badge {
font-weight: 500;
padding: 0.35em 0.65em;
}
.badge.bg-pink {
background-color: #e83e8c !important;
color: white;
}
.specialty-badges {
max-width: 200px;
}
/* Свитч */
.form-switch .form-check-input {
width: 3em;
height: 1.5em;
}
/* Поля ввода */
.form-control:focus,
.form-select:focus {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
/* Кнопки */
.btn {
border-radius: 6px;
font-weight: 500;
transition: all 0.2s ease;
}
.btn-lg {
padding: 0.75rem 1.5rem;
font-size: 1.1rem;
}
/* Уведомления */
.alert-fixed {
position: fixed;
top: 80px;
right: 20px;
z-index: 1050;
min-width: 300px;
max-width: 400px;
}
.alert {
border-radius: 8px;
border: none;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
/* Анимации */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}
/* Пользовательские стили */
.user-row {
cursor: pointer;
}
.user-row:hover td {
background-color: rgba(13, 110, 253, 0.05);
}
/* Статус планировщика */
#schedulerStatus .badge {
font-size: 0.85rem;
padding: 0.4em 0.8em;
}
/* Адаптивность */
@media (max-width: 768px) {
.table-responsive {
font-size: 0.9rem;
}
.specialty-badges .badge {
font-size: 0.7rem;
padding: 0.25em 0.5em;
margin-bottom: 0.25rem;
}
.btn {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
}
/* Пустая таблица */
.text-center.py-5 {
color: #6c757d;
}
.text-center.py-5 .display-1 {
font-size: 4rem;
margin-bottom: 1rem;
}
+6 -6
View File
@@ -54,7 +54,7 @@ async function saveServerUrl() {
} }
try { try {
const response = await fetch('/settings/medods', { const response = await fetch('/api/medods', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: serverUrl }) body: JSON.stringify({ url: serverUrl })
@@ -107,7 +107,7 @@ async function uploadApiKey() {
apiKey[headers[i]] = keyInfo[i]; apiKey[headers[i]] = keyInfo[i];
} }
const response = await fetch('/settings/medods', { const response = await fetch('/api/medods', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKey }) body: JSON.stringify({ apiKey })
@@ -132,7 +132,7 @@ async function uploadApiKey() {
// Загрузка списка запросов // Загрузка списка запросов
async function loadRequests() { async function loadRequests() {
try { try {
const response = await fetch('/settings/requests'); const response = await fetch('/api/requests');
const data = await response.json(); const data = await response.json();
requestsData = data.requests ? data.requests : []; requestsData = data.requests ? data.requests : [];
renderRequestsList(); renderRequestsList();
@@ -392,7 +392,7 @@ async function saveRequest() {
if (id) requestData.id = parseInt(id); if (id) requestData.id = parseInt(id);
try { try {
const response = await fetch('/settings/requests', { const response = await fetch('/api/requests', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData) body: JSON.stringify(requestData)
@@ -417,7 +417,7 @@ async function deleteRequest(id) {
if (!confirm('Вы уверены, что хотите удалить этот запрос?')) return; if (!confirm('Вы уверены, что хотите удалить этот запрос?')) return;
try { try {
const response = await fetch(`/settings/requests/${id}`, { const response = await fetch(`/api/requests/${id}`, {
method: 'DELETE' method: 'DELETE'
}); });
@@ -448,7 +448,7 @@ async function executeCurrentRequest() {
async function executeRequest(id) { async function executeRequest(id) {
try { try {
showLoader(true); showLoader(true);
const response = await fetch('/settings/requests', { const response = await fetch('/api/requests', {
method: 'PATCH', method: 'PATCH',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: parseInt(id) }) body: JSON.stringify({ id: parseInt(id) })
+362
View File
@@ -0,0 +1,362 @@
// Глобальные переменные
let originalSettings = null;
let hasChanges = false;
// Инициализация при загрузке страницы
document.addEventListener('DOMContentLoaded', function () {
// Сохраняем оригинальные настройки для сравнения
saveOriginalSettings();
// Отслеживаем изменения в форме
setupChangeListeners();
// Обновляем статус планировщика
updateSchedulerStatus();
});
// Сохранение оригинальных настроек
function saveOriginalSettings() {
originalSettings = {
selected_users: getSelectedUsers(),
static_text: document.getElementById('static_text').value,
full_name: document.getElementById('full_name').checked,
start_hour: parseInt(document.getElementById('start_hour').value),
end_hour: parseInt(document.getElementById('end_hour').value),
interval_minutes: parseInt(document.getElementById('interval_minutes').value),
enabled: document.getElementById('scheduler_enabled').checked
};
}
// Настройка отслеживания изменений
function setupChangeListeners() {
// Чекбоксы пользователей
document.querySelectorAll('.user-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', function () {
updateSelectAllCheckbox();
checkForChanges();
});
});
// Поля формы
const formFields = ['static_text', 'start_hour', 'end_hour', 'interval_minutes'];
formFields.forEach(field => {
const element = document.getElementById(field);
if (element) {
element.addEventListener('input', checkForChanges);
}
});
// Свитчи
document.getElementById('full_name').addEventListener('change', checkForChanges);
document.getElementById('scheduler_enabled').addEventListener('change', checkForChanges);
document.getElementById('selectAll').addEventListener('change', checkForChanges);
}
// Проверка на изменения
function checkForChanges() {
const currentSettings = {
selected_users: getSelectedUsers(),
static_text: document.getElementById('static_text').value,
full_name: document.getElementById('full_name').checked,
start_hour: parseInt(document.getElementById('start_hour').value),
end_hour: parseInt(document.getElementById('end_hour').value),
interval_minutes: parseInt(document.getElementById('interval_minutes').value),
enabled: document.getElementById('scheduler_enabled').checked
};
hasChanges = JSON.stringify(originalSettings) !== JSON.stringify(currentSettings);
// Можно добавить визуальное отображение изменений
const saveButton = document.querySelector('.btn-success');
if (hasChanges && saveButton) {
saveButton.innerHTML = '<i class="bi bi-save me-2"></i>Сохранить изменения';
saveButton.classList.add('btn-warning');
saveButton.classList.remove('btn-success');
} else if (saveButton) {
saveButton.innerHTML = '<i class="bi bi-save me-2"></i>Сохранить все настройки';
saveButton.classList.remove('btn-warning');
saveButton.classList.add('btn-success');
}
}
// Получение выбранных пользователей
function getSelectedUsers() {
const selectedUsers = [];
document.querySelectorAll('.user-checkbox:checked').forEach(checkbox => {
const userId = parseInt(checkbox.id.replace('user_', ''));
selectedUsers.push(userId);
});
return selectedUsers;
}
// Обновление чекбокса "Выбрать все"
function updateSelectAllCheckbox() {
const allCheckboxes = document.querySelectorAll('.user-checkbox');
const checkedCheckboxes = document.querySelectorAll('.user-checkbox:checked');
const selectAllCheckbox = document.getElementById('selectAll');
if (allCheckboxes.length === checkedCheckboxes.length) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else if (checkedCheckboxes.length === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
} else {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
}
}
// Выбрать всех пользователей
function selectAllUsers() {
document.querySelectorAll('.user-checkbox').forEach(checkbox => {
checkbox.checked = true;
});
updateSelectAllCheckbox();
checkForChanges();
}
// Снять выбор со всех пользователей
function deselectAllUsers() {
document.querySelectorAll('.user-checkbox').forEach(checkbox => {
checkbox.checked = false;
});
updateSelectAllCheckbox();
checkForChanges();
}
// Переключить выбор всех пользователей
function toggleAllUsers() {
const selectAllCheckbox = document.getElementById('selectAll');
const isChecked = selectAllCheckbox.checked;
document.querySelectorAll('.user-checkbox').forEach(checkbox => {
checkbox.checked = isChecked;
});
selectAllCheckbox.indeterminate = false;
checkForChanges();
}
// Обновление списка пользователей
async function updateUsersList() {
try {
const response = await fetch('/api/posts?action=update_users');
const data = await response.json();
if (data.ok) {
showAlert('success', 'Список пользователей обновлен!');
// Перезагружаем страницу через 1.5 секунды
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showAlert('danger', 'Ошибка обновления списка пользователей');
}
} catch (error) {
console.error('Ошибка:', error);
showAlert('danger', 'Ошибка обновления списка пользователей');
}
}
// Публикация сейчас
async function publishNow() {
const selectedUsers = getSelectedUsers();
const staticText = document.getElementById('static_text').value.trim();
if (selectedUsers.length === 0) {
showAlert('warning', 'Выберите хотя бы одного сотрудника для публикации');
return;
}
if (!staticText) {
showAlert('warning', 'Введите текст поста');
return;
}
try {
const response = await fetch('/api/posts?action=handle_posts');
const data = await response.json();
if (data.ok) {
showAlert('success', 'Публикация запущена! Проверьте ваше сообщество VK.');
} else {
showAlert('danger', 'Ошибка при запуске публикации');
}
} catch (error) {
console.error('Ошибка:', error);
showAlert('danger', 'Ошибка при запуске публикации');
}
}
// Сохранение настроек
async function saveSettings() {
const selectedUsers = getSelectedUsers();
const staticText = document.getElementById('static_text').value.trim();
const fullName = document.getElementById('full_name').checked;
const startHour = parseInt(document.getElementById('start_hour').value);
const endHour = parseInt(document.getElementById('end_hour').value);
const intervalMinutes = parseInt(document.getElementById('interval_minutes').value);
const schedulerEnabled = document.getElementById('scheduler_enabled').checked;
// Валидация
if (selectedUsers.length === 0 && hasChanges) {
showAlert('warning', 'Выберите хотя бы одного сотрудника для публикации');
return;
}
if (!staticText && hasChanges) {
showAlert('warning', 'Введите текст поста');
return;
}
if (startHour < 0 || startHour > 23) {
showAlert('warning', 'Время начала должно быть от 0 до 23 часов');
return;
}
if (endHour < 0 || endHour > 23) {
showAlert('warning', 'Время окончания должно быть от 0 до 23 часов');
return;
}
if (startHour >= endHour) {
showAlert('warning', 'Время начала должно быть раньше времени окончания');
return;
}
if (intervalMinutes < 1 || intervalMinutes > 1440) {
showAlert('warning', 'Интервал должен быть от 1 до 1440 минут');
return;
}
// Подготовка данных для отправки
const postData = {
vkPostData: {},
schedulerData: {}
};
// Только измененные данные для vkPostData
if (JSON.stringify(selectedUsers) !== JSON.stringify(originalSettings.selected_users) ||
staticText !== originalSettings.static_text ||
fullName !== originalSettings.full_name) {
postData.vkPostData = {
selectedUsers: selectedUsers,
static_text: staticText,
full_name: fullName
};
}
// Только измененные данные для schedulerData
if (startHour !== originalSettings.start_hour ||
endHour !== originalSettings.end_hour ||
intervalMinutes !== originalSettings.interval_minutes ||
schedulerEnabled !== originalSettings.enabled) {
postData.schedulerData = {
startTime: startHour.toString(),
endTime: endHour.toString(),
interval_minutes: intervalMinutes.toString(),
enabled: schedulerEnabled
};
}
// Если нет изменений
if (Object.keys(postData.vkPostData).length === 0 &&
Object.keys(postData.schedulerData).length === 0) {
showAlert('info', 'Нет изменений для сохранения');
return;
}
try {
const response = await fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(postData)
});
const data = await response.json();
if (response.ok && data.status === 'ok') {
showAlert('success', 'Настройки успешно сохранены!');
// Обновляем оригинальные настройки
saveOriginalSettings();
checkForChanges();
// Если менялись настройки планировщика, обновляем статус
if (Object.keys(postData.schedulerData).length > 0) {
setTimeout(updateSchedulerStatus, 1000);
}
} else {
const error = data.message || 'Ошибка сохранения';
showAlert('danger', error);
}
} catch (error) {
console.error('Ошибка:', error);
showAlert('danger', 'Ошибка сохранения настроек');
}
}
// Обновление статуса планировщика
async function updateSchedulerStatus() {
try {
// Здесь можно добавить запрос для получения актуального статуса
// Пока просто обновляем визуально
const enabled = document.getElementById('scheduler_enabled').checked;
const statusBadge = document.getElementById('schedulerStatus');
if (statusBadge) {
if (enabled) {
statusBadge.innerHTML = '<span class="badge bg-success"><i class="bi bi-play-circle me-1"></i>Активен</span>';
} else {
statusBadge.innerHTML = '<span class="badge bg-secondary"><i class="bi bi-stop-circle me-1"></i>Неактивен</span>';
}
}
} catch (error) {
console.error('Ошибка обновления статуса:', error);
}
}
// Вспомогательные функции для уведомлений
function showAlert(type, message) {
const alertContainer = document.getElementById('alertContainer');
// Очищаем старые алерты
alertContainer.innerHTML = '';
// Создаем новый алерт
const alert = document.createElement('div');
alert.className = `alert alert-${type} alert-dismissible fade show shadow`;
// Иконка для типа алерта
const icon = getAlertIcon(type);
alert.innerHTML = `
<div class="d-flex align-items-center">
<i class="bi ${icon} me-2 fs-5"></i>
<div class="flex-grow-1">${message}</div>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
alertContainer.appendChild(alert);
// Автоматическое удаление через 5 секунд
setTimeout(() => {
if (alert.parentNode) {
alert.classList.remove('show');
setTimeout(() => alert.remove(), 150);
}
}, 5000);
}
function getAlertIcon(type) {
const icons = {
'success': 'bi-check-circle-fill',
'warning': 'bi-exclamation-triangle-fill',
'danger': 'bi-x-circle-fill',
'info': 'bi-info-circle-fill'
};
return icons[type] || 'bi-info-circle-fill';
}
+1 -1
View File
@@ -72,7 +72,7 @@ async function saveVkSettings() {
} }
try { try {
const response = await fetch('/settings/vk', { const response = await fetch('/api/vk', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
+248 -25
View File
@@ -1,29 +1,252 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Посты{% endblock %} {% block title %}Посты{% endblock %}
{% block content %} {% block styles %}
<h3 class="mb-4">📝 Посты</h3> <link href="/static/css/posts.css" rel="stylesheet">
{% endblock %}
<div class="card mb-4">
<div class="card-header">Новый пост</div> {% block content %}
<div class="card-body"> <!-- Заголовок -->
<textarea class="form-control mb-3" rows="3" placeholder="Текст поста"></textarea> <div class="d-flex justify-content-between align-items-center mb-4">
<div>
<input type="file" class="form-control mb-3"> <h2 class="mb-1"><i class="bi bi-megaphone-fill text-primary me-2"></i>Управление постами</h2>
<p class="text-muted mb-0">Создание и планирование публикаций в VK</p>
<input type="datetime-local" class="form-control mb-3"> </div>
<div class="badge bg-primary fs-6 px-3 py-2">
<button class="btn btn-success">Добавить в очередь</button> <i class="bi bi-calendar-week me-1"></i>Публикации
</div> </div>
</div> </div>
<h4>📅 Запланированные</h4> <!-- Основной контент -->
<div class="row g-4">
<div class="card"> <!-- Левая колонка: Сотрудники -->
<div class="card-body"> <div class="col-lg-7">
<pre> <div class="card h-100">
{{ posts or "Постов нет" }} <div class="card-header d-flex justify-content-between align-items-center">
</pre> <h5 class="mb-0"><i class="bi bi-people-fill me-2"></i>Сотрудники Medods</h5>
</div> <div>
</div> <button type="button" class="btn btn-sm btn-outline-primary me-2" onclick="selectAllUsers()">
<i class="bi bi-check-all me-1"></i>Все
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="deselectAllUsers()">
<i class="bi bi-x-circle me-1"></i>Никто
</button>
</div>
</div>
<div class="card-body p-0">
<!-- Таблица сотрудников -->
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th width="50">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="selectAll"
onchange="toggleAllUsers()">
</div>
</th>
<th>ID</th>
<th>Имя</th>
<th>Короткое имя</th>
<th>Пол</th>
<th>Прием, мин.</th>
<th>Специальности</th>
</tr>
</thead>
<tbody id="usersTable">
{% for user in data.medodsUsers %}
<tr class="user-row" data-user-id="{{ user.id }}">
<td>
<div class="form-check">
<input class="form-check-input user-checkbox" type="checkbox"
id="user_{{ user.id }}" {% if data.vkPost and user.id in
data.vkPost.selected_users %}checked{% endif %}>
</div>
</td>
<td><span class="badge bg-secondary">{{ user.id }}</span></td>
<td>
<span class="fw-semibold">{{ user.name }}</span>
</td>
<td>{{ user.shortName }}</td>
<td>
{% if user.sex == 'male' %}
<span class="badge bg-info">Муж</span>
{% elif user.sex == 'female' %}
<span class="badge bg-pink">Жен</span>
{% else %}
<span class="badge bg-secondary">-</span>
{% endif %}
</td>
<td>
<span class="badge bg-light text-dark">{{ user.step }}</span>
</td>
<td>
{% if user.specialties %}
<div class="specialty-badges">
{% for specialty in user.specialties %}
<span class="badge bg-light text-dark me-1 mb-1">{{ specialty }}</span>
{% endfor %}
</div>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Если нет сотрудников -->
{% if not data.medodsUsers %}
<div class="text-center py-5">
<i class="bi bi-people display-1 text-muted mb-3"></i>
<h5 class="text-muted">Нет сотрудников</h5>
<p class="text-muted">Загрузите список сотрудников из Medods</p>
</div>
{% endif %}
</div>
<div class="card-footer">
<button type="button" class="btn btn-outline-primary w-100" onclick="updateUsersList()">
<i class="bi bi-arrow-clockwise me-2"></i>Обновить список сотрудников
</button>
</div>
</div>
</div>
<!-- Правая колонка: Настройки публикации -->
<div class="col-lg-5">
<!-- Настройки текста -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-text-paragraph me-2"></i>Настройки текста поста</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label fw-semibold">Статический текст поста</label>
<textarea class="form-control" id="static_text" rows="4"
placeholder="Введите основной текст поста...">{{ data.vkPost.static_text if data.vkPost else '' }}</textarea>
<div class="form-text">
Будет добавлен в начало поста перед именами сотрудников
</div>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">Отображение имен</label>
<div class="form-check form-switch d-flex justify-content-left gap-2 p-0">
<input class="form-check-input m-0" type="checkbox" role="switch" id="full_name" {% if
data.vkPost and data.vkPost.full_name %}checked{% endif %}>
<label class="form-check-label mb-0" for="full_name">
Использовать полные имена сотрудников
</label>
</div>
<div class="form-text">
Если выключено, будут использоваться короткие имена
</div>
</div>
<button type="button" class="btn btn-warning w-100" onclick="publishNow()">
<i class="bi bi-send-fill me-2"></i>Опубликовать сейчас
</button>
</div>
</div>
<!-- Настройки расписания -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-clock me-2"></i>Настройки расписания</h5>
<span id="schedulerStatus" class="badge">
{% if data.schedulerStatus.scheduler %}
<span class="badge bg-success"><i class="bi bi-play-circle me-1"></i>Активен</span>
{% else %}
<span class="badge bg-secondary"><i class="bi bi-stop-circle me-1"></i>Неактивен</span>
{% endif %}
</span>
</div>
<div class="card-body">
<!-- Время работы -->
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label fw-semibold">Начало публикации</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-sun"></i></span>
<input type="number" class="form-control" id="start_hour" min="0" max="23"
value="{{ data.schedulerSettings.start_hour if data.schedulerSettings else 9 }}">
<span class="input-group-text">:00</span>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Конец публикации</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-moon"></i></span>
<input type="number" class="form-control" id="end_hour" min="0" max="23"
value="{{ data.schedulerSettings.end_hour if data.schedulerSettings else 21 }}">
<span class="input-group-text">:00</span>
</div>
</div>
</div>
<!-- Интервал -->
<div class="mb-3">
<label class="form-label fw-semibold">Интервал публикации (минут)</label>
<input type="number" class="form-control" id="interval_minutes" min="1" max="1440"
value="{{ data.schedulerSettings.interval_minutes if data.schedulerSettings else 60 }}">
<div class="form-text">
Через сколько минут публиковать следующий пост
</div>
</div>
<!-- Включить/выключить -->
<div class="mb-4">
<label class="form-label fw-semibold">Статус планировщика</label>
<div class="form-check form-switch d-flex justify-content-left gap-2 p-0">
<input class="form-check-input m-0" type="checkbox" role="switch" id="scheduler_enabled" {% if
data.schedulerSettings and data.schedulerSettings.enabled %}checked{% endif %}>
<label class="form-check-label mb-0" for="scheduler_enabled">
Автоматическая публикация по расписанию
</label>
</div>
</div>
<!-- Информация о следующем запуске -->
{% if data.schedulerStatus.next_run_time %}
<div class="alert alert-info">
<div class="d-flex">
<div class="me-3">
<i class="bi bi-info-circle fs-5"></i>
</div>
<div>
<h6 class="alert-heading mb-1">Следующая публикация</h6>
<p class="mb-0">{{ data.schedulerStatus.next_run_time }}</p>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Кнопка сохранения -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-body text-center py-3">
<button type="button" class="btn btn-success btn-lg px-5" onclick="saveSettings()">
<i class="bi bi-save me-2"></i>Сохранить все настройки
</button>
<div class="form-text mt-2">
Сохраняются: выбранные сотрудники, текст поста, настройки расписания
</div>
</div>
</div>
</div>
</div>
<!-- Контейнер для уведомлений -->
<div id="alertContainer" class="alert-fixed"></div>
{% endblock %}
{% block scripts %}
<script src="/static/js/posts.js"></script>
{% endblock %} {% endblock %}
+1 -23
View File
@@ -88,28 +88,6 @@
</div> </div>
</div> </div>
<!-- Информационная панель -->
<div class="alert alert-info">
<div class="d-flex">
<div class="me-3">
<i class="bi bi-lightbulb fs-4"></i>
</div>
<div>
<h6 class="alert-heading">Как получить данные:</h6>
<ul class="mb-0 small">
<li><strong>Access Token:</strong> Создайте Standalone-приложение в <a
href="https://vk.com/apps?act=manage" target="_blank">управлении
приложениями VK</a></li>
<li><strong>ID сообщества:</strong> Число в адресе сообщества после
<code>vk.com/public</code> или <code>vk.com/club</code>
</li>
<li><strong>ID Базового фото:</strong> Загрузите фото в альбом сообщества и
скопируйте ID из адреса фото</li>
</ul>
</div>
</div>
</div>
<!-- Кнопки действий --> <!-- Кнопки действий -->
<div class="d-flex gap-3 pt-3 border-top"> <div class="d-flex gap-3 pt-3 border-top">
<button type="button" class="btn btn-outline-secondary" onclick="resetForm()"> <button type="button" class="btn btn-outline-secondary" onclick="resetForm()">
@@ -136,7 +114,7 @@
<div class="card-body"> <div class="card-body">
<h6 class="card-title"><i class="bi bi-key me-2"></i>Access Token</h6> <h6 class="card-title"><i class="bi bi-key me-2"></i>Access Token</h6>
<p class="card-text small text-truncate" id="tokenPreview"> <p class="card-text small text-truncate" id="tokenPreview">
{{ data.vk_settings.access_token[:15] }}... {{ data.vk_settings.access_token[:25] }}...
</p> </p>
</div> </div>
</div> </div>
+51
View File
@@ -0,0 +1,51 @@
from datetime import datetime
import vk_api
from db import VkAPI, db
from medods_handler import setDynamicText
def handle_vk_post():
from app import logger
logger.info("Обновление поста")
vkApi = VkAPI.query.first()
if not vkApi:
logger.error("Информация для работы не найдена")
return
vkPost = setDynamicText()
if not vkPost:
logger.error("Информация для размещения поста не найдена")
return
if not vkPost.dynamic_text and not vkPost.post_id:
logger.info("Не требуется публикация или удаление поста")
return
vk_session = vk_api.VkApi(token=vkApi.access_token)
vk = vk_session.get_api()
new_post = {}
if vkPost.dynamic_text:
logger.info("Публикация поста")
new_post = vk.wall.post(
owner_id=-vkApi.group_id,
from_group=1,
message=f"{vkPost.dynamic_text}/n{vkPost.static_text}".strip(),
attachments=f"photo-{vkApi.group_id}_{vkApi.base_photo_url}",
)
if vkPost.post_id:
logger.info("Удаление поста")
vk.wall.delete(owner_id=-vkApi.group_id, post_id=vkPost.post_id)
vkPost.post_id = None
vkPost.publish_at = None
if vkPost.dynamic_text:
vkPost.dynamic_text = None
vkPost.post_id = new_post.get("post_id")
vkPost.publish_at = datetime.now()
db.session.commit()
-30
View File
@@ -1,30 +0,0 @@
import vk_api
from config import Config
from db import db, VkPost
from datetime import datetime
def publish_vk_posts():
vk_session = vk_api.VkApi(token=Config.VK_GROUP_TOKEN)
vk = vk_session.get_api()
posts = VkPost.query.filter(
VkPost.published.is_(False), VkPost.publish_at <= datetime.utcnow()
).all()
for post in posts:
attachments = []
if post.image_path:
upload = vk_api.VkUpload(vk_session)
photo = upload.photo_wall(post.image_path, group_id=Config.VK_GROUP_ID)
attachments.append(f"photo{photo[0]['owner_id']}_{photo[0]['id']}")
vk.wall.post(
owner_id=-Config.VK_GROUP_ID,
message=post.text,
attachments=",".join(attachments),
)
post.published = True
db.session.commit()