работа с публикацией
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 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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
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) })
|
||||||
|
|||||||
@@ -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 {
|
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'
|
||||||
|
|||||||
+241
-18
@@ -1,29 +1,252 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Посты{% endblock %}
|
{% block title %}Посты{% endblock %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link href="/static/css/posts.css" rel="stylesheet">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3 class="mb-4">📝 Посты</h3>
|
<!-- Заголовок -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<div class="card mb-4">
|
<div>
|
||||||
<div class="card-header">Новый пост</div>
|
<h2 class="mb-1"><i class="bi bi-megaphone-fill text-primary me-2"></i>Управление постами</h2>
|
||||||
<div class="card-body">
|
<p class="text-muted mb-0">Создание и планирование публикаций в VK</p>
|
||||||
<textarea class="form-control mb-3" rows="3" placeholder="Текст поста"></textarea>
|
</div>
|
||||||
|
<div class="badge bg-primary fs-6 px-3 py-2">
|
||||||
<input type="file" class="form-control mb-3">
|
<i class="bi bi-calendar-week me-1"></i>Публикации
|
||||||
|
|
||||||
<input type="datetime-local" class="form-control mb-3">
|
|
||||||
|
|
||||||
<button class="btn btn-success">Добавить в очередь</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4>📅 Запланированные</h4>
|
<!-- Основной контент -->
|
||||||
|
<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>
|
||||||
|
|
||||||
<div class="card">
|
<!-- Если нет сотрудников -->
|
||||||
<div class="card-body">
|
{% if not data.medodsUsers %}
|
||||||
<pre>
|
<div class="text-center py-5">
|
||||||
{{ posts or "Постов нет" }}
|
<i class="bi bi-people display-1 text-muted mb-3"></i>
|
||||||
</pre>
|
<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>
|
</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
@@ -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>
|
||||||
|
|||||||
@@ -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