Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/integer duration #238

Merged
merged 4 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion babex-vue/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,6 @@ class GenericApiPart<T> extends ApiPart {

interface AppointmentCreate {
start: Date,
end: Date,
experiment: number,
leader: number,
participant: number,
Expand Down
6 changes: 4 additions & 2 deletions babex-vue/src/components/agenda/AgendaCalendar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
// optional experiment id for limiting feeds
experiment?: number,

scheduling?: boolean,
duration?: number,
}>();

// from https://stackoverflow.com/a/64090995
Expand Down Expand Up @@ -130,7 +130,9 @@
allDaySlot: false,
slotMinTime: "07:00:00",
slotMaxTime: "20:00:00",
slotDuration: props.scheduling ? "00:15:00" : "00:30:00",
slotDuration: {minutes: props.duration ?? 30},
defaultTimedEventDuration: {minutes: props.duration},
forceEventDuration: props.duration ? true : false,
eventTimeFormat: {
hour: '2-digit',
minute: '2-digit',
Expand Down
10 changes: 6 additions & 4 deletions babex-vue/src/components/agenda/AppointmentForm.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import {defineEmits, defineProps, ref} from 'vue';
import {computed, defineEmits, defineProps, ref} from 'vue';
import {babexApi} from '../../api';
import {Location} from '../../types';
import { _ } from '@/util';
Expand All @@ -18,7 +18,6 @@

const form = ref({
start: props.event.start,
end: props.event.end,
comment: props.event ? props.event.extendedProps.comment : null,
location: props.event.extendedProps.location,
leader: props.event.extendedProps.leader,
Expand Down Expand Up @@ -64,6 +63,9 @@
function isCanceled() {
return props.event.extendedProps.outcome == 'CANCELED';
}

const eventEnd = computed(
() => new Date(form.value!.start.getTime() + 60 * 1000 * props.event.extendedProps.experiment.session_duration));
</script>

<template>
Expand Down Expand Up @@ -101,7 +103,7 @@
<DateTimePicker class="appointment-start" v-model="form.start" :readonly="hasOutcome()" />

<div>{{ _('To:') }}</div>
<DateTimePicker class="appointment-end" v-model="form.end" :readonly="hasOutcome()" />
<DateTimePicker class="appointment-end" v-model="eventEnd" :readonly="true" />

<div>
<label>{{ _('Comments:') }}</label>
Expand All @@ -121,7 +123,7 @@
</div>
</div>

<div class="mt-4"><button class="btn btn-primary save" :disabled="form.end <= form.start">{{ _('Save') }}</button></div>
<div class="mt-4"><button class="btn btn-primary save">{{ _('Save') }}</button></div>
</form>

<div v-if="!isPast() && !isCanceled()">
Expand Down
20 changes: 9 additions & 11 deletions babex-vue/src/components/invite/CallHome.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { _ } from '@/util';
import {defineProps, ref} from 'vue';
import {defineProps, ref, computed} from 'vue';
import AgendaCalendar from '../agenda/AgendaCalendar.vue';
import {babexApi} from '../../api';
import {Call} from '../../types';
Expand All @@ -10,7 +10,7 @@

const props = defineProps<{
participant: {id: number, name: string},
experiment: {id: number, name: string},
experiment: {id: number, name: string, session_duration: number},
leaders: {id: number, name: string}[],
statuses: { [id: string]: string},
call: Call,
Expand All @@ -32,7 +32,7 @@
// event start and end times saved as separate refs because
// our DateTimePicker doesn't play nicely with fullcalendar's event object
const eventStart = ref<Date|null>(null);
const eventEnd = ref<Date|null>(null);
const eventEnd = computed(() => new Date(eventStart.value!.getTime() + 60 * 1000 * props.experiment.session_duration));

const callStatus = ref<string|null>(null);
const comment = ref('');
Expand Down Expand Up @@ -72,13 +72,12 @@
}

function confirm() {
if(!event.value || !eventStart.value || !eventEnd.value) {
if(!event.value || !eventStart.value) {
return;
}

babexApi.call.appointment.create({
start: eventStart.value,
end: eventEnd.value,
experiment: props.experiment.id,
participant: props.participant.id,
leader: confirmationForm.value.leader,
Expand Down Expand Up @@ -112,11 +111,9 @@
}
event.value = calendar.value?.calendar.getApi().addEvent({
start: selectionInfo.start,
end: selectionInfo.end,
startEditable: true
});
eventStart.value = selectionInfo.start;
eventEnd.value = selectionInfo.end;
eventStart.value = event.value!.start;
}
else {
step.value = 1;
Expand Down Expand Up @@ -170,6 +167,7 @@
function complete() {
location.href = props.completeUrl;
}

</script>

<template>
Expand Down Expand Up @@ -204,15 +202,15 @@
<div class="modal-dialog modal-dialog-centered modal-lg">
<div v-if="step < 2" class="modal-content">
<div class="modal-body">
<AgendaCalendar ref="calendar" @select="onSelect" :start="start" :end="end" :experiment="experiment.id" :scheduling="true"></AgendaCalendar>
<AgendaCalendar ref="calendar" @select="onSelect" :start="start" :end="end" :experiment="experiment.id" :duration="experiment.session_duration"></AgendaCalendar>
</div>
<div class="modal-footer">
<button @click="reset()" type="button" class="btn btn-secondary" :disabled="step < 1">{{ _('Back') }}</button>
<button @click="step = 2" type="button" class="btn btn-primary" :disabled="event==null">{{ _('Next') }}</button>
<button @click="modalVisible = false" type="button" class="btn btn-secondary">{{ _('Cancel') }}</button>
</div>
</div>
<div v-if="step === 2 && eventStart && eventEnd" class="modal-content">
<div v-if="step === 2 && eventStart" class="modal-content">
<div class="modal-body">
<h2>{{ _('Appointment details') }}</h2>
<table class="table mt-3">
Expand All @@ -225,7 +223,7 @@
</tr>
<tr>
<th>{{ _('To') }}</th>
<td><DateTimePicker v-model="eventEnd" /></td>
<td><DateTimePicker :readonly="true" v-model="eventEnd" /></td>
</tr>
</table>

Expand Down
17 changes: 8 additions & 9 deletions integration_tests/test_invite.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,17 @@ def test_cancel_appointment_from_email(apps, participant, mailbox, link_from_mai
DefaultCriteria = apps.lab.get_model('experiments', 'DefaultCriteria')
User = apps.lab.get_model('main', 'User')
Appointment = apps.lab.get_model('experiments', 'Appointment')
experiment = Experiment.objects.create(defaultcriteria=DefaultCriteria.objects.create())
experiment = Experiment.objects.create(duration=10, session_duration=20)

# somewhat abusing the get_model() calls above to setup django for the following to work
from experiments.models import make_appointment
from utils.appointment_mail import send_appointment_mail, prepare_appointment_mail

leader = User.objects.first() # admin
start = timezone.now()
end = start + timedelta(hours=1)
experiment.leaders.add(leader)
experiment.save()
appointment = make_appointment(experiment, participant, leader, start, end)
appointment = make_appointment(experiment, participant, leader, start)

send_appointment_mail(appointment, prepare_appointment_mail(appointment))

Expand All @@ -71,18 +70,18 @@ def test_appointment_in_parent_overview(apps, participant, mailbox, page, login_
Experiment = apps.lab.get_model('experiments', 'Experiment')
DefaultCriteria = apps.lab.get_model('experiments', 'DefaultCriteria')
User = apps.lab.get_model('main', 'User')
experiment = Experiment.objects.create(defaultcriteria=DefaultCriteria.objects.create(),
experiment = Experiment.objects.create(duration=10,
session_duration=20,
name='Test Experiment')

# somewhat abusing the get_model() calls above to setup django for the following to work
from experiments.models import make_appointment

leader = User.objects.first() # admin
start = timezone.now()
end = start + timedelta(hours=1)
experiment.leaders.add(leader)
experiment.save()
appointment = make_appointment(experiment, participant, leader, start, end)
appointment = make_appointment(experiment, participant, leader, start)

login_as(participant.email)
try:
Expand All @@ -99,18 +98,18 @@ def test_past_appointment_not_in_parent_overview(apps, participant, mailbox, pag
Experiment = apps.lab.get_model('experiments', 'Experiment')
DefaultCriteria = apps.lab.get_model('experiments', 'DefaultCriteria')
User = apps.lab.get_model('main', 'User')
experiment = Experiment.objects.create(defaultcriteria=DefaultCriteria.objects.create(),
experiment = Experiment.objects.create(duration=10,
session_duration=20,
name='Test Experiment')

# somewhat abusing the get_model() calls above to setup django for the following to work
from experiments.models import make_appointment

leader = User.objects.first() # admin
start = timezone.now() - timedelta(days=30)
end = start + timedelta(hours=1)
experiment.leaders.add(leader)
experiment.save()
appointment = make_appointment(experiment, participant, leader, start, end)
appointment = make_appointment(experiment, participant, leader, start)

login_as(participant.email)
try:
Expand Down
2 changes: 1 addition & 1 deletion lab/agenda/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def perform_update(self, serializer):
updated_timeslot = updated.timeslot

# check if we should inform the participant about changed time
if original_timeslot.start != updated_timeslot.start or original_timeslot.end != updated_timeslot.end:
if original_timeslot.start != updated_timeslot.start:
send_appointment_mail(updated, prepare_appointment_mail(updated))


Expand Down
2 changes: 1 addition & 1 deletion lab/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

@pytest.fixture
def sample_experiment(admin_user, db):
yield admin_user.experiments.create(defaultcriteria=DefaultCriteria.objects.create())
yield admin_user.experiments.create(duration=15, session_duration=30)


@pytest.fixture
Expand Down
4 changes: 2 additions & 2 deletions lab/experiments/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ def __init__(self, *args, **kwargs):
</p>
<p>
<strong>Het experiment</strong><br/>
Het experiment duurt maximaal {{experiment_duration}}.
Omdat we ook de procedure uitleggen en er achteraf tijd is voor vragen, zult u ongeveer {{session_duration}} kwijt zijn
Het experiment duurt maximaal {{experiment_duration}} minuten.
Omdat we ook de procedure uitleggen en er achteraf tijd is voor vragen, zult u ongeveer {{session_duration}} minuten kwijt zijn
aan uw bezoek aan het Babylab. In de bijlage van deze mail vindt u meer informatie over het experiment en onze werkwijze.
</p>
<p>
Expand Down
2 changes: 0 additions & 2 deletions lab/experiments/forms/experiment_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ class Meta:
exclude = ("defaultcriteria",)
widgets = {
"name": forms.TextInput,
"duration": forms.TextInput,
"session_duration": forms.TextInput,
"task_description": forms.Textarea(
{
"rows": 7,
Expand Down
5 changes: 4 additions & 1 deletion lab/experiments/locale/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -917,4 +917,7 @@ msgid "default_criteria:attribute:min_age"
msgstr "Min. age"

msgid "default_criteria:attribute:max_age"
msgstr "Max. age"
msgstr "Max. age"

msgid "experiments:duration:minutes"
msgstr "minutes"
5 changes: 4 additions & 1 deletion lab/experiments/locale/nl/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -949,4 +949,7 @@ msgstr "Geëxcludeerd"

#: experiments/models/appointment_models.py:38
msgid "experiments:appointment:outcome:canceled"
msgstr "Geannuleerd"
msgstr "Geannuleerd"

msgid "experiments:duration:minutes"
msgstr "minuten"
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 4.2.11 on 2024-11-14 09:09

from django.db import migrations, models
import experiments.email


class Migration(migrations.Migration):

dependencies = [
("experiments", "0022_alter_experiment_confirmation_email"),
]

operations = [
migrations.AlterField(
model_name="experiment",
name="confirmation_email",
field=models.TextField(
default='<p>Beste {{parent_name}},</p>\n <p>\n U heeft een afspraak gemaakt om mee te doen met het experiment:\n <strong>{{experiment_name}}</strong><br/><br/>\n We verwachten u en {{participant_name}} op:<br/><br/>\n Datum: <strong>{{date}}</strong><br/>\n Tijd: <strong>{{time}} uur</strong><br/>\n Locatie: <strong>Janskerkhof 13a</strong> (let op: dit is de groene voordeur met de helling ervoor).<br/>\n </p>\n <p>\n <strong>Gezondheidsklachten</strong><br/>\n Wij vragen u de afspraak te verzetten wanneer uw kind vlak voor de afspraak gehoorproblemen\n en/of oorontsteking heeft en dus mogelijk minder goed hoort.\n </p>\n <p>\n <strong>Aankomst in het Babylab</strong><br/>\n Als u aanbelt bij Janskerkhof 13a en via de intercom zegt dat u voor het Babylab komt, dan wordt de deur op afstand voor u geopend.\n Wanneer u binnenkomt, kunt u gelijk na de hal met de lift (of met de trap) naar beneden.\n Daar vindt u aan uw rechterhand de wachtkamer, waar u plaats kunt nemen. De onderzoeksassistent zal u daar komen ophalen.\n </p>\n <p>\n <strong>Het experiment</strong><br/>\n Het experiment duurt maximaal {{experiment_duration}} minuten.\n Omdat we ook de procedure uitleggen en er achteraf tijd is voor vragen, zult u ongeveer {{session_duration}} minuten kwijt zijn\n aan uw bezoek aan het Babylab. In de bijlage van deze mail vindt u meer informatie over het experiment en onze werkwijze.\n </p>\n <p>\n Het is belangrijk voor ons onderzoek dat er geen broertje of zusje meekomt tijdens het bezoek aan het lab.\n Als u hierover van tevoren een andere afspraak heeft gemaakt met de assistent van het Babylab, dan geldt uiteraard die afspraak.\n </p>\n <p>\n <strong>Afspraak verzetten/afzeggen</strong><br/>\n Als u deze afspraak wilt afzeggen, kunt u dat doen via <a href="{{cancel_link}}">deze link</a>.\n Doe dat a.u.b. minstens 24 uur van tevoren. Als u vlak van tevoren ontdekt dat u verhinderd bent,\n neem dan contact op met de testleider ({{leader_name}}, email: [email protected]).\n </p>\n <p>\n Meer informatie over het Babylab, bijvoorbeeld de routebeschrijving, kunt u vinden op de\n <a href="https://babylab.wp.hum.uu.nl/">website van het Babylab</a>.\n Wij danken u alvast hartelijk voor uw medewerking. Zonder uw deelname kunnen wij geen onderzoek doen!\n </p>\n <p>\n Vriendelijke groet,<br/><br/>\n Het team van het Babylab voor Taalonderzoek\n </p>',
help_text=experiments.email.AppointmentConfirmEmail.help_text,
verbose_name="experiment:attribute:confirmation_email",
),
),
migrations.AlterField(
model_name="experiment",
name="duration",
field=models.IntegerField(verbose_name="experiment:attribute:duration"),
),
migrations.AlterField(
model_name="experiment",
name="session_duration",
field=models.IntegerField(verbose_name="experiment:attribute:session_duration"),
),
]
10 changes: 8 additions & 2 deletions lab/experiments/models/appointment_models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timedelta

from cdh.mail.classes import TemplateEmail
from django.db import models
Expand Down Expand Up @@ -69,7 +69,11 @@ def save(self, *args, **kwargs):
# verify that the leader is valid
if self.leader not in self.experiment.leaders.all():
raise ValueError("Leader {} is not part of experiment {}".format(self.leader, self.experiment))

# make sure the end time is correct
self.end = self.start + timedelta(minutes=self.experiment.session_duration)
self.timeslot.save()

super().save(*args, **kwargs)

def __str__(self):
Expand Down Expand Up @@ -108,10 +112,12 @@ def is_canceled(self):
return self.outcome == Appointment.Outcome.CANCELED


def make_appointment(experiment: Experiment, participant: Participant, leader: User, start: datetime, end: datetime):
def make_appointment(experiment: Experiment, participant: Participant, leader: User, start: datetime):
if leader not in experiment.leaders.all():
raise ValueError(f'{leader} is not a leader in the experiment "{experiment}"')

# appointment end is determined by the experiment's session duration
end = start + timedelta(minutes=experiment.session_duration)
timeslot = TimeSlot.objects.create(start=start, end=end, experiment=experiment)
appointment = Appointment.objects.create(
participant=participant, timeslot=timeslot, experiment=experiment, leader=leader
Expand Down
4 changes: 2 additions & 2 deletions lab/experiments/models/experiment_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ def _get_dt_2_hours_ago() -> datetime:
class Experiment(models.Model):
name = models.TextField(_("experiment:attribute:name"))

duration = models.TextField(_("experiment:attribute:duration"))
duration = models.IntegerField(_("experiment:attribute:duration"))

session_duration = models.TextField(_("experiment:attribute:session_duration"))
session_duration = models.IntegerField(_("experiment:attribute:session_duration"))

# how many participants are aimed for
recruitment_target = models.PositiveIntegerField(
Expand Down
7 changes: 4 additions & 3 deletions lab/experiments/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class AppointmentSerializer(serializers.ModelSerializer):
class AppointmentExperimentSerializer(serializers.ModelSerializer):
class Meta:
model = Experiment
fields = ["id", "name", "leaders"]
fields = ["id", "name", "leaders", "session_duration"]

leaders = ExperimentLeadersSerializer(read_only=True, many=True)

Expand Down Expand Up @@ -51,7 +51,7 @@ class Meta:
contact_phone = serializers.ReadOnlyField(source="leader.phonenumber")

start = serializers.DateTimeField()
end = serializers.DateTimeField()
end = serializers.ReadOnlyField()

session_duration = serializers.ReadOnlyField(source="experiment.session_duration")

Expand All @@ -70,6 +70,7 @@ class Meta:
"id",
"name",
"duration",
"session_duration",
"task_description",
"location",
"leaders",
Expand Down Expand Up @@ -104,6 +105,6 @@ class Meta:
contact_phone = serializers.ReadOnlyField(source="leader.phonenumber")

start = serializers.DateTimeField()
end = serializers.DateTimeField()
end = serializers.ReadOnlyField()

session_duration = serializers.ReadOnlyField(source="experiment.session_duration")
Loading
Loading