Introduced support for scheduling actions
This commit is contained in:
parent
cddee2604f
commit
4d899d367f
9 changed files with 1238 additions and 59 deletions
758
app.py
758
app.py
|
|
@ -1,5 +1,5 @@
|
|||
###############################################################################
|
||||
# app.py - PyraFiles Application with Proper Agent and User Separation
|
||||
# app.py - Enhanced PyraFiles Application with Scheduled Actions and More
|
||||
###############################################################################
|
||||
import os
|
||||
import base64
|
||||
|
|
@ -15,6 +15,10 @@ import mimetypes
|
|||
import json
|
||||
import logging
|
||||
import unicodedata
|
||||
import threading
|
||||
import time
|
||||
import signal
|
||||
import sys
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from pyramid.config import Configurator
|
||||
|
|
@ -33,6 +37,7 @@ from sqlalchemy import (
|
|||
ForeignKey,
|
||||
Text,
|
||||
or_,
|
||||
inspect,
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
declarative_base,
|
||||
|
|
@ -181,6 +186,19 @@ def get_mime_type(filename):
|
|||
return mime_type
|
||||
|
||||
|
||||
def is_ascii_file(mime_type):
|
||||
"""Check if the file is ASCII-based (text files)."""
|
||||
ascii_types = [
|
||||
"text/",
|
||||
"application/json",
|
||||
"application/xml",
|
||||
"application/javascript",
|
||||
"application/x-yaml",
|
||||
"application/yaml",
|
||||
]
|
||||
return any(mime_type.startswith(t) for t in ascii_types)
|
||||
|
||||
|
||||
def uuid_to_short_id(u):
|
||||
"""Encode UUID to a URL-safe base64 string without padding."""
|
||||
return base64.urlsafe_b64encode(u.bytes).decode("ascii").rstrip("=")
|
||||
|
|
@ -351,6 +369,15 @@ class Agent(Base):
|
|||
)
|
||||
|
||||
|
||||
# New table for scheduler lock
|
||||
class SchedulerLock(Base):
|
||||
__tablename__ = "scheduler_lock"
|
||||
id = Column(Integer, primary_key=True)
|
||||
locked = Column(Boolean, default=False)
|
||||
locked_at = Column(DateTime, nullable=True)
|
||||
process_id = Column(String, nullable=True)
|
||||
|
||||
|
||||
################################################################################
|
||||
# Namespace Database Models
|
||||
################################################################################
|
||||
|
|
@ -365,15 +392,33 @@ class Media(NamespaceBase):
|
|||
filename = Column(String, nullable=False)
|
||||
title = Column(String, nullable=True) # Optional title
|
||||
media_type = Column(String, nullable=False) # 'image', 'audio', 'video'
|
||||
mime_type = Column(String, nullable=True) # MIME type for compatibility
|
||||
media_b64 = Column(Text, nullable=False)
|
||||
upload_date = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
is_public = Column(Boolean, default=True)
|
||||
visibility = Column(String, default="public") # 'public', 'private', 'unlisted'
|
||||
size = Column(Integer, nullable=False) # Size in bytes
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Media(id='{self.id}', filename='{self.filename}')>"
|
||||
|
||||
|
||||
class ScheduledAction(NamespaceBase):
|
||||
__tablename__ = "scheduled_actions"
|
||||
id = Column(String, primary_key=True) # UUID
|
||||
media_id = Column(String, ForeignKey("media.id"), nullable=False)
|
||||
action_type = Column(
|
||||
String, nullable=False
|
||||
) # 'set_public', 'set_private', 'set_unlisted', 'delete'
|
||||
scheduled_date = Column(DateTime, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
status = Column(String, default="pending") # 'pending', 'completed', 'failed'
|
||||
|
||||
media = relationship("Media")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ScheduledAction(media_id='{self.media_id}', action='{self.action_type}', date='{self.scheduled_date}')>"
|
||||
|
||||
|
||||
################################################################################
|
||||
# Jinja2 Environment and Custom Filters
|
||||
################################################################################
|
||||
|
|
@ -572,6 +617,208 @@ def reader_required(view_func):
|
|||
return wrapper
|
||||
|
||||
|
||||
################################################################################
|
||||
# Scheduler Functions
|
||||
################################################################################
|
||||
|
||||
|
||||
def acquire_scheduler_lock(dbsession):
|
||||
"""Acquire the scheduler lock."""
|
||||
process_id = f"{os.getpid()}_{int(time.time())}"
|
||||
|
||||
# Try to get existing lock
|
||||
lock = dbsession.query(SchedulerLock).first()
|
||||
|
||||
if not lock:
|
||||
# Create new lock
|
||||
lock = SchedulerLock(
|
||||
locked=True, locked_at=datetime.datetime.utcnow(), process_id=process_id
|
||||
)
|
||||
dbsession.add(lock)
|
||||
dbsession.commit()
|
||||
return True
|
||||
|
||||
# Check if lock is stale (older than 5 minutes)
|
||||
if lock.locked and lock.locked_at:
|
||||
if datetime.datetime.utcnow() - lock.locked_at > datetime.timedelta(minutes=5):
|
||||
log.warning("Detected stale scheduler lock, releasing it")
|
||||
lock.locked = False
|
||||
lock.locked_at = None
|
||||
lock.process_id = None
|
||||
dbsession.commit()
|
||||
|
||||
if not lock.locked:
|
||||
lock.locked = True
|
||||
lock.locked_at = datetime.datetime.utcnow()
|
||||
lock.process_id = process_id
|
||||
dbsession.commit()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def release_scheduler_lock(dbsession):
|
||||
"""Release the scheduler lock."""
|
||||
lock = dbsession.query(SchedulerLock).first()
|
||||
if lock and lock.locked:
|
||||
lock.locked = False
|
||||
lock.locked_at = None
|
||||
lock.process_id = None
|
||||
dbsession.commit()
|
||||
|
||||
|
||||
def process_scheduled_actions():
|
||||
"""Process all scheduled actions across all namespaces."""
|
||||
main_engine = create_engine(
|
||||
DB_URL,
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
MainSessionFactory = sessionmaker(bind=main_engine)
|
||||
main_dbsession = MainSessionFactory()
|
||||
|
||||
try:
|
||||
if not acquire_scheduler_lock(main_dbsession):
|
||||
return # Another process is already running
|
||||
|
||||
log.info("Scheduler acquired lock, processing scheduled actions...")
|
||||
|
||||
# Get all namespaces
|
||||
namespaces = main_dbsession.query(Namespace).all()
|
||||
|
||||
for namespace in namespaces:
|
||||
process_namespace_scheduled_actions(namespace.id)
|
||||
|
||||
log.info("Finished processing scheduled actions")
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error in scheduler: {e}")
|
||||
finally:
|
||||
release_scheduler_lock(main_dbsession)
|
||||
main_dbsession.close()
|
||||
main_engine.dispose()
|
||||
|
||||
|
||||
def process_namespace_scheduled_actions(namespace_id):
|
||||
"""Process scheduled actions for a specific namespace."""
|
||||
namespace_db_url = get_namespace_db_url(namespace_id)
|
||||
|
||||
try:
|
||||
namespace_engine = create_engine(
|
||||
namespace_db_url,
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
NamespaceSessionFactory = sessionmaker(bind=namespace_engine)
|
||||
namespace_dbsession = NamespaceSessionFactory()
|
||||
|
||||
# Check if scheduled_actions table exists
|
||||
inspector = inspect(namespace_engine)
|
||||
if "scheduled_actions" not in inspector.get_table_names():
|
||||
log.debug(
|
||||
f"No scheduled_actions table in namespace {namespace_id}, skipping"
|
||||
)
|
||||
return
|
||||
|
||||
# Get pending actions that are due
|
||||
now = datetime.datetime.utcnow()
|
||||
pending_actions = (
|
||||
namespace_dbsession.query(ScheduledAction)
|
||||
.filter(
|
||||
ScheduledAction.status == "pending",
|
||||
ScheduledAction.scheduled_date <= now,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
for action in pending_actions:
|
||||
try:
|
||||
media = (
|
||||
namespace_dbsession.query(Media)
|
||||
.filter_by(id=action.media_id)
|
||||
.first()
|
||||
)
|
||||
if not media:
|
||||
action.status = "failed"
|
||||
continue
|
||||
|
||||
if action.action_type == "delete":
|
||||
namespace_dbsession.delete(media)
|
||||
log.info(f"Scheduled delete executed for media {media.filename}")
|
||||
elif action.action_type == "set_public":
|
||||
media.visibility = "public"
|
||||
log.info(
|
||||
f"Scheduled set_public executed for media {media.filename}"
|
||||
)
|
||||
elif action.action_type == "set_private":
|
||||
media.visibility = "private"
|
||||
log.info(
|
||||
f"Scheduled set_private executed for media {media.filename}"
|
||||
)
|
||||
elif action.action_type == "set_unlisted":
|
||||
media.visibility = "unlisted"
|
||||
log.info(
|
||||
f"Scheduled set_unlisted executed for media {media.filename}"
|
||||
)
|
||||
|
||||
action.status = "completed"
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error processing scheduled action {action.id}: {e}")
|
||||
action.status = "failed"
|
||||
|
||||
namespace_dbsession.commit()
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error processing namespace {namespace_id}: {e}")
|
||||
finally:
|
||||
if "namespace_dbsession" in locals():
|
||||
namespace_dbsession.close()
|
||||
if "namespace_engine" in locals():
|
||||
namespace_engine.dispose()
|
||||
|
||||
|
||||
# Global variables for scheduler thread
|
||||
scheduler_thread = None
|
||||
scheduler_running = False
|
||||
|
||||
|
||||
def scheduler_worker():
|
||||
"""Background scheduler worker that runs every minute."""
|
||||
global scheduler_running
|
||||
while scheduler_running:
|
||||
try:
|
||||
process_scheduled_actions()
|
||||
except Exception as e:
|
||||
log.error(f"Scheduler worker error: {e}")
|
||||
|
||||
# Sleep for 60 seconds
|
||||
for i in range(60):
|
||||
if not scheduler_running:
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def start_scheduler():
|
||||
"""Start the background scheduler."""
|
||||
global scheduler_thread, scheduler_running
|
||||
if not scheduler_running:
|
||||
scheduler_running = True
|
||||
scheduler_thread = threading.Thread(target=scheduler_worker, daemon=True)
|
||||
scheduler_thread.start()
|
||||
log.info("Scheduler started")
|
||||
|
||||
|
||||
def stop_scheduler():
|
||||
"""Stop the background scheduler."""
|
||||
global scheduler_thread, scheduler_running
|
||||
if scheduler_running:
|
||||
scheduler_running = False
|
||||
if scheduler_thread:
|
||||
scheduler_thread.join(timeout=5)
|
||||
log.info("Scheduler stopped")
|
||||
|
||||
|
||||
################################################################################
|
||||
# Routes and Views
|
||||
################################################################################
|
||||
|
|
@ -1202,19 +1449,93 @@ def upload_media_post_view(request):
|
|||
media_id = str(media_uuid)
|
||||
media_short_id = uuid_to_short_id(media_uuid)
|
||||
|
||||
is_public = request.POST.get("is_public") == "on"
|
||||
visibility = request.POST.get("visibility", "public")
|
||||
if visibility not in ["public", "private", "unlisted"]:
|
||||
visibility = "public"
|
||||
|
||||
media = Media(
|
||||
id=media_id,
|
||||
short_id=media_short_id,
|
||||
filename=filename,
|
||||
title=title, # now has a fallback of filename
|
||||
title=title,
|
||||
media_type=media_type,
|
||||
mime_type=mime_type,
|
||||
media_b64=encoded_str,
|
||||
is_public=is_public,
|
||||
visibility=visibility,
|
||||
size=file_size,
|
||||
)
|
||||
namespace_dbsession.add(media)
|
||||
|
||||
# Handle scheduled actions
|
||||
scheduled_public = request.POST.get("scheduled_public")
|
||||
scheduled_private = request.POST.get("scheduled_private")
|
||||
scheduled_unlisted = request.POST.get("scheduled_unlisted")
|
||||
scheduled_delete = request.POST.get("scheduled_delete")
|
||||
|
||||
# Check if scheduled_actions table exists before adding scheduled actions
|
||||
try:
|
||||
inspector = inspect(namespace_dbsession.bind)
|
||||
has_scheduled_actions = "scheduled_actions" in inspector.get_table_names()
|
||||
except:
|
||||
has_scheduled_actions = False
|
||||
|
||||
if has_scheduled_actions:
|
||||
if scheduled_public:
|
||||
try:
|
||||
scheduled_date = datetime.datetime.fromisoformat(scheduled_public)
|
||||
action_uuid = uuid.uuid4()
|
||||
action = ScheduledAction(
|
||||
id=str(action_uuid),
|
||||
media_id=media_id,
|
||||
action_type="set_public",
|
||||
scheduled_date=scheduled_date,
|
||||
)
|
||||
namespace_dbsession.add(action)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if scheduled_private:
|
||||
try:
|
||||
scheduled_date = datetime.datetime.fromisoformat(scheduled_private)
|
||||
action_uuid = uuid.uuid4()
|
||||
action = ScheduledAction(
|
||||
id=str(action_uuid),
|
||||
media_id=media_id,
|
||||
action_type="set_private",
|
||||
scheduled_date=scheduled_date,
|
||||
)
|
||||
namespace_dbsession.add(action)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if scheduled_unlisted:
|
||||
try:
|
||||
scheduled_date = datetime.datetime.fromisoformat(scheduled_unlisted)
|
||||
action_uuid = uuid.uuid4()
|
||||
action = ScheduledAction(
|
||||
id=str(action_uuid),
|
||||
media_id=media_id,
|
||||
action_type="set_unlisted",
|
||||
scheduled_date=scheduled_date,
|
||||
)
|
||||
namespace_dbsession.add(action)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if scheduled_delete:
|
||||
try:
|
||||
scheduled_date = datetime.datetime.fromisoformat(scheduled_delete)
|
||||
action_uuid = uuid.uuid4()
|
||||
action = ScheduledAction(
|
||||
id=str(action_uuid),
|
||||
media_id=media_id,
|
||||
action_type="delete",
|
||||
scheduled_date=scheduled_date,
|
||||
)
|
||||
namespace_dbsession.add(action)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
namespace_dbsession.flush()
|
||||
|
||||
return HTTPFound(
|
||||
|
|
@ -1236,24 +1557,37 @@ def list_media_view(request):
|
|||
return HTTPForbidden("You do not have access to this namespace.")
|
||||
|
||||
# Get the media items, excluding the media_b64 column
|
||||
media_items = (
|
||||
namespace_dbsession.query(Media)
|
||||
.options(
|
||||
load_only(
|
||||
Media.id,
|
||||
Media.short_id,
|
||||
Media.filename,
|
||||
Media.title,
|
||||
Media.media_type,
|
||||
Media.upload_date,
|
||||
Media.is_public,
|
||||
Media.size,
|
||||
)
|
||||
# Only show public media unless user has editor+ access
|
||||
query = namespace_dbsession.query(Media).options(
|
||||
load_only(
|
||||
Media.id,
|
||||
Media.short_id,
|
||||
Media.filename,
|
||||
Media.title,
|
||||
Media.media_type,
|
||||
Media.upload_date,
|
||||
Media.visibility,
|
||||
Media.size,
|
||||
)
|
||||
.order_by(Media.upload_date.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
# Filter based on user permissions
|
||||
user_role = get_user_or_agent_namespace_role(request)
|
||||
if user_role in ["owner", "editor"]:
|
||||
# Owners and editors can see all media except unlisted
|
||||
media_items = (
|
||||
query.filter(Media.visibility != "unlisted")
|
||||
.order_by(Media.upload_date.desc())
|
||||
.all()
|
||||
)
|
||||
else:
|
||||
# Readers and public users can only see public media
|
||||
media_items = (
|
||||
query.filter(Media.visibility == "public")
|
||||
.order_by(Media.upload_date.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
return {
|
||||
"request": request,
|
||||
"namespace": namespace,
|
||||
|
|
@ -1274,15 +1608,58 @@ def view_media_details_view(request):
|
|||
role = get_user_or_agent_namespace_role(request)
|
||||
is_owner_or_editor = role in ["owner", "editor"]
|
||||
|
||||
# Check if media is public or user has access
|
||||
if not media.is_public and not check_namespace_permission(request, "reader"):
|
||||
# Check visibility permissions
|
||||
if media.visibility == "private" and not check_namespace_permission(
|
||||
request, "reader"
|
||||
):
|
||||
return Response("Media not available.", status=403)
|
||||
elif media.visibility == "unlisted" and not is_owner_or_editor:
|
||||
return Response("Media not available.", status=403)
|
||||
elif media.visibility == "public":
|
||||
# Public media is always accessible
|
||||
pass
|
||||
|
||||
# Check if this is an ASCII file and get preview content
|
||||
ascii_preview = None
|
||||
mime_type = get_mime_type(media.filename)
|
||||
if is_ascii_file(mime_type):
|
||||
try:
|
||||
raw_content = base64.b64decode(media.media_b64).decode("utf-8")
|
||||
lines = raw_content.split("\n")
|
||||
if len(lines) > 100:
|
||||
ascii_preview = "\n".join(lines[:100]) + "\n... (truncated)"
|
||||
else:
|
||||
ascii_preview = raw_content
|
||||
except (UnicodeDecodeError, Exception):
|
||||
ascii_preview = None
|
||||
|
||||
# Get scheduled actions for this media
|
||||
scheduled_actions = []
|
||||
if is_owner_or_editor:
|
||||
try:
|
||||
# Check if scheduled_actions table exists
|
||||
inspector = inspect(namespace_dbsession.bind)
|
||||
if "scheduled_actions" in inspector.get_table_names():
|
||||
scheduled_actions = (
|
||||
namespace_dbsession.query(ScheduledAction)
|
||||
.filter(
|
||||
ScheduledAction.media_id == media.id,
|
||||
ScheduledAction.status == "pending",
|
||||
)
|
||||
.order_by(ScheduledAction.scheduled_date)
|
||||
.all()
|
||||
)
|
||||
except Exception as e:
|
||||
log.debug(f"Could not query scheduled actions: {e}")
|
||||
scheduled_actions = []
|
||||
|
||||
return {
|
||||
"request": request,
|
||||
"media": media,
|
||||
"namespace": namespace,
|
||||
"is_owner_or_editor": is_owner_or_editor,
|
||||
"ascii_preview": ascii_preview,
|
||||
"scheduled_actions": scheduled_actions,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1297,6 +1674,20 @@ def delete_media_view(request):
|
|||
if not media:
|
||||
return Response("Media not found.", status=404)
|
||||
|
||||
# Delete associated scheduled actions if table exists
|
||||
try:
|
||||
inspector = inspect(namespace_dbsession.bind)
|
||||
if "scheduled_actions" in inspector.get_table_names():
|
||||
scheduled_actions = (
|
||||
namespace_dbsession.query(ScheduledAction)
|
||||
.filter_by(media_id=media.id)
|
||||
.all()
|
||||
)
|
||||
for action in scheduled_actions:
|
||||
namespace_dbsession.delete(action)
|
||||
except Exception as e:
|
||||
log.debug(f"Could not delete scheduled actions: {e}")
|
||||
|
||||
namespace_dbsession.delete(media)
|
||||
namespace_dbsession.flush()
|
||||
|
||||
|
|
@ -1318,10 +1709,20 @@ def edit_media_get_view(request):
|
|||
if not media:
|
||||
return Response("Media not found.", status=404)
|
||||
|
||||
# Check if this is an ASCII file and get editable content
|
||||
ascii_content = None
|
||||
mime_type = get_mime_type(media.filename)
|
||||
if is_ascii_file(mime_type):
|
||||
try:
|
||||
ascii_content = base64.b64decode(media.media_b64).decode("utf-8")
|
||||
except (UnicodeDecodeError, Exception):
|
||||
ascii_content = None
|
||||
|
||||
return {
|
||||
"request": request,
|
||||
"media": media,
|
||||
"namespace": namespace,
|
||||
"ascii_content": ascii_content,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1340,6 +1741,20 @@ def edit_media_post_view(request):
|
|||
new_title = request.POST.get("title", "").strip()
|
||||
media.title = new_title
|
||||
|
||||
# Handle ASCII content update
|
||||
ascii_content = request.POST.get("ascii_content", "").strip()
|
||||
mime_type = get_mime_type(media.filename)
|
||||
if ascii_content and is_ascii_file(mime_type):
|
||||
try:
|
||||
# Update the media content with the new ASCII content
|
||||
encoded_str = base64.b64encode(ascii_content.encode("utf-8")).decode(
|
||||
"utf-8"
|
||||
)
|
||||
media.media_b64 = encoded_str
|
||||
media.size = len(ascii_content.encode("utf-8"))
|
||||
except Exception as e:
|
||||
return Response(f"Error updating content: {e}", status=400)
|
||||
|
||||
# Handle media file update
|
||||
new_media_file = request.POST.get("media_file")
|
||||
if new_media_file is not None and getattr(new_media_file, "filename", "").strip():
|
||||
|
|
@ -1358,12 +1773,14 @@ def edit_media_post_view(request):
|
|||
|
||||
media.filename = filename
|
||||
media.media_type = media_type
|
||||
media.mime_type = mime_type # Update mime_type too
|
||||
media.media_b64 = encoded_str
|
||||
media.size = file_size
|
||||
|
||||
# Handle public/private update
|
||||
is_public = request.POST.get("is_public") == "on"
|
||||
media.is_public = is_public
|
||||
# Handle visibility update
|
||||
visibility = request.POST.get("visibility", "public")
|
||||
if visibility in ["public", "private", "unlisted"]:
|
||||
media.visibility = visibility
|
||||
|
||||
namespace_dbsession.flush()
|
||||
|
||||
|
|
@ -1391,9 +1808,21 @@ def view_media_view(request):
|
|||
if not media:
|
||||
return Response("Media not found.", status=404)
|
||||
|
||||
# Check if media is public or user has access
|
||||
if not media.is_public and not check_namespace_permission(request, "reader"):
|
||||
role = get_user_or_agent_namespace_role(request)
|
||||
is_owner_or_editor = role in ["owner", "editor"]
|
||||
|
||||
# Check visibility permissions
|
||||
if media.visibility == "private" and not check_namespace_permission(
|
||||
request, "reader"
|
||||
):
|
||||
return Response("Media not available.", status=403)
|
||||
elif media.visibility == "unlisted":
|
||||
# Unlisted media is accessible via direct link but not listed
|
||||
if not is_owner_or_editor and not check_namespace_permission(request, "reader"):
|
||||
return Response("Media not available.", status=403)
|
||||
elif media.visibility == "public":
|
||||
# Public media is always accessible via direct link
|
||||
pass
|
||||
|
||||
media_data = base64.b64decode(media.media_b64)
|
||||
mime_type = get_mime_type(media.filename)
|
||||
|
|
@ -1423,6 +1852,248 @@ def view_media_view(request):
|
|||
return response
|
||||
|
||||
|
||||
################################################################################
|
||||
# Scheduled Action Management
|
||||
################################################################################
|
||||
|
||||
|
||||
@view_config(route_name="delete_scheduled_action", request_method="POST")
|
||||
@editor_required
|
||||
def delete_scheduled_action_view(request):
|
||||
namespace = request.namespace
|
||||
namespace_dbsession = request.namespace_dbsession
|
||||
media_short_id = request.matchdict.get("media_short_id")
|
||||
action_id = request.matchdict.get("action_id")
|
||||
|
||||
# Verify media exists
|
||||
media = namespace_dbsession.query(Media).filter_by(short_id=media_short_id).first()
|
||||
if not media:
|
||||
return Response("Media not found.", status=404)
|
||||
|
||||
# Check if scheduled_actions table exists
|
||||
try:
|
||||
inspector = inspect(namespace_dbsession.bind)
|
||||
if "scheduled_actions" not in inspector.get_table_names():
|
||||
return Response("Scheduled actions not supported.", status=404)
|
||||
except:
|
||||
return Response("Scheduled actions not supported.", status=404)
|
||||
|
||||
# Find and delete the scheduled action
|
||||
scheduled_action = (
|
||||
namespace_dbsession.query(ScheduledAction)
|
||||
.filter(ScheduledAction.id == action_id, ScheduledAction.media_id == media.id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not scheduled_action:
|
||||
return Response("Scheduled action not found.", status=404)
|
||||
|
||||
namespace_dbsession.delete(scheduled_action)
|
||||
namespace_dbsession.flush()
|
||||
|
||||
request.session.flash(
|
||||
f"Scheduled {scheduled_action.action_type.replace('set_', '').replace('_', ' ')} action deleted."
|
||||
)
|
||||
|
||||
return HTTPFound(
|
||||
location=request.route_url(
|
||||
"view_media_details",
|
||||
namespace_short_id=namespace.short_id,
|
||||
media_short_id=media.short_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@view_config(
|
||||
route_name="edit_scheduled_action",
|
||||
request_method="GET",
|
||||
renderer="edit_scheduled_action.html.j2",
|
||||
)
|
||||
@editor_required
|
||||
def edit_scheduled_action_get_view(request):
|
||||
namespace = request.namespace
|
||||
namespace_dbsession = request.namespace_dbsession
|
||||
media_short_id = request.matchdict.get("media_short_id")
|
||||
action_id = request.matchdict.get("action_id")
|
||||
|
||||
# Verify media exists
|
||||
media = namespace_dbsession.query(Media).filter_by(short_id=media_short_id).first()
|
||||
if not media:
|
||||
return Response("Media not found.", status=404)
|
||||
|
||||
# Check if scheduled_actions table exists
|
||||
try:
|
||||
inspector = inspect(namespace_dbsession.bind)
|
||||
if "scheduled_actions" not in inspector.get_table_names():
|
||||
return Response("Scheduled actions not supported.", status=404)
|
||||
except:
|
||||
return Response("Scheduled actions not supported.", status=404)
|
||||
|
||||
# Find the scheduled action
|
||||
scheduled_action = (
|
||||
namespace_dbsession.query(ScheduledAction)
|
||||
.filter(ScheduledAction.id == action_id, ScheduledAction.media_id == media.id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not scheduled_action:
|
||||
return Response("Scheduled action not found.", status=404)
|
||||
|
||||
return {
|
||||
"request": request,
|
||||
"namespace": namespace,
|
||||
"media": media,
|
||||
"scheduled_action": scheduled_action,
|
||||
}
|
||||
|
||||
|
||||
@view_config(route_name="edit_scheduled_action", request_method="POST")
|
||||
@editor_required
|
||||
def edit_scheduled_action_post_view(request):
|
||||
namespace = request.namespace
|
||||
namespace_dbsession = request.namespace_dbsession
|
||||
media_short_id = request.matchdict.get("media_short_id")
|
||||
action_id = request.matchdict.get("action_id")
|
||||
|
||||
# Verify media exists
|
||||
media = namespace_dbsession.query(Media).filter_by(short_id=media_short_id).first()
|
||||
if not media:
|
||||
return Response("Media not found.", status=404)
|
||||
|
||||
# Check if scheduled_actions table exists
|
||||
try:
|
||||
inspector = inspect(namespace_dbsession.bind)
|
||||
if "scheduled_actions" not in inspector.get_table_names():
|
||||
return Response("Scheduled actions not supported.", status=404)
|
||||
except:
|
||||
return Response("Scheduled actions not supported.", status=404)
|
||||
|
||||
# Find the scheduled action
|
||||
scheduled_action = (
|
||||
namespace_dbsession.query(ScheduledAction)
|
||||
.filter(ScheduledAction.id == action_id, ScheduledAction.media_id == media.id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not scheduled_action:
|
||||
return Response("Scheduled action not found.", status=404)
|
||||
|
||||
# Update the scheduled action
|
||||
new_action_type = request.POST.get("action_type")
|
||||
new_scheduled_date = request.POST.get("scheduled_date")
|
||||
|
||||
if new_action_type not in ["set_public", "set_private", "set_unlisted", "delete"]:
|
||||
return Response("Invalid action type.", status=400)
|
||||
|
||||
try:
|
||||
scheduled_date = datetime.datetime.fromisoformat(new_scheduled_date)
|
||||
except ValueError:
|
||||
return Response("Invalid date format.", status=400)
|
||||
|
||||
scheduled_action.action_type = new_action_type
|
||||
scheduled_action.scheduled_date = scheduled_date
|
||||
namespace_dbsession.flush()
|
||||
|
||||
request.session.flash(
|
||||
f"Scheduled action updated to {new_action_type.replace('set_', '').replace('_', ' ')} on {scheduled_date.strftime('%Y-%m-%d %H:%M')}."
|
||||
)
|
||||
|
||||
return HTTPFound(
|
||||
location=request.route_url(
|
||||
"view_media_details",
|
||||
namespace_short_id=namespace.short_id,
|
||||
media_short_id=media.short_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@view_config(
|
||||
route_name="add_scheduled_action",
|
||||
request_method="GET",
|
||||
renderer="add_scheduled_action.html.j2",
|
||||
)
|
||||
@editor_required
|
||||
def add_scheduled_action_get_view(request):
|
||||
namespace = request.namespace
|
||||
namespace_dbsession = request.namespace_dbsession
|
||||
media_short_id = request.matchdict.get("media_short_id")
|
||||
|
||||
# Verify media exists
|
||||
media = namespace_dbsession.query(Media).filter_by(short_id=media_short_id).first()
|
||||
if not media:
|
||||
return Response("Media not found.", status=404)
|
||||
|
||||
# Check if scheduled_actions table exists
|
||||
try:
|
||||
inspector = inspect(namespace_dbsession.bind)
|
||||
if "scheduled_actions" not in inspector.get_table_names():
|
||||
return Response("Scheduled actions not supported.", status=404)
|
||||
except:
|
||||
return Response("Scheduled actions not supported.", status=404)
|
||||
|
||||
return {
|
||||
"request": request,
|
||||
"namespace": namespace,
|
||||
"media": media,
|
||||
}
|
||||
|
||||
|
||||
@view_config(route_name="add_scheduled_action", request_method="POST")
|
||||
@editor_required
|
||||
def add_scheduled_action_post_view(request):
|
||||
namespace = request.namespace
|
||||
namespace_dbsession = request.namespace_dbsession
|
||||
media_short_id = request.matchdict.get("media_short_id")
|
||||
|
||||
# Verify media exists
|
||||
media = namespace_dbsession.query(Media).filter_by(short_id=media_short_id).first()
|
||||
if not media:
|
||||
return Response("Media not found.", status=404)
|
||||
|
||||
# Check if scheduled_actions table exists
|
||||
try:
|
||||
inspector = inspect(namespace_dbsession.bind)
|
||||
if "scheduled_actions" not in inspector.get_table_names():
|
||||
return Response("Scheduled actions not supported.", status=404)
|
||||
except:
|
||||
return Response("Scheduled actions not supported.", status=404)
|
||||
|
||||
# Get form data
|
||||
action_type = request.POST.get("action_type")
|
||||
scheduled_date_str = request.POST.get("scheduled_date")
|
||||
|
||||
if action_type not in ["set_public", "set_private", "set_unlisted", "delete"]:
|
||||
return Response("Invalid action type.", status=400)
|
||||
|
||||
try:
|
||||
scheduled_date = datetime.datetime.fromisoformat(scheduled_date_str)
|
||||
except ValueError:
|
||||
return Response("Invalid date format.", status=400)
|
||||
|
||||
# Create new scheduled action
|
||||
action_uuid = uuid.uuid4()
|
||||
scheduled_action = ScheduledAction(
|
||||
id=str(action_uuid),
|
||||
media_id=media.id,
|
||||
action_type=action_type,
|
||||
scheduled_date=scheduled_date,
|
||||
)
|
||||
namespace_dbsession.add(scheduled_action)
|
||||
namespace_dbsession.flush()
|
||||
|
||||
request.session.flash(
|
||||
f"Scheduled {action_type.replace('set_', '').replace('_', ' ')} action added for {scheduled_date.strftime('%Y-%m-%d %H:%M')}."
|
||||
)
|
||||
|
||||
return HTTPFound(
|
||||
location=request.route_url(
|
||||
"view_media_details",
|
||||
namespace_short_id=namespace.short_id,
|
||||
media_short_id=media.short_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
################################################################################
|
||||
# Main
|
||||
################################################################################
|
||||
|
|
@ -1471,6 +2142,9 @@ def main(*config, **settings):
|
|||
session_factory_ = sessionmaker(bind=engine)
|
||||
Base.metadata.bind = engine
|
||||
|
||||
# Create tables if they don't exist
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
DBSession = scoped_session(session_factory_)
|
||||
# Register with zope.sqlalchemy
|
||||
register(DBSession)
|
||||
|
|
@ -1539,10 +2213,40 @@ def main(*config, **settings):
|
|||
"/namespace/{namespace_short_id}/media/{media_short_id}",
|
||||
)
|
||||
|
||||
# Scheduled Actions Management
|
||||
config.add_route(
|
||||
"delete_scheduled_action",
|
||||
"/namespace/{namespace_short_id}/media/{media_short_id}/scheduled_action/{action_id}/delete",
|
||||
)
|
||||
config.add_route(
|
||||
"edit_scheduled_action",
|
||||
"/namespace/{namespace_short_id}/media/{media_short_id}/scheduled_action/{action_id}/edit",
|
||||
)
|
||||
config.add_route(
|
||||
"add_scheduled_action",
|
||||
"/namespace/{namespace_short_id}/media/{media_short_id}/add_scheduled_action",
|
||||
)
|
||||
|
||||
config.scan()
|
||||
|
||||
# Start the scheduler
|
||||
start_scheduler()
|
||||
|
||||
return config.make_wsgi_app()
|
||||
|
||||
|
||||
# Signal handler for graceful shutdown
|
||||
def signal_handler(signum, frame):
|
||||
log.info("Received shutdown signal, stopping scheduler...")
|
||||
stop_scheduler()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
# Register signal handlers
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
|
||||
# Expose the WSGI application callable for uwsgi
|
||||
uwsgi_app = main({})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue