работа с публикацией
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
from flask import Flask, request, jsonify
|
||||
from flask import Flask, request, jsonify, render_template
|
||||
from config import Config
|
||||
from db import VkAPI, db, MedodsAPI, ApiEndpoint
|
||||
from scheduler import start_scheduler
|
||||
from db import PostScheduler, UsersMedods, VkAPI, VkPost, db, MedodsAPI, ApiEndpoint
|
||||
from medods_handler import updateMedodsUsers
|
||||
from scheduler import enable_publish_job, get_scheduler_status, start_scheduler
|
||||
from http_client import send_request
|
||||
import logging
|
||||
import os
|
||||
|
||||
from vk_handler import handle_vk_post
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
|
||||
@@ -24,7 +27,13 @@ logging.basicConfig(
|
||||
|
||||
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("/")
|
||||
@@ -47,7 +56,7 @@ def medods():
|
||||
return render_template("medods.html", data=data)
|
||||
|
||||
|
||||
@app.route("/vk")
|
||||
@app.route("/vk", methods=["GET"])
|
||||
def vk():
|
||||
vkDB = VkAPI.query.first()
|
||||
data = {}
|
||||
@@ -56,22 +65,33 @@ def vk():
|
||||
return render_template("vk.html", data=data)
|
||||
|
||||
|
||||
@app.route("/posts")
|
||||
@app.route("/posts", methods=["GET"])
|
||||
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
|
||||
def init():
|
||||
db.create_all()
|
||||
start_scheduler()
|
||||
logger.info("Приложение запущено")
|
||||
|
||||
|
||||
@app.route("/settings/medods", methods=["POST"])
|
||||
def medods_url():
|
||||
@app.route("/api/medods", methods=["POST"])
|
||||
def api_medods():
|
||||
try:
|
||||
data = request.json
|
||||
logger.info(data)
|
||||
apiKey = data.get("apiKey", None)
|
||||
url = data.get("url", None)
|
||||
if url is not None:
|
||||
@@ -96,23 +116,34 @@ def medods_url():
|
||||
logger.info("Обновлены ключи")
|
||||
except Exception:
|
||||
db.session.merge(
|
||||
MedodsAPI(identity=apiKey["identity"], secretKey=apiKey["secretKey"])
|
||||
MedodsAPI(
|
||||
identity=apiKey["identity"], secretKey=apiKey["secretKey"]
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
logger.info("Добавлены ключи")
|
||||
return jsonify({"ok": True})
|
||||
# return jsonify({"ok": False}), 400
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обновлении ключей: {e}")
|
||||
return jsonify({"ok": False}), 500
|
||||
|
||||
|
||||
@app.route("/settings/requests", methods=["GET", "POST", "PATCH", "DELETE"])
|
||||
def get_requests():
|
||||
@app.route("/api/requests", methods=["GET", "POST", "PATCH", "DELETE"])
|
||||
def api_requests():
|
||||
requestData = (
|
||||
request.json if request.method in ["POST", "PATCH", "DELETE"] else None
|
||||
)
|
||||
match request.method:
|
||||
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(requestData)
|
||||
|
||||
return jsonify({"status": "ok"})
|
||||
case "POST":
|
||||
logger.info("Добавлен/обновлен запрос")
|
||||
@@ -145,7 +176,10 @@ def get_requests():
|
||||
|
||||
case "PATCH":
|
||||
logger.info("Выполнен запрос")
|
||||
requestParams = ApiEndpoint.query.filter_by(id=requestData["id"]).first()
|
||||
try:
|
||||
requestParams = ApiEndpoint.query.filter_by(
|
||||
id=requestData["id"]
|
||||
).first()
|
||||
medodsDB = MedodsAPI.query.first()
|
||||
baseUrl = medodsDB.url
|
||||
response = send_request(
|
||||
@@ -160,13 +194,15 @@ def get_requests():
|
||||
except:
|
||||
exitData = response.text
|
||||
return jsonify(exitData)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при выполнении запроса: {e}")
|
||||
return jsonify({"status": "error"}), 500
|
||||
|
||||
case "GET":
|
||||
logger.info("Получен список запросов")
|
||||
|
||||
requestsDB = ApiEndpoint.query.all()
|
||||
requestsList = [r.toDict() for r in requestsDB]
|
||||
logger.info(requestsList)
|
||||
return jsonify(
|
||||
{
|
||||
"status": "ok",
|
||||
@@ -179,10 +215,9 @@ def get_requests():
|
||||
return jsonify({"status": "error"}), 405
|
||||
|
||||
|
||||
@app.route("/settings/vk", methods=["POST"])
|
||||
def settings_vk():
|
||||
@app.route("/api/vk", methods=["POST"])
|
||||
def api_vk():
|
||||
requestData = request.json
|
||||
logger.info(requestData)
|
||||
if "id" in requestData:
|
||||
logger.info("Обновлен запрос")
|
||||
try:
|
||||
@@ -217,14 +252,77 @@ def settings_vk():
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
@app.route("/request", methods=["POST"])
|
||||
def make_request():
|
||||
data = request.json
|
||||
response = send_request(
|
||||
data["method"], data["url"], data.get("payload"), data.get("headers")
|
||||
)
|
||||
return jsonify({"status": response.status_code})
|
||||
@app.route("/api/posts", methods=["POST", "GET"])
|
||||
def api_posts():
|
||||
match request.method:
|
||||
case "POST":
|
||||
requestData = request.json
|
||||
logger.info("Настройки публикации и расписания")
|
||||
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__":
|
||||
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)
|
||||
|
||||
@@ -9,6 +9,3 @@ class Config:
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
LOG_FILE = os.path.join(BASE_DIR, "logs/app.log")
|
||||
|
||||
VK_GROUP_TOKEN = "GROUP_ACCESS_TOKEN"
|
||||
VK_GROUP_ID = 123456789
|
||||
|
||||
@@ -65,3 +65,78 @@ class VkAPI(db.Model):
|
||||
"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 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"),
|
||||
}
|
||||
|
||||
@@ -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
@@ -10,7 +10,6 @@ def send_request(method, url, json_data=None, params=None):
|
||||
bearer_token = generate_token()
|
||||
if bearer_token:
|
||||
headers["Authorization"] = f"Bearer {bearer_token}"
|
||||
logger.info(headers)
|
||||
|
||||
try:
|
||||
response = requests.request(
|
||||
@@ -20,8 +19,6 @@ def send_request(method, url, json_data=None, params=None):
|
||||
logger.error(f"Ошибка при выполнении запроса: {e}")
|
||||
return
|
||||
|
||||
logger.info(response.status_code)
|
||||
logger.info(response.headers)
|
||||
# logger.info(response.text)
|
||||
logger.debug(f"Статус код: {response.status_code}")
|
||||
|
||||
return response
|
||||
|
||||
@@ -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
|
||||
+59
-1
@@ -1,7 +1,65 @@
|
||||
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():
|
||||
global scheduler
|
||||
if scheduler is None:
|
||||
scheduler = BackgroundScheduler()
|
||||
# scheduler.add_job(publish_vk_posts, "interval", seconds=30)
|
||||
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
|
||||
|
||||
@@ -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
@@ -54,7 +54,7 @@ async function saveServerUrl() {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/settings/medods', {
|
||||
const response = await fetch('/api/medods', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: serverUrl })
|
||||
@@ -107,7 +107,7 @@ async function uploadApiKey() {
|
||||
apiKey[headers[i]] = keyInfo[i];
|
||||
}
|
||||
|
||||
const response = await fetch('/settings/medods', {
|
||||
const response = await fetch('/api/medods', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ apiKey })
|
||||
@@ -132,7 +132,7 @@ async function uploadApiKey() {
|
||||
// Загрузка списка запросов
|
||||
async function loadRequests() {
|
||||
try {
|
||||
const response = await fetch('/settings/requests');
|
||||
const response = await fetch('/api/requests');
|
||||
const data = await response.json();
|
||||
requestsData = data.requests ? data.requests : [];
|
||||
renderRequestsList();
|
||||
@@ -392,7 +392,7 @@ async function saveRequest() {
|
||||
if (id) requestData.id = parseInt(id);
|
||||
|
||||
try {
|
||||
const response = await fetch('/settings/requests', {
|
||||
const response = await fetch('/api/requests', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestData)
|
||||
@@ -417,7 +417,7 @@ async function deleteRequest(id) {
|
||||
if (!confirm('Вы уверены, что хотите удалить этот запрос?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/settings/requests/${id}`, {
|
||||
const response = await fetch(`/api/requests/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
@@ -448,7 +448,7 @@ async function executeCurrentRequest() {
|
||||
async function executeRequest(id) {
|
||||
try {
|
||||
showLoader(true);
|
||||
const response = await fetch('/settings/requests', {
|
||||
const response = await fetch('/api/requests', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: parseInt(id) })
|
||||
|
||||
@@ -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
@@ -72,7 +72,7 @@ async function saveVkSettings() {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/settings/vk', {
|
||||
const response = await fetch('/api/vk', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
+248
-25
@@ -1,29 +1,252 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Посты{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3 class="mb-4">📝 Посты</h3>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">Новый пост</div>
|
||||
<div class="card-body">
|
||||
<textarea class="form-control mb-3" rows="3" placeholder="Текст поста"></textarea>
|
||||
|
||||
<input type="file" class="form-control mb-3">
|
||||
|
||||
<input type="datetime-local" class="form-control mb-3">
|
||||
|
||||
<button class="btn btn-success">Добавить в очередь</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>📅 Запланированные</h4>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<pre>
|
||||
{{ posts or "Постов нет" }}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% block styles %}
|
||||
<link href="/static/css/posts.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Заголовок -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-1"><i class="bi bi-megaphone-fill text-primary me-2"></i>Управление постами</h2>
|
||||
<p class="text-muted mb-0">Создание и планирование публикаций в VK</p>
|
||||
</div>
|
||||
<div class="badge bg-primary fs-6 px-3 py-2">
|
||||
<i class="bi bi-calendar-week me-1"></i>Публикации
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Основной контент -->
|
||||
<div class="row g-4">
|
||||
<!-- Левая колонка: Сотрудники -->
|
||||
<div class="col-lg-7">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-people-fill me-2"></i>Сотрудники Medods</h5>
|
||||
<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 %}
|
||||
+1
-23
@@ -88,28 +88,6 @@
|
||||
</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">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="resetForm()">
|
||||
@@ -136,7 +114,7 @@
|
||||
<div class="card-body">
|
||||
<h6 class="card-title"><i class="bi bi-key me-2"></i>Access Token</h6>
|
||||
<p class="card-text small text-truncate" id="tokenPreview">
|
||||
{{ data.vk_settings.access_token[:15] }}...
|
||||
{{ data.vk_settings.access_token[:25] }}...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user