Adds Schedule Configuration UI & DB
This commit is contained in:
parent
36170462fa
commit
2b45c67ddf
@ -1,6 +1,7 @@
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .db import db
|
from .db import db
|
||||||
|
from .models import init_schedule_config
|
||||||
|
|
||||||
def create_app(test_config=None):
|
def create_app(test_config=None):
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@ -13,6 +14,7 @@ def create_app(test_config=None):
|
|||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
init_schedule_config()
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_app_title():
|
def inject_app_title():
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
from flask_sqlalchemy import SQLAlchemy, current_app, g
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
|
||||||
db_name = 'scipaperloader.db'
|
|
||||||
|
|
||||||
|
@ -16,4 +16,38 @@ class Paper(db.Model):
|
|||||||
|
|
||||||
class ScheduleConfig(db.Model):
|
class ScheduleConfig(db.Model):
|
||||||
hour = db.Column(db.Integer, primary_key=True) # 0-23
|
hour = db.Column(db.Integer, primary_key=True) # 0-23
|
||||||
volume = db.Column(db.Float) # weight or count
|
weight = db.Column(db.Float) # weight
|
||||||
|
|
||||||
|
class VolumeConfig(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
volume = db.Column(db.Float) # volume of papers to scrape per day
|
||||||
|
|
||||||
|
def init_schedule_config():
|
||||||
|
"""Initialize ScheduleConfig with default values if empty"""
|
||||||
|
if ScheduleConfig.query.count() == 0:
|
||||||
|
# Default schedule: Lower volume during business hours,
|
||||||
|
# higher volume at night
|
||||||
|
default_schedule = [
|
||||||
|
# Night hours (higher volume)
|
||||||
|
*[(hour, 1.0) for hour in range(0, 6)],
|
||||||
|
# Morning hours (low volume)
|
||||||
|
*[(hour, 0.3) for hour in range(6, 9)],
|
||||||
|
# Business hours (very low volume)
|
||||||
|
*[(hour, 0.2) for hour in range(9, 17)],
|
||||||
|
# Evening hours (medium volume)
|
||||||
|
*[(hour, 0.5) for hour in range(17, 21)],
|
||||||
|
# Late evening (high volume)
|
||||||
|
*[(hour, 0.8) for hour in range(21, 24)]
|
||||||
|
]
|
||||||
|
|
||||||
|
for hour, weight in default_schedule:
|
||||||
|
config = ScheduleConfig(hour=hour, weight=weight)
|
||||||
|
db.session.add(config)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if VolumeConfig.query.count() == 0:
|
||||||
|
# Default volume configuration
|
||||||
|
default_volume = VolumeConfig(volume=100)
|
||||||
|
db.session.add(default_volume)
|
||||||
|
db.session.commit()
|
222
scipaperloader/templates/schedule.html
Normal file
222
scipaperloader/templates/schedule.html
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<style>
|
||||||
|
.timeline {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
user-select: none; /* Prevent text selection during drag */
|
||||||
|
}
|
||||||
|
|
||||||
|
.hour-block {
|
||||||
|
width: 60px;
|
||||||
|
height: 70px; /* Increased height to fit additional text */
|
||||||
|
border-radius: 6px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding-top: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hour-block.selected {
|
||||||
|
outline: 2px solid #4584b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.papers {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message {
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message.success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
border-color: #c3e6cb;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message.error {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border-color: #f5c6cb;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message.fade {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const initialSchedule = {{ schedule | tojson }};
|
||||||
|
const totalVolume = {{ volume }};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div x-data="scheduleManager(initialSchedule, totalVolume)" class="container my-5">
|
||||||
|
<h2 class="mb-4">🕒 Configure Hourly Download Weights</h2>
|
||||||
|
|
||||||
|
<!-- Flash Messages -->
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
<div id="flash-messages">
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="flash-message {{ category }}" x-data="{}" x-init="setTimeout(() => $el.classList.add('fade'), 100); setTimeout(() => $el.remove(), 5000)">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<p class="text-muted">
|
||||||
|
Click to select one or more hours below. Then assign a weight to them using the input and apply it. Color indicates relative intensity. The total daily volume will be split proportionally across these weights.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="mt-4">Volume</h3>
|
||||||
|
<p class="text-muted">The total volume of data to be downloaded each day is <strong x-text="volume"></strong> papers.</p>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2">
|
||||||
|
<p class="text-muted mb-0" style="max-width: 600px;">
|
||||||
|
Click to select one or more hours below. Then assign a weight to them using the input and apply it. Color indicates relative intensity. The total daily volume will be split proportionally across these weights.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="hour-block"
|
||||||
|
style="pointer-events: none;"
|
||||||
|
x-init="$nextTick(() => previewWeight = 1.0)"
|
||||||
|
x-data="{ previewWeight: 1.0 }"
|
||||||
|
:style="getBackgroundStyleFromValue(previewWeight)">
|
||||||
|
<div><strong>14:00</strong></div>
|
||||||
|
<div class="weight"><span x-text="previewWeight.toFixed(1)"></span></div>
|
||||||
|
<div class="papers text-muted">example</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<h3 class="mt-4">Current Schedule</h3>
|
||||||
|
<form method="POST" action="{{ url_for('main.schedule') }}">
|
||||||
|
<div class="timeline mb-3" @mouseup="endDrag()" @mouseleave="endDrag()">
|
||||||
|
<template x-for="hour in Object.keys(schedule)" :key="hour">
|
||||||
|
<div class="hour-block"
|
||||||
|
:id="'hour-' + hour"
|
||||||
|
:data-hour="hour"
|
||||||
|
:style="getBackgroundStyle(hour)"
|
||||||
|
:class="{'selected': isSelected(hour)}"
|
||||||
|
@mousedown="startDrag($event, hour)"
|
||||||
|
@mouseover="dragSelect(hour)">
|
||||||
|
<div><strong x-text="formatHour(hour)"></strong></div>
|
||||||
|
<div class="weight"><span x-text="schedule[hour]"></span></div>
|
||||||
|
<div class="papers"><span x-text="getPapersPerHour(hour)"></span> p.</div>
|
||||||
|
<input type="hidden" :name="'hour_' + hour" :value="schedule[hour]">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group mb-4 w-50">
|
||||||
|
<label class="input-group-text">Set Weight:</label>
|
||||||
|
<input type="number" step="0.1" min="0" max="5" x-model="newWeight" class="form-control">
|
||||||
|
<button type="button" class="btn btn-outline-primary" @click="applyWeight()">Apply to Selected</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="/" class="btn btn-outline-secondary">⬅ Back</a>
|
||||||
|
<button type="submit" class="btn btn-success">💾 Save Schedule</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function scheduleManager(initial, volume) {
|
||||||
|
return {
|
||||||
|
schedule: { ...initial },
|
||||||
|
volume: volume,
|
||||||
|
selectedHours: [],
|
||||||
|
newWeight: 1.0,
|
||||||
|
isDragging: false,
|
||||||
|
dragOperation: null,
|
||||||
|
|
||||||
|
formatHour(h) {
|
||||||
|
return String(h).padStart(2, '0') + ":00";
|
||||||
|
},
|
||||||
|
|
||||||
|
getBackgroundStyle(hour) {
|
||||||
|
const weight = parseFloat(this.schedule[hour]);
|
||||||
|
const maxWeight = 2.5; // You can adjust this
|
||||||
|
|
||||||
|
// Normalize weight (0.0 to 1.0)
|
||||||
|
const t = Math.min(weight / maxWeight, 1.0);
|
||||||
|
|
||||||
|
// Interpolate HSL lightness: 95% (light) to 30% (dark)
|
||||||
|
const lightness = 95 - (t * 65); // 95 → 30
|
||||||
|
const backgroundColor = `hsl(210, 10%, ${lightness}%)`; // soft gray-blue palette
|
||||||
|
|
||||||
|
const textColor = t > 0.65 ? 'white' : 'black'; // adaptive text color
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor,
|
||||||
|
color: textColor
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
startDrag(event, hour) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.isDragging = true;
|
||||||
|
this.dragOperation = this.isSelected(hour) ? 'remove' : 'add';
|
||||||
|
this.toggleSelect(hour);
|
||||||
|
},
|
||||||
|
|
||||||
|
dragSelect(hour) {
|
||||||
|
if (!this.isDragging) return;
|
||||||
|
const selected = this.isSelected(hour);
|
||||||
|
if (this.dragOperation === 'add' && !selected) {
|
||||||
|
this.selectedHours.push(hour);
|
||||||
|
} else if (this.dragOperation === 'remove' && selected) {
|
||||||
|
this.selectedHours = this.selectedHours.filter(h => h !== hour);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
endDrag() {
|
||||||
|
this.isDragging = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleSelect(hour) {
|
||||||
|
if (this.isSelected(hour)) {
|
||||||
|
this.selectedHours = this.selectedHours.filter(h => h !== hour);
|
||||||
|
} else {
|
||||||
|
this.selectedHours.push(hour);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isSelected(hour) {
|
||||||
|
return this.selectedHours.includes(hour);
|
||||||
|
},
|
||||||
|
|
||||||
|
applyWeight() {
|
||||||
|
this.selectedHours.forEach(hour => {
|
||||||
|
this.schedule[hour] = parseFloat(this.newWeight).toFixed(1);
|
||||||
|
});
|
||||||
|
this.selectedHours = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
getTotalWeight() {
|
||||||
|
return Object.values(this.schedule).reduce((sum, w) => sum + parseFloat(w), 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
getPapersPerHour(hour) {
|
||||||
|
const total = this.getTotalWeight();
|
||||||
|
if (total === 0) return 0;
|
||||||
|
return ((parseFloat(this.schedule[hour]) / total) * this.volume).toFixed(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>{% endblock %}
|
@ -1,4 +1,6 @@
|
|||||||
from flask import Blueprint, render_template, current_app, request
|
from flask import Blueprint, render_template, current_app, request
|
||||||
|
from .models import ScheduleConfig, VolumeConfig
|
||||||
|
from .db import db
|
||||||
|
|
||||||
bp = Blueprint('main', __name__)
|
bp = Blueprint('main', __name__)
|
||||||
|
|
||||||
@ -17,9 +19,22 @@ def upload():
|
|||||||
def papers():
|
def papers():
|
||||||
return render_template("papers.html", app_title="PaperScraper")
|
return render_template("papers.html", app_title="PaperScraper")
|
||||||
|
|
||||||
@bp.route("/schedule")
|
@bp.route("/schedule", methods=["GET", "POST"])
|
||||||
def schedule():
|
def schedule():
|
||||||
return render_template("schedule.html", app_title="PaperScraper")
|
if request.method == "POST":
|
||||||
|
for hour in range(24):
|
||||||
|
key = f"hour_{hour}"
|
||||||
|
weight = float(request.form.get(key, 0))
|
||||||
|
config = ScheduleConfig.query.get(hour)
|
||||||
|
if config:
|
||||||
|
config.weight = weight
|
||||||
|
else:
|
||||||
|
db.session.add(ScheduleConfig(hour=hour, weight=weight))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
schedule = {sc.hour: sc.weight for sc in ScheduleConfig.query.order_by(ScheduleConfig.hour).all()}
|
||||||
|
volume = VolumeConfig.query.first()
|
||||||
|
return render_template("schedule.html", schedule=schedule, volume=volume.volume, app_title="PaperScraper")
|
||||||
|
|
||||||
@bp.route("/logs")
|
@bp.route("/logs")
|
||||||
def logs():
|
def logs():
|
||||||
|
Loading…
x
Reference in New Issue
Block a user