Project

General

Profile

Download (25.2 KB) Statistics
| Branch: | Tag: | Revision:

calebasse / calebasse / agenda / models.py @ cde3711b

1
# -*- coding: utf-8 -*-
2

    
3
from datetime import datetime, date, timedelta
4
from copy import copy
5

    
6
from django.utils.translation import ugettext_lazy as _
7
from django.contrib.auth.models import User
8
from django.db import models
9
from django import forms
10

    
11
from calebasse.agenda import managers
12
from calebasse.utils import weeks_since_epoch, weekday_ranks
13
from calebasse.personnes.models import Holiday
14

    
15
from ..middleware.request import get_request
16

    
17
from interval import Interval
18

    
19
__all__ = (
20
    'EventType',
21
    'Event',
22
    'EventWithAct',
23
)
24

    
25
class EventType(models.Model):
26
    '''
27
    Simple ``Event`` classifcation.
28
    '''
29
    class Meta:
30
        verbose_name = u'Type d\'événement'
31
        verbose_name_plural = u'Types d\'événement'
32

    
33
    def __unicode__(self):
34
        return self.label
35

    
36
    label = models.CharField(_('label'), max_length=50)
37
    rank = models.IntegerField(_('Sorting Rank'), null=True, blank=True, default=0)
38

    
39
class Event(models.Model):
40
    '''
41
    Container model for general agenda events
42
    '''
43
    objects = managers.EventManager()
44

    
45
    title = models.CharField(_('Title'), max_length=60, blank=True, default="")
46
    description = models.TextField(_('Description'), max_length=100, blank=True, null=True)
47
    event_type = models.ForeignKey(EventType, verbose_name=u"Type d'événement")
48
    creator = models.ForeignKey(User, verbose_name=_(u'Créateur'), blank=True, null=True)
49
    create_date = models.DateTimeField(_(u'Date de création'), auto_now_add=True)
50

    
51
    services = models.ManyToManyField('ressources.Service',
52
            null=True, blank=True, default=None)
53
    participants = models.ManyToManyField('personnes.People',
54
            null=True, blank=True, default=None)
55
    ressource = models.ForeignKey('ressources.Ressource', blank=True, null=True,
56
                                  verbose_name=u'Ressource')
57

    
58
    start_datetime = models.DateTimeField(_(u'Début'), db_index=True)
59
    end_datetime = models.DateTimeField(_(u'Fin'), blank=True, null=True)
60
    old_ev_id = models.CharField(max_length=8, blank=True, null=True)
61
    old_rr_id = models.CharField(max_length=8, blank=True, null=True)
62
    # only used when there is no rr id
63
    old_rs_id = models.CharField(max_length=8, blank=True, null=True)
64
    # exception to is mutually exclusive with recurrence_periodicity
65
    # an exception cannot be periodic
66
    exception_to = models.ForeignKey('self', related_name='exceptions',
67
            blank=True, null=True,
68
            verbose_name=u'Exception à')
69
    exception_date = models.DateField(blank=True, null=True,
70
            verbose_name=u'Reporté du', db_index=True)
71
    # canceled can only be used with exception to
72
    canceled = models.BooleanField(_(u'Annulé'), db_index=True, default=False)
73

    
74
    PERIODS = (
75
            (1, u'Toutes les semaines'),
76
            (2, u'Une semaine sur deux'),
77
            (3, u'Une semaine sur trois'),
78
            (4, u'Une semaine sur quatre'),
79
            (5, u'Une semaine sur cinq')
80
    )
81
    OFFSET = range(0, 4)
82
    PERIODICITIES = (
83
            (1, u'Toutes les semaines'),
84
            (2, u'Une semaine sur deux'),
85
            (3, u'Une semaine sur trois'),
86
            (4, u'Une semaine sur quatre'),
87
            (5, u'Une semaine sur cinq'),
88
            (6, u'La première semaine du mois'),
89
            (7, u'La deuxième semaine du mois'),
90
            (8, u'La troisième semaine du mois'),
91
            (9, u'La quatrième semaine du mois'),
92
            (13, u'La cinquième semaine du mois'),
93
            (10, u'La dernière semaine du mois'),
94
            (11, u'Les semaines paires'),
95
            (12, u'Les semaines impaires')
96
    )
97
    WEEK_RANKS = (
98
            (0, u'La première semaine du mois'),
99
            (1, u'La deuxième semaine du mois'),
100
            (2, u'La troisième semaine du mois'),
101
            (3, u'La quatrième semaine du mois'),
102
            (4, u'La cinquième semaine du mois'),
103
            (-1, u'La dernière semaine du mois'),
104
#            (-2, u'L\'avant dernière semaine du mois'),
105
#            (-3, u'L\'antépénultième semaine du mois'),
106
#            (-4, u'L\'anté-antépénultième semaine du mois'),
107
#            (-5, u'L\'anté-anté-antépénultième semaine du mois')
108

    
109
    )
110
    PARITIES = (
111
            (0, u'Les semaines paires'),
112
            (1, u'Les semaines impaires')
113
    )
114
    recurrence_periodicity = models.PositiveIntegerField(
115
            choices=PERIODICITIES,
116
            verbose_name=u"Périodicité",
117
            default=None,
118
            blank=True,
119
            null=True,
120
            db_index=True)
121
    recurrence_week_day = models.PositiveIntegerField(default=0, db_index=True)
122
    recurrence_week_offset = models.PositiveIntegerField(
123
            choices=zip(OFFSET, OFFSET),
124
            verbose_name=u"Décalage en semaines par rapport au 1/1/1970 pour le calcul de période",
125
            default=0,
126
            db_index=True)
127
    recurrence_week_period = models.PositiveIntegerField(
128
            choices=PERIODS,
129
            verbose_name=u"Période en semaines",
130
            default=None,
131
            blank=True,
132
            null=True,
133
            db_index=True)
134
    recurrence_week_rank = models.IntegerField(
135
            verbose_name=u"Rang de la semaine dans le mois",
136
            choices=WEEK_RANKS,
137
            blank=True, null=True, db_index=True)
138
    recurrence_week_parity = models.PositiveIntegerField(
139
            choices=PARITIES,
140
            verbose_name=u"Parité des semaines",
141
            blank=True,
142
            null=True,
143
            db_index=True)
144
    recurrence_end_date = models.DateField(
145
            verbose_name=_(u'Fin de la récurrence'),
146
            blank=True, null=True,
147
            db_index=True)
148

    
149
    PERIOD_LIST_TO_FIELDS = [(1, None, None),
150
        (2, None, None),
151
        (3, None, None),
152
        (4, None, None),
153
        (5, None, None),
154
        (None, 0, None),
155
        (None, 1, None),
156
        (None, 2, None),
157
        (None, 3, None),
158
        (None, -1, None),
159
        (None, None, 0),
160
        (None, None, 1),
161
        (None, 4, None),
162
    ]
163

    
164
    class Meta:
165
        verbose_name = u'Evénement'
166
        verbose_name_plural = u'Evénements'
167
        ordering = ('start_datetime', 'end_datetime', 'title')
168
        unique_together = (('exception_to', 'exception_date'),)
169

    
170
    def __init__(self, *args, **kwargs):
171
        if kwargs.get('start_datetime') and not kwargs.has_key('recurrence_end_date'):
172
            kwargs['recurrence_end_date'] = kwargs.get('start_datetime').date()
173
        super(Event, self).__init__(*args, **kwargs)
174

    
175
    def clean(self):
176
        '''Initialize recurrence fields if they are not.'''
177
        self.sanitize()
178
        if self.recurrence_periodicity:
179
            if self.recurrence_end_date and self.start_datetime and self.recurrence_end_date < self.start_datetime.date():
180
                raise forms.ValidationError(u'La date de fin de périodicité doit être postérieure à la date de début.')
181
        if self.recurrence_week_parity is not None:
182
            if self.start_datetime:
183
                week = self.start_datetime.date().isocalendar()[1]
184
                start_week_parity = week % 2
185
                if start_week_parity != self.recurrence_week_parity:
186
                    raise forms.ValidationError(u'La date de départ de la périodicité est en semaine {week}.'.format(week=week))
187
        if self.recurrence_week_rank is not None and self.start_datetime:
188
            start_week_ranks = weekday_ranks(self.start_datetime.date())
189
            if self.recurrence_week_rank not in start_week_ranks:
190
                raise forms.ValidationError('La date de début de périodicité doit faire partie de la bonne semaine dans le mois.')
191

    
192
    def sanitize(self):
193
        if self.recurrence_periodicity:
194
            l = self.PERIOD_LIST_TO_FIELDS[self.recurrence_periodicity-1]
195
        else:
196
            l = None, None, None
197
        self.recurrence_week_period = l[0]
198
        self.recurrence_week_rank = l[1]
199
        self.recurrence_week_parity = l[2]
200
        if self.start_datetime:
201
            if self.recurrence_periodicity:
202
                self.recurrence_week_day = self.start_datetime.weekday()
203
            if self.recurrence_week_period is not None:
204
                self.recurrence_week_offset = weeks_since_epoch(self.start_datetime) % self.recurrence_week_period
205

    
206
    def timedelta(self):
207
        '''Distance between start and end of the event'''
208
        return self.end_datetime - self.start_datetime
209

    
210
    @property
211
    def time(self):
212
        return self.start_datetime.time()
213

    
214
    @property
215
    def date(self):
216
        return self.start_datetime.date()
217

    
218
    @property
219
    def duration(self):
220
        if self.timedelta():
221
            return self.timedelta().seconds / 60
222
        return 0
223

    
224
    def match_date(self, date):
225
        if self.is_recurring():
226
            # consider exceptions
227
            exception = self.get_exceptions_dict().get(date)
228
            if exception is not None:
229
                return exception if exception.match_date(date) else None
230
            if self.canceled:
231
                return None
232
            if date.weekday() != self.recurrence_week_day:
233
                return None
234
            if self.start_datetime.date() > date:
235
                return None
236
            if self.recurrence_end_date and self.recurrence_end_date < date:
237
                return None
238
            if self.recurrence_week_period is not None:
239
                if weeks_since_epoch(date) % self.recurrence_week_period != self.recurrence_week_offset:
240
                    return None
241
            elif self.recurrence_week_parity is not None:
242
                if date.isocalendar()[1] % 2 != self.recurrence_week_parity:
243
                    return None
244
            elif self.recurrence_week_rank is not None:
245
                if self.recurrence_week_rank not in weekday_ranks(date):
246
                    return None
247
            else:
248
                raise NotImplemented
249
            return self
250
        else:
251
            return self if date == self.start_datetime.date() else None
252

    
253

    
254
    def today_occurrence(self, today=None, match=False, upgrade=True):
255
        '''For a recurring event compute the today 'Event'.
256

    
257
           The computed event is the fake one that you cannot save to the database.
258
        '''
259
        today = today or date.today()
260
        if self.canceled:
261
            return None
262
        if match:
263
            exception = self.get_exceptions_dict().get(today)
264
            if exception:
265
                if exception.start_datetime.date() == today:
266
                    return exception.today_occurrence(today)
267
                else:
268
                    return None
269
        else:
270
            exception_or_self = self.match_date(today)
271
            if exception_or_self is None:
272
                return None
273
            if exception_or_self != self:
274
                return exception_or_self.today_occurrence(today)
275
        if self.event_type_id == 1 and type(self) != EventWithAct and upgrade:
276
           self = self.eventwithact
277
        if self.recurrence_periodicity is None:
278
            return self
279
        start_datetime = datetime.combine(today, self.start_datetime.timetz())
280
        end_datetime = start_datetime + self.timedelta()
281
        event = copy(self)
282
        event.exception_to = self
283
        event.exception_date = today
284
        event.start_datetime = start_datetime
285
        event.end_datetime = end_datetime
286
        event.recurrence_periodicity = None
287
        event.recurrence_week_offset = 0
288
        event.recurrence_week_period = None
289
        event.recurrence_week_parity = None
290
        event.recurrence_week_rank = None
291
        event.recurrence_end_date = None
292
        event.parent = self
293
        # the returned event is "virtual", it must not be saved
294
        old_save = event.save
295
        old_participants = list(self.participants.all())
296
        old_services = list(self.services.all())
297
        def save(*args, **kwargs):
298
            event.id = None
299
            event.event_ptr_id = None
300
            old_save(*args, **kwargs)
301
            if hasattr(self, 'exceptions_dict'):
302
                self.exceptions_dict[event.start_datetime.date()] = event
303
            event.services = old_services
304
            event.participants = old_participants
305
            event.save = old_save
306
        event.save = save
307
        return event
308

    
309
    def next_occurence(self, today=None):
310
        '''Returns the next occurence after today.'''
311
        today = today or date.today()
312
        for occurence in self.all_occurences():
313
            if occurence.start_datetime.date() > today:
314
                return occurence
315

    
316
    def is_recurring(self):
317
        '''Is this event multiple ?'''
318
        return self.recurrence_periodicity is not None
319

    
320
    def get_exceptions_dict(self):
321
        if not hasattr(self, 'exceptions_dict'):
322
            self.exceptions_dict = dict()
323
            if self.exception_to_id is None:
324
                for exception in self.exceptions.all():
325
                    self.exceptions_dict[exception.exception_date] = exception
326
        return self.exceptions_dict
327

    
328
    def all_occurences(self, limit=90):
329
        '''Returns all occurences of this event as virtual Event objects
330

    
331
           limit - compute occurrences until limit days in the future
332

    
333
           Default is to limit to 90 days.
334
        '''
335
        if self.recurrence_periodicity is not None:
336
            day = self.start_datetime.date()
337
            max_end_date = max(date.today(), self.start_datetime.date()) + timedelta(days=limit)
338
            end_date = min(self.recurrence_end_date or max_end_date, max_end_date)
339
            occurrences = []
340
            if self.recurrence_week_period is not None:
341
                delta = timedelta(days=self.recurrence_week_period*7)
342
                while day <= end_date:
343
                    occurrence = self.today_occurrence(day, True)
344
                    if occurrence is not None:
345
                        occurrences.append(occurrence)
346
                    day += delta
347
            elif self.recurrence_week_parity is not None:
348
                delta = timedelta(days=7)
349
                while day <= end_date:
350
                    if day.isocalendar()[1] % 2 == self.recurrence_week_parity:
351
                        occurrence = self.today_occurrence(day, True)
352
                        if occurrence is not None:
353
                            occurrences.append(occurrence)
354
                    day += delta
355
            elif self.recurrence_week_rank is not None:
356
                delta = timedelta(days=7)
357
                while day <= end_date:
358
                    if self.recurrence_week_rank in weekday_ranks(day):
359
                        occurrence = self.today_occurrence(day, True)
360
                        if occurrence is not None:
361
                            occurrences.append(occurrence)
362
                    day += delta
363
            for exception in self.exceptions.all():
364
                if not exception.canceled:
365
                    if exception.exception_date != exception.start_datetime.date() or exception.exception_date > end_date:
366
                        occurrences.append(exception.eventwithact if exception.event_type_id == 1 else exception)
367
            return sorted(occurrences, key=lambda o: o.start_datetime)
368
        else:
369
            return [self]
370

    
371
    def save(self, *args, **kwargs):
372
        assert self.recurrence_periodicity is None or self.exception_to is None
373
        assert self.exception_to is None or self.exception_to.recurrence_periodicity is not None
374
        assert self.start_datetime is not None
375
        self.sanitize() # init periodicity fields
376
        super(Event, self).save(*args, **kwargs)
377
        self.events_cleaning()
378
        self.acts_cleaning()
379

    
380
    def delete(self, *args, **kwargs):
381
        if not self.one_act_already_billed():
382
            for exception in self.exceptions.all():
383
                exception.delete()
384
            self.canceled = True
385
            if hasattr(self, 'eventwithact'):
386
                # Needed
387
                self.eventwithact.canceled = True
388
            # save will clean acts
389
            self.save(*args, **kwargs)
390

    
391
    @property
392
    def acts(self):
393
        from ..actes.models import Act
394
        return Act.objects.filter(
395
                models.Q(parent_event=self) | \
396
                models.Q(parent_event__exception_to=self))
397

    
398
    def one_act_not_new(self):
399
        '''
400
            Return True if at least one act of the present event or an act
401
            of one of its exceptions is not new.
402
        '''
403
        return True if [a for a in self.acts if not a.is_new()] else False
404

    
405
    def last_act_not_new(self):
406
        '''
407
            Return True if at least one act of the present event or an act
408
            of one of its exceptions is not new.
409
        '''
410
        act = None
411
        for a in self.acts:
412
            if not a.is_new():
413
                if not act or a.date > act.date:
414
                    act = a
415
        return act
416

    
417
    def one_act_already_billed(self):
418
        '''
419
            Return True if at least one act of the present event or an act
420
            of one of its exceptions is already billed.
421
        '''
422
        return self.acts.filter(already_billed=True).exists()
423

    
424
    def one_act_is_billed(self):
425
        '''
426
            Return True if at least one act of the present event or an act
427
            of one of its exceptions is billed.
428
        '''
429
        return self.acts.filter(is_billed=True).exists()
430

    
431
    def events_cleaning(self):
432
        """
433
            Delete exceptions out of bounds if not with an associated act
434
            already billed.
435
        """
436
        events = None
437
        if self.recurrence_end_date:
438
            events = Event.objects.filter(
439
                models.Q(exception_to=self,
440
                    exception_date__lt=self.start_datetime.date()) | \
441
                models.Q(exception_to=self,
442
                    exception_date__gt=self.recurrence_end_date))
443
        else:
444
            events = Event.objects.filter(exception_to=self,
445
                    exception_date__lt=self.start_datetime.date())
446
        for event in events:
447
            if hasattr(event, 'eventwithact'):
448
                if not event.eventwithact.act.already_billed:
449
                    event.delete()
450
            else:
451
                event.delete()
452

    
453
    def acts_cleaning(self):
454
        # list of occurences may have changed
455
        from ..actes.models import Act
456
        if self.exception_to:
457
            # maybe a new exception, so look for parent acts with same date
458
            # as exception date
459
            acts = Act.objects.filter(models.Q(parent_event=self)
460
                    |models.Q(parent_event=self.exception_to,
461
                        date=self.exception_date))
462
        else:
463
            acts = Act.objects.filter(parent_event=self)
464
        acts = acts.prefetch_related('actvalidationstate_set')
465
        if acts:
466
            eventwithact = self.eventwithact
467
            for act in acts:
468
                occurrence = eventwithact.today_occurrence(act.date)
469
                if occurrence:
470
                    occurrence.update_act(act)
471
                else:
472
                    if not act.already_billed:
473
                        act.delete()
474

    
475
    def to_interval(self):
476
        return Interval(self.start_datetime, self.end_datetime)
477

    
478
    def is_event_absence(self):
479
        return False
480

    
481
    def get_inactive_participants(self):
482
        return self.participants.filter(worker__enabled=False)
483

    
484
    def get_missing_participants(self):
485
        missing_participants = []
486
        for participant in self.participants.all():
487
            holidays = None
488
            worker = participant.worker
489
            holidays = Holiday.objects.for_worker(worker) \
490
                  .for_timed_period(self.start_datetime.date(), self.start_datetime.time(), self.end_datetime.time())
491
            if holidays:
492
                missing_participants.append(participant)
493
        return missing_participants
494

    
495
    RECURRENCE_DESCRIPTION = [
496
            u'Tous les %s',      #(1, None, None),
497
            u'Un %s sur deux',   #(2, None, None),
498
            u'Un %s sur trois',  #(3, None, None),
499
            u'Un %s sur quatre', #(4, None, None),
500
            u'Un %s sur cinq',   #(5, None, None),
501
            u'Le premier %s du mois',   #(None, 0, None),
502
            u'Le deuxième %s du mois',  #(None, 1, None),
503
            u'Le troisième %s du mois', #(None, 2, None),
504
            u'Le quatrième %s du mois', #(None, 3, None),
505
            u'Le dernier %s du mois',   #(None, 4, None),
506
            u'Les %s les semaines paires',    #(None, None, 0),
507
            u'Les %s les semaines impaires', #(None, None, 1)
508
    ]
509

    
510
    WEEKDAY_DESRIPTION = [
511
            u'lundi',
512
            u'mardi',
513
            u'mercredi',
514
            u'jeudi',
515
            u'vendredi',
516
            u'samedi',
517
            u'dimanche'
518
    ]
519

    
520
    def recurrence_description(self):
521
        '''Self description of this recurring event'''
522
        if not self.recurrence_periodicity:
523
            return None
524
        parts = []
525
        parts.append(self.RECURRENCE_DESCRIPTION[self.recurrence_periodicity-1] \
526
            % self.WEEKDAY_DESRIPTION[self.recurrence_week_day])
527
        if self.recurrence_end_date:
528
            parts.append(u'du')
529
        else:
530
            parts.append(u'à partir du')
531
        parts.append(self.start_datetime.strftime('%d/%m/%Y'))
532
        if self.recurrence_end_date:
533
            parts.append(u'au')
534
            parts.append(self.recurrence_end_date.strftime('%d/%m/%Y'))
535
        return u' '.join(parts)
536

    
537
    def is_absent(self):
538
        try:
539
            return self.eventwithact.is_absent()
540
        except self.DoesNotExist:
541
            return False
542

    
543
    def __unicode__(self):
544
        return self.title
545

    
546
    def __repr__(self):
547
        return '<Event: on {start_datetime} with {participants}'.format(
548
                start_datetime=self.start_datetime,
549
                participants=self.participants.all() if self.id else '<un-saved>')
550

    
551

    
552
class EventWithActManager(managers.EventManager):
553
    def create_patient_appointment(self, creator, title, patient,
554
            doctors=[], act_type=None, service=None, start_datetime=None, end_datetime=None,
555
            ressource=None, periodicity=1, until=False):
556
        appointment = self.create_event(creator=creator,
557
                title=title,
558
                event_type=EventType(id=1),
559
                participants=doctors,
560
                services=[service],
561
                start_datetime=start_datetime,
562
                end_datetime=end_datetime,
563
                ressource=ressource,
564
                periodicity=periodicity,
565
                until=until,
566
                act_type=act_type,
567
                patient=patient)
568
        return appointment
569

    
570

    
571
class EventWithAct(Event):
572
    '''An event corresponding to an act.'''
573
    objects = EventWithActManager()
574
    act_type = models.ForeignKey('ressources.ActType',
575
        verbose_name=u'Type d\'acte')
576
    patient = models.ForeignKey('dossiers.PatientRecord')
577
    convocation_sent = models.BooleanField(blank=True,
578
        verbose_name=u'Convoqué', db_index=True, default=False)
579

    
580

    
581
    @property
582
    def act(self):
583
        for act in self.act_set.all():
584
            if act.date == self.start_datetime.date():
585
                return act
586
        return self.build_act()
587

    
588
    def get_state(self):
589
        act = self.act
590
        if act.id:
591
            return act.get_state()
592
        return None
593

    
594
    def is_absent(self):
595
        act = self.act
596
        if act.id:
597
            return act.is_absent()
598
        return False
599

    
600
    def build_act(self):
601
        from ..actes.models import Act, ActValidationState
602
        act = Act()
603
        self.init_act(act)
604
        old_save = act.save
605
        def save(*args, **kwargs):
606
            act.save = old_save
607
            old_save(*args, **kwargs)
608
            act.doctors = (participant.worker for participant in self.participants.all())
609
            last_validation_state = ActValidationState.objects.create(
610
                    act=act, state_name='NON_VALIDE',
611
                    author=self.creator, previous_state=None)
612
            act.last_validation_state = last_validation_state
613
            old_save(*args, **kwargs)
614
        act.save = save
615
        return act
616

    
617
    def update_act(self, act):
618
        '''Update an act to match details of the meeting'''
619
        self.init_act(act)
620
        act.save()
621

    
622
    def init_act(self, act):
623
        if not act.is_billed:
624
            delta = self.timedelta()
625
            duration = delta.seconds // 60
626
            act._duration = duration
627
            act.act_type = self.act_type
628
            act.patient = self.patient
629
            act.date = self.start_datetime.date()
630
            act.time = self.start_datetime.time()
631
        act.parent_event = self
632

    
633
    def save(self, *args, **kwargs):
634
        '''Force event_type to be patient meeting.'''
635
        self.event_type = EventType(id=1)
636
        super(EventWithAct, self).save(*args, **kwargs)
637

    
638
    def is_event_absence(self):
639
        return self.act.is_absent()
640

    
641
    def __unicode__(self):
642
        kwargs = {
643
                'patient': self.patient,
644
                'start_datetime': self.start_datetime,
645
                'act_type': self.act_type
646
        }
647
        kwargs['doctors'] = ', '.join(map(unicode, self.participants.all())) if self.id else ''
648
        return u'Rdv le {start_datetime} de {patient} avec {doctors} pour ' \
649
            '{act_type} ({act_type.id})'.format(**kwargs)
650

    
651

    
652
from django.db.models.signals import m2m_changed
653
from django.dispatch import receiver
654

    
655

    
656
@receiver(m2m_changed, sender=Event.participants.through)
657
def participants_changed(sender, instance, action, **kwargs):
658
    if action.startswith('post'):
659
        workers = [ p.worker for p in instance.participants.prefetch_related('worker') ]
660
        for act in instance.act_set.all():
661
            act.doctors = workers
(7-7/10)