Implement periodic import of shifts
This commit is contained in:
parent
38a72a97be
commit
3dd3c028e1
|
@ -3,22 +3,29 @@ asgiref==3.5.0
|
||||||
async-timeout==4.0.2
|
async-timeout==4.0.2
|
||||||
billiard==3.6.4.0
|
billiard==3.6.4.0
|
||||||
celery==5.2.6
|
celery==5.2.6
|
||||||
|
certifi==2021.10.8
|
||||||
|
charset-normalizer==2.0.12
|
||||||
click==8.1.2
|
click==8.1.2
|
||||||
click-didyoumean==0.3.0
|
click-didyoumean==0.3.0
|
||||||
click-plugins==1.1.1
|
click-plugins==1.1.1
|
||||||
click-repl==0.2.0
|
click-repl==0.2.0
|
||||||
Deprecated==1.2.13
|
Deprecated==1.2.13
|
||||||
Django==4.0.4
|
Django==4.0.4
|
||||||
|
icalendar==4.0.9
|
||||||
|
idna==3.3
|
||||||
kombu==5.2.4
|
kombu==5.2.4
|
||||||
librabbitmq==2.0.0
|
librabbitmq==2.0.0
|
||||||
packaging==21.3
|
packaging==21.3
|
||||||
prompt-toolkit==3.0.29
|
prompt-toolkit==3.0.29
|
||||||
psycopg2-binary==2.9.3
|
psycopg2-binary==2.9.3
|
||||||
pyparsing==3.0.8
|
pyparsing==3.0.8
|
||||||
|
python-dateutil==2.8.2
|
||||||
pytz==2022.1
|
pytz==2022.1
|
||||||
redis==4.2.2
|
redis==4.2.2
|
||||||
|
requests==2.27.1
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
sqlparse==0.4.2
|
sqlparse==0.4.2
|
||||||
|
urllib3==1.26.9
|
||||||
vine==5.0.0
|
vine==5.0.0
|
||||||
wcwidth==0.2.5
|
wcwidth==0.2.5
|
||||||
wrapt==1.14.0
|
wrapt==1.14.0
|
||||||
|
|
|
@ -7,7 +7,7 @@ admin.site.register(Room)
|
||||||
|
|
||||||
@admin.register(Shift)
|
@admin.register(Shift)
|
||||||
class ShiftAdmin(admin.ModelAdmin):
|
class ShiftAdmin(admin.ModelAdmin):
|
||||||
list_display = ("room_name", "start_at", "free_slots")
|
list_display = ("room_name", "start_at", "free_slots", "deleted")
|
||||||
|
|
||||||
def room_name(self, object):
|
def room_name(self, object):
|
||||||
return object.room.name
|
return object.room.name
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.0.4 on 2022-04-23 00:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("app", "0005_alter_helper_phone"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="shift",
|
||||||
|
name="deleted",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
|
@ -18,7 +18,9 @@ class Shift(models.Model):
|
||||||
room = models.ForeignKey(Room, on_delete=models.RESTRICT)
|
room = models.ForeignKey(Room, on_delete=models.RESTRICT)
|
||||||
start_at = models.DateTimeField()
|
start_at = models.DateTimeField()
|
||||||
duration = models.DurationField()
|
duration = models.DurationField()
|
||||||
|
deleted = models.BooleanField(default=False)
|
||||||
# todo: add helper amount override field
|
# todo: add helper amount override field
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.room.name}: {self.start_at}"
|
return f"{self.room.name}: {self.start_at}"
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,11 @@ def index(request):
|
||||||
|
|
||||||
free_shifts = (
|
free_shifts = (
|
||||||
Shift.objects.annotate(reg_count=Count("shiftregistration"))
|
Shift.objects.annotate(reg_count=Count("shiftregistration"))
|
||||||
.filter(start_at__gt=timezone.now(), room__required_helpers__gt=F("reg_count"))
|
.filter(
|
||||||
|
start_at__gt=timezone.now(),
|
||||||
|
room__required_helpers__gt=F("reg_count"),
|
||||||
|
deleted=False,
|
||||||
|
)
|
||||||
.order_by("start_at")
|
.order_by("start_at")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -39,7 +43,9 @@ def index(request):
|
||||||
free_shifts = (
|
free_shifts = (
|
||||||
Shift.objects.annotate(reg_count=Count("shiftregistration"))
|
Shift.objects.annotate(reg_count=Count("shiftregistration"))
|
||||||
.filter(
|
.filter(
|
||||||
start_at__gt=timezone.now(), room__required_helpers__gt=F("reg_count")
|
start_at__gt=timezone.now(),
|
||||||
|
room__required_helpers__gt=F("reg_count"),
|
||||||
|
deleted=False,
|
||||||
)
|
)
|
||||||
.filter(~Q(shiftregistration__helper=request.helper))
|
.filter(~Q(shiftregistration__helper=request.helper))
|
||||||
.order_by("start_at")
|
.order_by("start_at")
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from .models import Calendar
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Calendar)
|
||||||
|
class CalendarAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("url", "has_errors")
|
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ImporterConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "shiftregister.importer"
|
|
@ -0,0 +1,83 @@
|
||||||
|
from datetime import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import transaction
|
||||||
|
from icalendar import Calendar
|
||||||
|
from .models import Event, Room, Shift
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
def import_calendar(calendar):
|
||||||
|
try:
|
||||||
|
r = requests.get(calendar.url)
|
||||||
|
r.raise_for_status()
|
||||||
|
except:
|
||||||
|
if settings.DEBUG:
|
||||||
|
raise
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not r.headers["content-type"].startswith("text/calendar"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
cal = Calendar.from_ical(r.text)
|
||||||
|
|
||||||
|
rooms = {}
|
||||||
|
events = {}
|
||||||
|
for event in cal.walk("vevent"):
|
||||||
|
uid = event.decoded("uid").decode()
|
||||||
|
room = (
|
||||||
|
event.decoded("location", None) or event.decoded("summary")
|
||||||
|
).decode()
|
||||||
|
start = event.decoded("dtstart").astimezone(timezone.utc)
|
||||||
|
end = event.decoded("dtend").astimezone(timezone.utc)
|
||||||
|
|
||||||
|
if not uid or not room:
|
||||||
|
return False
|
||||||
|
|
||||||
|
rooms[room] = None
|
||||||
|
events[uid] = (
|
||||||
|
room,
|
||||||
|
{
|
||||||
|
"start_at": start,
|
||||||
|
"duration": end - start,
|
||||||
|
"uuid": uid,
|
||||||
|
"calendar": calendar,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
for r in Room.objects.filter(name__in=rooms):
|
||||||
|
rooms[r.name] = r
|
||||||
|
for room, r in rooms.items():
|
||||||
|
if r == None:
|
||||||
|
rooms[room] = Room(
|
||||||
|
name=room, required_helpers=0
|
||||||
|
) # required_helpers=0 ensures a shift in a new room is not displayed until the correct number of required helpers is set
|
||||||
|
rooms[room].save()
|
||||||
|
|
||||||
|
for e in Event.objects.filter(calendar=calendar, uuid__in=events):
|
||||||
|
uuid = str(e.uuid)
|
||||||
|
room, event = events[uuid]
|
||||||
|
|
||||||
|
e.room = rooms[room]
|
||||||
|
e.start_at = event["start_at"]
|
||||||
|
e.duration = event["duration"]
|
||||||
|
e.save()
|
||||||
|
|
||||||
|
events[uuid] = (room, e)
|
||||||
|
for event in events:
|
||||||
|
room, e = events[event]
|
||||||
|
if not isinstance(e, Event):
|
||||||
|
Event(room=rooms[room], **e).save()
|
||||||
|
|
||||||
|
for event in Event.objects.filter(calendar=calendar).exclude(
|
||||||
|
uuid__in=events
|
||||||
|
):
|
||||||
|
event.deleted = True
|
||||||
|
event.save()
|
||||||
|
except:
|
||||||
|
if settings.DEBUG:
|
||||||
|
raise
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Generated by Django 4.0.4 on 2022-04-23 00:42
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("app", "0006_shift_deleted"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Calendar",
|
||||||
|
fields=[
|
||||||
|
("url", models.URLField(primary_key=True, serialize=False)),
|
||||||
|
("has_errors", models.BooleanField(default=False, editable=False)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Event",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"shift_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
to="app.shift",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(editable=False, primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"calendar",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="importer.calendar",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
bases=("app.shift",),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,12 @@
|
||||||
|
from django.db import models
|
||||||
|
from shiftregister.app.models import *
|
||||||
|
|
||||||
|
|
||||||
|
class Calendar(models.Model):
|
||||||
|
url = models.URLField(primary_key=True)
|
||||||
|
has_errors = models.BooleanField(default=False, editable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Event(Shift):
|
||||||
|
uuid = models.UUIDField(primary_key=True, editable=False)
|
||||||
|
calendar = models.ForeignKey(Calendar, on_delete=models.CASCADE)
|
|
@ -0,0 +1,10 @@
|
||||||
|
from celery import shared_task
|
||||||
|
from .importer import import_calendar
|
||||||
|
from .models import Calendar
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def import_shifts():
|
||||||
|
for calendar in Calendar.objects.all():
|
||||||
|
calendar.has_errors = not import_calendar(calendar)
|
||||||
|
calendar.save()
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
|
@ -35,6 +35,7 @@ ALLOWED_HOSTS = list(filter(lambda s: s != "", getenv("ALLOWED_HOSTS", "").split
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"shiftregister.app.apps.AppConfig",
|
"shiftregister.app.apps.AppConfig",
|
||||||
|
"shiftregister.importer.apps.ImporterConfig",
|
||||||
"shiftregister.team.apps.TeamConfig",
|
"shiftregister.team.apps.TeamConfig",
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
|
@ -141,3 +142,10 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
CELERY_BROKER_URL = getenv("CELERY_BROKER_URL", "amqp://guest:guest@localhost:5672//")
|
CELERY_BROKER_URL = getenv("CELERY_BROKER_URL", "amqp://guest:guest@localhost:5672//")
|
||||||
|
|
||||||
CELERY_RESULT_BACKEND = getenv("CELERY_RESULT_BACKEND", "redis://")
|
CELERY_RESULT_BACKEND = getenv("CELERY_RESULT_BACKEND", "redis://")
|
||||||
|
|
||||||
|
CELERY_BEAT_SCHEDULE = {
|
||||||
|
"import-shifts-every-60-seconds": {
|
||||||
|
"task": "shiftregister.importer.tasks.import_shifts",
|
||||||
|
"schedule": float(getenv("SHIFT_IMPORT_INTERVAL", 60.0)), # seconds
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue