Implement periodic import of shifts

pull/1/head
Luca 3 years ago
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…
Cancel
Save