Compare commits
4 Commits
a08c1ea5f0
...
832c95198d
Author | SHA1 | Date |
---|---|---|
Oystein Kristoffer Tveit | 832c95198d | |
Oystein Kristoffer Tveit | 03f221a807 | |
Oystein Kristoffer Tveit | 369180ff85 | |
Oystein Kristoffer Tveit | 1550c1f2e3 |
|
@ -70,8 +70,8 @@ Run `poetry run worblehat --help` for more info
|
||||||
### Deadline daemon
|
### Deadline daemon
|
||||||
|
|
||||||
- [X] Ability to be notified when deadlines are due
|
- [X] Ability to be notified when deadlines are due
|
||||||
- [ ] Ability to be notified when books are available
|
- [X] Ability to be notified when books are available
|
||||||
- [ ] Ability to have expiring queue positions automatically expire
|
- [X] Ability to have expiring queue positions automatically expire
|
||||||
|
|
||||||
### Web version of the program
|
### Web version of the program
|
||||||
|
|
||||||
|
|
|
@ -33,5 +33,8 @@ from = 'worblehat@pvv.ntnu.no'
|
||||||
subject_prefix = '[Worblehat]'
|
subject_prefix = '[Worblehat]'
|
||||||
|
|
||||||
[deadline_daemon]
|
[deadline_daemon]
|
||||||
warn_days_before_borrow_deadline = [ "5", "1" ]
|
enabled = true
|
||||||
warn_days_before_expiring_queue_position_deadline = [ "3", "1" ]
|
dryrun = false
|
||||||
|
warn_days_before_borrowing_deadline = [ 5, 1 ]
|
||||||
|
days_before_queue_position_expires = 14
|
||||||
|
warn_days_before_expiring_queue_position_deadline = [ 3, 1 ]
|
|
@ -12,14 +12,10 @@ isbn, note, bookcase, shelf
|
||||||
9781292024967, abstract alg, arbeidsrom_smal, 5
|
9781292024967, abstract alg, arbeidsrom_smal, 5
|
||||||
9780471728979, kreyzig, arbeidsrom_smal, 5
|
9780471728979, kreyzig, arbeidsrom_smal, 5
|
||||||
9781847762399, calc 1, arbeidsrom_smal, 5
|
9781847762399, calc 1, arbeidsrom_smal, 5
|
||||||
9781847762399, calc 1 again, arbeidsrom_smal, 5
|
|
||||||
9781787267763, calc 1 nome, arbeidsrom_smal, 5
|
9781787267763, calc 1 nome, arbeidsrom_smal, 5
|
||||||
9781787267770, calc 2 nome, arbeidsrom_smal, 5
|
9781787267770, calc 2 nome, arbeidsrom_smal, 5
|
||||||
9780199208258, non lin ode, arbeidsrom_smal, 5
|
9780199208258, non lin ode, arbeidsrom_smal, 5
|
||||||
9788251915953, tabeller, arbeidsrom_smal, 5
|
9788251915953, tabeller, arbeidsrom_smal, 5
|
||||||
9788251915953, taeller 2, arbeidsrom_smal, 5
|
|
||||||
9788251915953, tabeller 3, arbeidsrom_smal, 5
|
|
||||||
9788251915953, tabeller 4, arbeidsrom_smal, 5
|
|
||||||
9780750304009, fractals and chaos, arbeidsrom_smal, 5
|
9780750304009, fractals and chaos, arbeidsrom_smal, 5
|
||||||
9788241902116, geometri, arbeidsrom_smal, 5
|
9788241902116, geometri, arbeidsrom_smal, 5
|
||||||
9781620402788, simpsons, arbeidsrom_smal, 5
|
9781620402788, simpsons, arbeidsrom_smal, 5
|
||||||
|
|
|
|
@ -39,5 +39,9 @@
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
devShells.${system}.default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [ poetry sqlite ];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
event,
|
event,
|
||||||
select,
|
select,
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
@ -191,10 +191,10 @@ class WorblehatCli(NumberedCmd):
|
||||||
|
|
||||||
def do_exit(self, _: str):
|
def do_exit(self, _: str):
|
||||||
if self.sql_session_dirty:
|
if self.sql_session_dirty:
|
||||||
if prompt_yes_no('Would you like to save your changes?'):
|
if prompt_yes_no('Would you like to save your changes?'):
|
||||||
self.sql_session.commit()
|
self.sql_session.commit()
|
||||||
else:
|
else:
|
||||||
self.sql_session.rollback()
|
self.sql_session.rollback()
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,14 +11,25 @@ from worblehat.models import (
|
||||||
DeadlineDaemonLastRunDatetime,
|
DeadlineDaemonLastRunDatetime,
|
||||||
BookcaseItemBorrowingQueue,
|
BookcaseItemBorrowingQueue,
|
||||||
)
|
)
|
||||||
|
|
||||||
from worblehat.services.email import send_email
|
from worblehat.services.email import send_email
|
||||||
|
|
||||||
class DeadlineDaemon:
|
class DeadlineDaemon:
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
|
if not Config['deadline_daemon.enabled']:
|
||||||
|
return
|
||||||
|
|
||||||
self.sql_session = sql_session
|
self.sql_session = sql_session
|
||||||
|
|
||||||
self.last_run = self.sql_session.scalars(
|
self.last_run = self.sql_session.scalars(
|
||||||
select(DeadlineDaemonLastRunDatetime),
|
select(DeadlineDaemonLastRunDatetime),
|
||||||
).one()
|
).one_or_none()
|
||||||
|
|
||||||
|
if self.last_run is None:
|
||||||
|
logging.info('No previous run found, assuming this is the first run')
|
||||||
|
self.last_run = DeadlineDaemonLastRunDatetime(time=datetime.now())
|
||||||
|
self.sql_session.add(self.last_run)
|
||||||
|
self.sql_session.commit()
|
||||||
|
|
||||||
self.last_run_datetime = self.last_run.time
|
self.last_run_datetime = self.last_run.time
|
||||||
self.current_run_datetime = datetime.now()
|
self.current_run_datetime = datetime.now()
|
||||||
|
@ -26,6 +37,12 @@ class DeadlineDaemon:
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
logging.info('Deadline daemon started')
|
logging.info('Deadline daemon started')
|
||||||
|
if not Config['deadline_daemon.enabled']:
|
||||||
|
logging.warn('Deadline daemon disabled, exiting')
|
||||||
|
return
|
||||||
|
|
||||||
|
if Config['deadline_daemon.dryrun']:
|
||||||
|
logging.warn('Running in dryrun mode')
|
||||||
|
|
||||||
self.send_close_deadline_reminder_mails()
|
self.send_close_deadline_reminder_mails()
|
||||||
self.send_overdue_mails()
|
self.send_overdue_mails()
|
||||||
|
@ -36,6 +53,97 @@ class DeadlineDaemon:
|
||||||
self.last_run.time = self.current_run_datetime
|
self.last_run.time = self.current_run_datetime
|
||||||
self.sql_session.commit()
|
self.sql_session.commit()
|
||||||
|
|
||||||
|
###################
|
||||||
|
# EMAIL TEMPLATES #
|
||||||
|
###################
|
||||||
|
|
||||||
|
def _send_close_deadline_mail(self, borrowing: BookcaseItemBorrowing):
|
||||||
|
logging.info(f'Sending close deadline mail to {borrowing.username}@pvv.ntnu.no.')
|
||||||
|
send_email(
|
||||||
|
f'{borrowing.username}@pvv.ntnu.no',
|
||||||
|
'Reminder - Your borrowing deadline is approaching',
|
||||||
|
dedent(f'''
|
||||||
|
Your borrowing deadline for the following item is approaching:
|
||||||
|
|
||||||
|
{borrowing.item.name}
|
||||||
|
|
||||||
|
Please return the item by {borrowing.end_time.strftime("%a %b %d, %Y")}
|
||||||
|
''',
|
||||||
|
).strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _send_overdue_mail(self, borrowing: BookcaseItemBorrowing):
|
||||||
|
logging.info(f'Sending overdue mail to {borrowing.username}@pvv.ntnu.no for {borrowing.item.isbn} - {borrowing.end_time.strftime("%a %b %d, %Y")}')
|
||||||
|
send_email(
|
||||||
|
f'{borrowing.username}@pvv.ntnu.no',
|
||||||
|
'Your deadline has passed',
|
||||||
|
dedent(f'''
|
||||||
|
Your delivery deadline for the following item has passed:
|
||||||
|
|
||||||
|
{borrowing.item.name}
|
||||||
|
|
||||||
|
Please return the item as soon as possible.
|
||||||
|
''',
|
||||||
|
).strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _send_newly_available_mail(self, queue_item: BookcaseItemBorrowingQueue):
|
||||||
|
logging.info(f'Sending newly available mail to {queue_item.username}')
|
||||||
|
|
||||||
|
days_before_queue_expires = Config['deadline_daemon.days_before_queue_position_expires']
|
||||||
|
|
||||||
|
# TODO: calculate and format the date of when the queue position expires in the mail.
|
||||||
|
send_email(
|
||||||
|
f'{queue_item.username}@pvv.ntnu.no',
|
||||||
|
'An item you have queued for is now available',
|
||||||
|
dedent(f'''
|
||||||
|
The following item is now available for you to borrow:
|
||||||
|
|
||||||
|
{queue_item.item.name}
|
||||||
|
|
||||||
|
Please pick up the item within {days_before_queue_expires} days.
|
||||||
|
''',
|
||||||
|
).strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _send_expiring_queue_position_mail(self, queue_position: BookcaseItemBorrowingQueue, day: int):
|
||||||
|
logging.info(f'Sending queue position expiry reminder to {queue_position.username}@pvv.ntnu.no.')
|
||||||
|
send_email(
|
||||||
|
f'{queue_position.username}@pvv.ntnu.no',
|
||||||
|
'Reminder - Your queue position expiry deadline is approaching',
|
||||||
|
dedent(f'''
|
||||||
|
Your queue position expiry deadline for the following item is approaching:
|
||||||
|
|
||||||
|
{queue_position.item.name}
|
||||||
|
|
||||||
|
Please borrow the item by {(queue_position.item_became_available_time + timedelta(days=day)).strftime("%a %b %d, %Y")}
|
||||||
|
''',
|
||||||
|
).strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _send_queue_position_expired_mail(self, queue_position: BookcaseItemBorrowingQueue):
|
||||||
|
send_email(
|
||||||
|
f'{queue_position.username}@pvv.ntnu.no',
|
||||||
|
'Your queue position has expired',
|
||||||
|
dedent(f'''
|
||||||
|
Your queue position for the following item has expired:
|
||||||
|
|
||||||
|
{queue_position.item.name}
|
||||||
|
|
||||||
|
You can queue for the item again at any time, but you will be placed at the back of the queue.
|
||||||
|
|
||||||
|
There are currently {len(queue_position.item.borrowing_queue)} users in the queue.
|
||||||
|
''',
|
||||||
|
).strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
##################
|
||||||
|
# EMAIL ROUTINES #
|
||||||
|
##################
|
||||||
|
|
||||||
def _sql_subtract_date(self, x: datetime, y: timedelta):
|
def _sql_subtract_date(self, x: datetime, y: timedelta):
|
||||||
if self.sql_session.bind.dialect.name == 'sqlite':
|
if self.sql_session.bind.dialect.name == 'sqlite':
|
||||||
|
@ -48,10 +156,10 @@ class DeadlineDaemon:
|
||||||
|
|
||||||
|
|
||||||
def send_close_deadline_reminder_mails(self):
|
def send_close_deadline_reminder_mails(self):
|
||||||
logging.info('Sending mails about items with a closing deadline')
|
logging.info('Sending mails for items with a closing deadline')
|
||||||
|
|
||||||
# TODO: This should be int-parsed and validated before the daemon started
|
# TODO: This should be int-parsed and validated before the daemon started
|
||||||
days = [int(d) for d in Config['deadline_daemon.warn_days_before_borrow_deadline']]
|
days = [int(d) for d in Config['deadline_daemon.warn_days_before_borrowing_deadline']]
|
||||||
|
|
||||||
for day in days:
|
for day in days:
|
||||||
borrowings_to_remind = self.sql_session.scalars(
|
borrowings_to_remind = self.sql_session.scalars(
|
||||||
|
@ -69,23 +177,11 @@ class DeadlineDaemon:
|
||||||
),
|
),
|
||||||
).all()
|
).all()
|
||||||
for borrowing in borrowings_to_remind:
|
for borrowing in borrowings_to_remind:
|
||||||
logging.info(f' Sending close deadline mail to {borrowing.username}@pvv.ntnu.no. {day} days left')
|
self._send_close_deadline_mail(borrowing)
|
||||||
send_email(
|
|
||||||
f'{borrowing.username}@pvv.ntnu.no',
|
|
||||||
'Reminder - Your borrowing deadline is approaching',
|
|
||||||
dedent(f'''
|
|
||||||
Your borrowing deadline for the following item is approaching:
|
|
||||||
|
|
||||||
{borrowing.item.name}
|
|
||||||
|
|
||||||
Please return the item by {borrowing.end_time.strftime("%a %b %d, %Y")}
|
|
||||||
''',
|
|
||||||
).strip(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def send_overdue_mails(self):
|
def send_overdue_mails(self):
|
||||||
logging.info('Sending mails about overdue items')
|
logging.info('Sending mails for overdue items')
|
||||||
|
|
||||||
to_remind = self.sql_session.scalars(
|
to_remind = self.sql_session.scalars(
|
||||||
select(BookcaseItemBorrowing)
|
select(BookcaseItemBorrowing)
|
||||||
|
@ -96,19 +192,7 @@ class DeadlineDaemon:
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
for borrowing in to_remind:
|
for borrowing in to_remind:
|
||||||
logging.info(f' Sending overdue mail to {borrowing.username}@pvv.ntnu.no for {borrowing.item.isbn} - {borrowing.end_time.strftime("%a %b %d, %Y")}')
|
self._send_overdue_mail(borrowing)
|
||||||
send_email(
|
|
||||||
f'{borrowing.username}@pvv.ntnu.no',
|
|
||||||
'Your deadline has passed',
|
|
||||||
dedent(f'''
|
|
||||||
Your delivery deadline for the following item has passed:
|
|
||||||
|
|
||||||
{borrowing.item.name}
|
|
||||||
|
|
||||||
Please return the item as soon as possible.
|
|
||||||
''',
|
|
||||||
).strip(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def send_newly_available_mails(self):
|
def send_newly_available_mails(self):
|
||||||
|
@ -133,15 +217,75 @@ class DeadlineDaemon:
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
for queue_item in newly_available:
|
for queue_item in newly_available:
|
||||||
logging.info(f'Sending newly available mail to {queue_item.username}')
|
logging.info(f'Adding user {queue_item.username} to queue for {queue_item.item.name}')
|
||||||
logging.warning('Not implemented')
|
queue_item.item_became_available_time = self.current_run_datetime
|
||||||
|
self.sql_session.commit()
|
||||||
|
|
||||||
|
self._send_newly_available_mail(queue_item)
|
||||||
|
|
||||||
|
|
||||||
def send_expiring_queue_position_mails(self):
|
def send_expiring_queue_position_mails(self):
|
||||||
logging.info('Sending mails about queue positions which are expiring soon')
|
logging.info('Sending mails about queue positions which are expiring soon')
|
||||||
logging.warning('Not implemented')
|
logging.warning('Not implemented')
|
||||||
|
|
||||||
|
days = [int(d) for d in Config['deadline_daemon.warn_days_before_expiring_queue_position_deadline']]
|
||||||
|
for day in days:
|
||||||
|
queue_positions_to_remind = self.sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowingQueue)
|
||||||
|
.join(
|
||||||
|
BookcaseItemBorrowing,
|
||||||
|
BookcaseItemBorrowing.fk_bookcase_item_uid == BookcaseItemBorrowingQueue.fk_bookcase_item_uid,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
self._sql_subtract_date(
|
||||||
|
BookcaseItemBorrowingQueue.item_became_available_time + timedelta(days=day),
|
||||||
|
timedelta(days=day),
|
||||||
|
)
|
||||||
|
.between(
|
||||||
|
self.last_run_datetime,
|
||||||
|
self.current_run_datetime,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).all()
|
||||||
|
|
||||||
|
for queue_position in queue_positions_to_remind:
|
||||||
|
self._send_expiring_queue_position_mail(queue_position, day)
|
||||||
|
|
||||||
|
|
||||||
def auto_expire_queue_positions(self):
|
def auto_expire_queue_positions(self):
|
||||||
logging.info('Expiring queue positions which are too old')
|
logging.info('Expiring queue positions which are too old')
|
||||||
logging.warning('Not implemented')
|
|
||||||
|
queue_position_expiry_days = int(Config['deadline_daemon.days_before_queue_position_expires'])
|
||||||
|
|
||||||
|
overdue_queue_positions = self.sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowingQueue)
|
||||||
|
.where(
|
||||||
|
BookcaseItemBorrowingQueue.item_became_available_time + timedelta(days=queue_position_expiry_days) < self.current_run_datetime,
|
||||||
|
BookcaseItemBorrowingQueue.expired.is_(False),
|
||||||
|
),
|
||||||
|
).all()
|
||||||
|
|
||||||
|
for queue_position in overdue_queue_positions:
|
||||||
|
logging.info(f'Expiring queue position for {queue_position.username} for item {queue_position.item.name}')
|
||||||
|
|
||||||
|
queue_position.expired = True
|
||||||
|
|
||||||
|
next_queue_position = self.sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowingQueue)
|
||||||
|
.where(
|
||||||
|
BookcaseItemBorrowingQueue.fk_bookcase_item_uid == queue_position.fk_bookcase_item_uid,
|
||||||
|
BookcaseItemBorrowingQueue.item_became_available_time.is_(None),
|
||||||
|
)
|
||||||
|
.order_by(BookcaseItemBorrowingQueue.entered_queue_time)
|
||||||
|
.limit(1),
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
self._send_queue_position_expired_mail(queue_position)
|
||||||
|
|
||||||
|
if next_queue_position is not None:
|
||||||
|
next_queue_position.item_became_available_time = self.current_run_datetime
|
||||||
|
|
||||||
|
logging.info(f'Next user in queue for item {next_queue_position.item.name} is {next_queue_position.username}')
|
||||||
|
self._send_newly_available_mail(next_queue_position)
|
||||||
|
|
||||||
|
self.sql_session.commit()
|
|
@ -0,0 +1,117 @@
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from worblehat.models import (
|
||||||
|
BookcaseItem,
|
||||||
|
BookcaseItemBorrowing,
|
||||||
|
BookcaseItemBorrowingQueue,
|
||||||
|
DeadlineDaemonLastRunDatetime,
|
||||||
|
)
|
||||||
|
|
||||||
|
from worblehat.services.config import Config
|
||||||
|
|
||||||
|
from .seed_test_data import main as seed_test_data_main
|
||||||
|
|
||||||
|
|
||||||
|
def clear_db(sql_session):
|
||||||
|
sql_session.query(BookcaseItemBorrowingQueue).delete()
|
||||||
|
sql_session.query(BookcaseItemBorrowing).delete()
|
||||||
|
sql_session.query(DeadlineDaemonLastRunDatetime).delete()
|
||||||
|
sql_session.commit()
|
||||||
|
|
||||||
|
# NOTE: feel free to change this function to suit your needs
|
||||||
|
# it's just a quick and dirty way to get some data into the database
|
||||||
|
# for testing the deadline daemon - oysteikt 2024
|
||||||
|
def main(sql_session):
|
||||||
|
borrow_warning_days = [timedelta(days=int(d)) for d in Config['deadline_daemon.warn_days_before_borrowing_deadline']]
|
||||||
|
queue_warning_days = [timedelta(days=int(d)) for d in Config['deadline_daemon.warn_days_before_expiring_queue_position_deadline']]
|
||||||
|
queue_expire_days = int(Config['deadline_daemon.days_before_queue_position_expires'])
|
||||||
|
|
||||||
|
clear_db(sql_session)
|
||||||
|
seed_test_data_main(sql_session)
|
||||||
|
|
||||||
|
books = sql_session.query(BookcaseItem).all()
|
||||||
|
|
||||||
|
last_run_datetime = datetime.now() - timedelta(days=16)
|
||||||
|
last_run = DeadlineDaemonLastRunDatetime(last_run_datetime)
|
||||||
|
sql_session.add(last_run)
|
||||||
|
|
||||||
|
# Create at least one item that is borrowed and not supposed to be returned yet
|
||||||
|
borrowing = BookcaseItemBorrowing(
|
||||||
|
item=books[0],
|
||||||
|
username='test_borrower_still_borrowing',
|
||||||
|
)
|
||||||
|
borrowing.start_time = last_run_datetime - timedelta(days=1)
|
||||||
|
borrowing.end_time = datetime.now() - timedelta(days=6)
|
||||||
|
sql_session.add(borrowing)
|
||||||
|
|
||||||
|
# Create at least one item that is borrowed and is supposed to be returned soon
|
||||||
|
borrowing = BookcaseItemBorrowing(
|
||||||
|
item=books[1],
|
||||||
|
username='test_borrower_return_soon',
|
||||||
|
)
|
||||||
|
borrowing.start_time = last_run_datetime - timedelta(days=1)
|
||||||
|
borrowing.end_time = datetime.now() - timedelta(days=2)
|
||||||
|
sql_session.add(borrowing)
|
||||||
|
|
||||||
|
# Create at least one item that is borrowed and is overdue
|
||||||
|
borrowing = BookcaseItemBorrowing(
|
||||||
|
item=books[2],
|
||||||
|
username='test_borrower_overdue',
|
||||||
|
)
|
||||||
|
borrowing.start_time = datetime.now() - timedelta(days=1)
|
||||||
|
borrowing.end_time = datetime.now() + timedelta(days=1)
|
||||||
|
sql_session.add(borrowing)
|
||||||
|
|
||||||
|
# Create at least one item that is in the queue and is not supposed to be borrowed yet
|
||||||
|
queue_item = BookcaseItemBorrowingQueue(
|
||||||
|
item=books[3],
|
||||||
|
username='test_queue_user_still_waiting',
|
||||||
|
)
|
||||||
|
queue_item.entered_queue_time = last_run_datetime - timedelta(days=1)
|
||||||
|
borrowing = BookcaseItemBorrowing(
|
||||||
|
item=books[3],
|
||||||
|
username='test_borrower_return_soon',
|
||||||
|
)
|
||||||
|
borrowing.start_time = last_run_datetime - timedelta(days=1)
|
||||||
|
borrowing.end_time = datetime.now() - timedelta(days=2)
|
||||||
|
sql_session.add(queue_item)
|
||||||
|
sql_session.add(borrowing)
|
||||||
|
|
||||||
|
# Create at least three items that is in the queue and two items were just returned
|
||||||
|
for i in range(3):
|
||||||
|
queue_item = BookcaseItemBorrowingQueue(
|
||||||
|
item=books[4 + i],
|
||||||
|
username=f'test_queue_user_{i}',
|
||||||
|
)
|
||||||
|
sql_session.add(queue_item)
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
borrowing = BookcaseItemBorrowing(
|
||||||
|
item=books[4 + i],
|
||||||
|
username=f'test_borrower_returned_{i}',
|
||||||
|
)
|
||||||
|
borrowing.start_time = last_run_datetime - timedelta(days=2)
|
||||||
|
borrowing.end_time = datetime.now() + timedelta(days=1)
|
||||||
|
|
||||||
|
if i != 2:
|
||||||
|
borrowing.delivered = datetime.now() - timedelta(days=1)
|
||||||
|
|
||||||
|
sql_session.add(borrowing)
|
||||||
|
|
||||||
|
# Create at least one item that has been in the queue for so long that the queue position should expire
|
||||||
|
queue_item = BookcaseItemBorrowingQueue(
|
||||||
|
item=books[7],
|
||||||
|
username='test_queue_user_expired',
|
||||||
|
)
|
||||||
|
queue_item.entered_queue_time = datetime.now() - timedelta(days=15)
|
||||||
|
|
||||||
|
# Create at least one item that has been in the queue for so long that the queue position should expire,
|
||||||
|
# but the queue person has already been notified
|
||||||
|
queue_item = BookcaseItemBorrowingQueue(
|
||||||
|
item=books[8],
|
||||||
|
username='test_queue_user_expired_notified',
|
||||||
|
)
|
||||||
|
queue_item.entered_queue_time = datetime.now() - timedelta(days=15)
|
||||||
|
|
||||||
|
sql_session.commit()
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
import csv
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from worblehat.models import (
|
||||||
|
Bookcase,
|
||||||
|
BookcaseItem,
|
||||||
|
BookcaseShelf,
|
||||||
|
MediaType,
|
||||||
|
Language,
|
||||||
|
)
|
||||||
|
from worblehat.services.config import Config
|
||||||
|
|
||||||
|
|
||||||
|
CSV_FILE = Path(__file__).parent.parent.parent / 'data' / 'arbeidsrom_smal_hylle_5.csv'
|
||||||
|
|
||||||
|
def clear_db(sql_session):
|
||||||
|
sql_session.query(BookcaseItem).delete()
|
||||||
|
sql_session.query(BookcaseShelf).delete()
|
||||||
|
sql_session.query(Bookcase).delete()
|
||||||
|
sql_session.query(MediaType).delete()
|
||||||
|
sql_session.query(Language).delete()
|
||||||
|
sql_session.commit()
|
||||||
|
|
||||||
|
def main(sql_session):
|
||||||
|
clear_db(sql_session)
|
||||||
|
|
||||||
|
media_type = MediaType(
|
||||||
|
name='Book',
|
||||||
|
description='A book',
|
||||||
|
)
|
||||||
|
sql_session.add(media_type)
|
||||||
|
|
||||||
|
language = Language(
|
||||||
|
name='Norwegian',
|
||||||
|
iso639_1_code='no',
|
||||||
|
)
|
||||||
|
sql_session.add(language)
|
||||||
|
|
||||||
|
seed_case = Bookcase(
|
||||||
|
name='seed_case',
|
||||||
|
description='test bookcase with test data',
|
||||||
|
)
|
||||||
|
sql_session.add(seed_case)
|
||||||
|
|
||||||
|
seed_shelf_1 = BookcaseShelf(
|
||||||
|
row=1,
|
||||||
|
column=1,
|
||||||
|
bookcase=seed_case,
|
||||||
|
description='test shelf with test data 1',
|
||||||
|
)
|
||||||
|
seed_shelf_2 = BookcaseShelf(
|
||||||
|
row=2,
|
||||||
|
column=1,
|
||||||
|
bookcase=seed_case,
|
||||||
|
description='test shelf with test data 2',
|
||||||
|
)
|
||||||
|
sql_session.add(seed_shelf_1)
|
||||||
|
sql_session.add(seed_shelf_2)
|
||||||
|
|
||||||
|
bookcase_items = []
|
||||||
|
with open(CSV_FILE) as csv_file:
|
||||||
|
csv_reader = csv.reader(csv_file, delimiter=',')
|
||||||
|
|
||||||
|
next(csv_reader)
|
||||||
|
for row in csv_reader:
|
||||||
|
item = BookcaseItem(
|
||||||
|
isbn=row[0],
|
||||||
|
name=row[1],
|
||||||
|
)
|
||||||
|
item.media_type = media_type
|
||||||
|
item.language = language
|
||||||
|
bookcase_items.append(item)
|
||||||
|
|
||||||
|
half = len(bookcase_items) // 2
|
||||||
|
first_half = bookcase_items[:half]
|
||||||
|
second_half = bookcase_items[half:]
|
||||||
|
|
||||||
|
for item in first_half:
|
||||||
|
item.bookcase_shelf = seed_shelf_1
|
||||||
|
|
||||||
|
for item in second_half:
|
||||||
|
item.bookcase_shelf = seed_shelf_2
|
||||||
|
|
||||||
|
sql_session.add_all(bookcase_items)
|
||||||
|
sql_session.commit()
|
|
@ -60,6 +60,19 @@ def main():
|
||||||
WorblehatCli.run_with_safe_exit_wrapper(sql_session)
|
WorblehatCli.run_with_safe_exit_wrapper(sql_session)
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
|
if args.command == 'devscripts':
|
||||||
|
sql_session = _connect_to_database(echo=Config['logging.debug_sql'])
|
||||||
|
if args.script == 'seed-content-for-deadline-daemon':
|
||||||
|
from .devscripts.seed_content_for_deadline_daemon import main
|
||||||
|
main(sql_session)
|
||||||
|
elif args.script == 'seed-test-data':
|
||||||
|
from .devscripts.seed_test_data import main
|
||||||
|
main(sql_session)
|
||||||
|
else:
|
||||||
|
print(f'Error: no such script: {args.script}')
|
||||||
|
exit(1)
|
||||||
|
exit(0)
|
||||||
|
|
||||||
if args.command == 'flask-dev':
|
if args.command == 'flask-dev':
|
||||||
flask_dev_main()
|
flask_dev_main()
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
|
@ -2,10 +2,11 @@ from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
|
ForeignKey,
|
||||||
Integer,
|
Integer,
|
||||||
SmallInteger,
|
SmallInteger,
|
||||||
String,
|
String,
|
||||||
ForeignKey,
|
Text,
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import (
|
from sqlalchemy.orm import (
|
||||||
Mapped,
|
Mapped,
|
||||||
|
@ -31,8 +32,11 @@ if TYPE_CHECKING:
|
||||||
from .Language import Language
|
from .Language import Language
|
||||||
from .MediaType import MediaType
|
from .MediaType import MediaType
|
||||||
|
|
||||||
class BookcaseItem(Base, UidMixin, UniqueNameMixin):
|
from worblehat.flaskapp.database import db
|
||||||
|
|
||||||
|
class BookcaseItem(Base, UidMixin):
|
||||||
isbn: Mapped[int] = mapped_column(String, unique=True, index=True)
|
isbn: Mapped[int] = mapped_column(String, unique=True, index=True)
|
||||||
|
name: Mapped[str] = mapped_column(Text, index=True)
|
||||||
owner: Mapped[str] = mapped_column(String, default='PVV')
|
owner: Mapped[str] = mapped_column(String, default='PVV')
|
||||||
amount: Mapped[int] = mapped_column(SmallInteger, default=1)
|
amount: Mapped[int] = mapped_column(SmallInteger, default=1)
|
||||||
|
|
||||||
|
@ -63,4 +67,13 @@ class BookcaseItem(Base, UidMixin, UniqueNameMixin):
|
||||||
):
|
):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.isbn = isbn
|
self.isbn = isbn
|
||||||
self.owner = owner
|
self.owner = owner
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_isbn(cls, isbn: str, sql_session: Session = db.session) -> Self | None:
|
||||||
|
"""
|
||||||
|
NOTE:
|
||||||
|
This method defaults to using the flask_sqlalchemy session.
|
||||||
|
It will not work outside of a request context, unless another session is provided.
|
||||||
|
"""
|
||||||
|
return sql_session.query(cls).where(cls.isbn == isbn).one_or_none()
|
|
@ -21,7 +21,8 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
class BookcaseItemBorrowingQueue(Base, UidMixin):
|
class BookcaseItemBorrowingQueue(Base, UidMixin):
|
||||||
username: Mapped[str] = mapped_column(String)
|
username: Mapped[str] = mapped_column(String)
|
||||||
entered_queue_time = mapped_column(DateTime, default=datetime.now())
|
entered_queue_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now())
|
||||||
|
item_became_available_time: Mapped[datetime | None] = mapped_column(DateTime)
|
||||||
expired = mapped_column(Boolean, default=False)
|
expired = mapped_column(Boolean, default=False)
|
||||||
|
|
||||||
fk_bookcase_item_uid: Mapped[int] = mapped_column(ForeignKey('BookcaseItem.uid'), index=True)
|
fk_bookcase_item_uid: Mapped[int] = mapped_column(ForeignKey('BookcaseItem.uid'), index=True)
|
||||||
|
|
|
@ -20,4 +20,8 @@ class DeadlineDaemonLastRunDatetime(Base):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
uid: Mapped[bool] = mapped_column(Boolean, primary_key=True, default=True)
|
uid: Mapped[bool] = mapped_column(Boolean, primary_key=True, default=True)
|
||||||
time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now())
|
time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now())
|
||||||
|
|
||||||
|
def __init__(self, time: datetime | None = None):
|
||||||
|
if time is not None:
|
||||||
|
self.time = time
|
|
@ -31,6 +31,14 @@ subparsers.add_parser(
|
||||||
help = 'Start the web interface in production mode',
|
help = 'Start the web interface in production mode',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
subparsers.add_parser(
|
||||||
|
'devscripts',
|
||||||
|
help = 'Run development scripts',
|
||||||
|
).add_argument(
|
||||||
|
'script',
|
||||||
|
help = 'The development script to run',
|
||||||
|
)
|
||||||
|
|
||||||
arg_parser.add_argument(
|
arg_parser.add_argument(
|
||||||
'-V',
|
'-V',
|
||||||
'--version',
|
'--version',
|
||||||
|
|
|
@ -24,6 +24,9 @@ class Config:
|
||||||
]
|
]
|
||||||
|
|
||||||
def __class_getitem__(cls, name: str) -> Any:
|
def __class_getitem__(cls, name: str) -> Any:
|
||||||
|
if cls._config is None:
|
||||||
|
raise RuntimeError('Configuration not loaded, call Config.load_configuration() first.')
|
||||||
|
|
||||||
__config = cls._config
|
__config = cls._config
|
||||||
for attr in name.split('.'):
|
for attr in name.split('.'):
|
||||||
__config = __config.get(attr)
|
__config = __config.get(attr)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from pathlib import Path
|
|
||||||
import smtplib
|
import smtplib
|
||||||
|
|
||||||
|
from textwrap import indent
|
||||||
|
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
|
||||||
|
@ -8,16 +9,16 @@ from .config import Config
|
||||||
|
|
||||||
|
|
||||||
def send_email(to: str, subject: str, body: str):
|
def send_email(to: str, subject: str, body: str):
|
||||||
if Config['smtp.enabled']:
|
msg = MIMEMultipart()
|
||||||
msg = MIMEMultipart()
|
msg['From'] = Config['smtp.from']
|
||||||
msg['From'] = Config['smtp.from']
|
msg['To'] = to
|
||||||
msg['To'] = to
|
if Config['smtp.subject_prefix']:
|
||||||
if Config['smtp.subject_prefix']:
|
msg['Subject'] = f"{Config['smtp.subject_prefix']} {subject}"
|
||||||
msg['Subject'] = f"{Config['smtp.subject_prefix']} {subject}"
|
else:
|
||||||
else:
|
msg['Subject'] = subject
|
||||||
msg['Subject'] = subject
|
msg.attach(MIMEText(body, 'plain'))
|
||||||
msg.attach(MIMEText(body, 'plain'))
|
|
||||||
|
|
||||||
|
if Config['smtp.enabled'] and not Config['deadline_daemon.dryrun']:
|
||||||
try:
|
try:
|
||||||
with smtplib.SMTP(Config['smtp.host'], Config['smtp.port']) as server:
|
with smtplib.SMTP(Config['smtp.host'], Config['smtp.port']) as server:
|
||||||
server.starttls()
|
server.starttls()
|
||||||
|
@ -31,6 +32,4 @@ def send_email(to: str, subject: str, body: str):
|
||||||
print(err)
|
print(err)
|
||||||
else:
|
else:
|
||||||
print('Debug: Email sending is disabled, so the following email was not sent:')
|
print('Debug: Email sending is disabled, so the following email was not sent:')
|
||||||
print(f' To: {to}')
|
print(indent(msg.as_string(), ' '))
|
||||||
print(f' Subject: {subject}')
|
|
||||||
print(f' Body: {body}')
|
|
Loading…
Reference in New Issue