• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

chiefonboarding / ChiefOnboarding / 6700616467

31 Oct 2023 01:00AM UTC coverage: 92.452% (-1.2%) from 93.66%
6700616467

Pull #383

github

web-flow
Merge 300d02535 into fb838f71e
Pull Request #383: Add offboarding sequences

6161 of 6664 relevant lines covered (92.45%)

0.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.9
back/admin/sequences/models.py
1
from django.conf import settings
1✔
2
from django.db import models
1✔
3
from django.db.models import Case, F, IntegerField, Prefetch, When
1✔
4
from django.template.loader import render_to_string
1✔
5
from django.urls import reverse
1✔
6
from django.utils.translation import gettext_lazy as _
1✔
7
from twilio.rest import Client
1✔
8

9
from admin.admin_tasks.models import AdminTask
1✔
10
from admin.appointments.models import Appointment
1✔
11
from admin.badges.models import Badge
1✔
12
from admin.integrations.models import Integration
1✔
13
from admin.introductions.models import Introduction
1✔
14
from admin.preboarding.models import Preboarding
1✔
15
from admin.resources.models import Resource
1✔
16
from admin.to_do.models import ToDo
1✔
17
from misc.fields import ContentJSONField, EncryptedJSONField
1✔
18
from misc.mixins import ContentMixin
1✔
19
from organization.models import Notification
1✔
20
from slack_bot.models import SlackChannel
1✔
21
from slack_bot.utils import Slack
1✔
22

23
from .emails import send_sequence_message
1✔
24

25

26
class OnboardingSequenceManager(models.Manager):
1✔
27
    def get_queryset(self):
1✔
28
        return super().get_queryset().filter(category=Sequence.Category.ONBOARDING)
1✔
29

30

31
class OffboardingSequenceManager(models.Manager):
1✔
32
    def get_queryset(self):
1✔
33
        return super().get_queryset().filter(category=Sequence.Category.OFFBOARDING)
1✔
34

35

36
class Sequence(models.Model):
1✔
37
    class Category(models.IntegerChoices):
1✔
38
        ONBOARDING = 0, _("Onboarding sequence")
1✔
39
        OFFBOARDING = 1, _("Offboarding sequence")
1✔
40

41
    name = models.CharField(verbose_name=_("Name"), max_length=240)
1✔
42
    auto_add = models.BooleanField(default=False)
1✔
43
    category = models.IntegerField(choices=Category.choices)
1✔
44

45
    objects = models.Manager()
1✔
46
    onboarding = OnboardingSequenceManager()
1✔
47
    offboarding = OffboardingSequenceManager()
1✔
48

49
    def __str__(self):
1✔
50
        return self.name
1✔
51

52
    @property
1✔
53
    def update_url(self):
1✔
54
        return reverse("sequences:update", args=[self.id])
1✔
55

56
    @property
1✔
57
    def is_onboarding(self):
1✔
58
        return self.category == Sequence.Category.ONBOARDING
1✔
59

60
    @property
1✔
61
    def is_offboarding(self):
1✔
62
        return self.category == Sequence.Category.OFFBOARDING
×
63

64
    def class_name(self):
1✔
65
        return self.__class__.__name__
1✔
66

67
    def duplicate(self):
1✔
68
        old_sequence = Sequence.objects.get(pk=self.pk)
1✔
69
        self.pk = None
1✔
70
        self.name = _("%(name)s (duplicate)") % {"name": self.name}
1✔
71
        self.auto_add = False
1✔
72
        self.save()
1✔
73

74
        admin_tasks = {}
1✔
75
        for condition in old_sequence.conditions.all():
1✔
76
            new_condition, admin_tasks = condition.duplicate(admin_tasks=admin_tasks)
1✔
77
            self.conditions.add(new_condition)
1✔
78
        return self
1✔
79

80
    def assign_to_user(self, user):
1✔
81
        # adding conditions
82
        for sequence_condition in self.conditions.all():
1✔
83
            user_condition = None
1✔
84

85
            # Check what kind of condition it is
86
            if sequence_condition.condition_type in [
1✔
87
                Condition.Type.BEFORE,
88
                Condition.Type.AFTER,
89
            ]:
90
                # Get the timed based condition or return None if not exist
91
                user_condition = user.conditions.filter(
1✔
92
                    condition_type=sequence_condition.condition_type,
93
                    days=sequence_condition.days,
94
                    time=sequence_condition.time,
95
                ).first()
96

97
            elif sequence_condition.condition_type == Condition.Type.TODO:
1✔
98
                # For to_do items, filter all condition items to find if one matches
99
                # Both the amount and the todos itself need to match exactly
100
                conditions = user.conditions.filter(condition_type=Condition.Type.TODO)
1✔
101
                original_condition_to_do_ids = (
1✔
102
                    sequence_condition.condition_to_do.all().values_list(
103
                        "id", flat=True
104
                    )
105
                )
106

107
                for condition in conditions:
1✔
108
                    # Quickly check if the amount of items match - if not match, drop
109
                    if condition.condition_to_do.all().count() != len(
1✔
110
                        original_condition_to_do_ids
111
                    ):
112
                        continue
×
113

114
                    found_to_do_items = condition.condition_to_do.filter(
1✔
115
                        id__in=original_condition_to_do_ids
116
                    ).count()
117

118
                    if found_to_do_items == len(original_condition_to_do_ids):
1✔
119
                        # We found our match. Amount matches AND the todos match
120
                        user_condition = condition
1✔
121
                        break
1✔
122

123
            elif sequence_condition.condition_type == Condition.Type.ADMIN_TASK:
1✔
124
                # For admin to do items, filter all condition items to find if one
125
                # matches. Both the amount and the admin to do itself need to match
126
                # exactly
127
                conditions = user.conditions.filter(
1✔
128
                    condition_type=Condition.Type.ADMIN_TASK
129
                )
130
                original_condition_admin_tasks_ids = (
1✔
131
                    sequence_condition.condition_admin_tasks.all().values_list(
132
                        "id", flat=True
133
                    )
134
                )
135

136
                for condition in conditions:
1✔
137
                    # Quickly check if the amount of items match - if not match, drop
138
                    if condition.condition_admin_tasks.all().count() != len(  # noqa
1✔
139
                        original_condition_admin_tasks_ids
140
                    ):
141
                        continue
1✔
142

143
                    found_admin_tasks = condition.condition_admin_tasks.filter(
1✔
144
                        id__in=original_condition_admin_tasks_ids
145
                    ).count()
146

147
                    if found_admin_tasks == len(original_condition_admin_tasks_ids):
1✔
148
                        # We found our match. Amount matches AND the admin_tasks match
149
                        user_condition = condition
1✔
150
                        break
1✔
151
            else:
152
                # Condition (always just one) that will be assigned directly (type == 3)
153
                # Just run the condition with the new hire
154
                sequence_condition.process_condition(user)
1✔
155
                continue
1✔
156

157
            # Let's add the condition to the new hire. Either through adding it to the
158
            # exising one or creating a new one
159
            if user_condition is not None:
1✔
160
                # adding items to existing condition
161
                user_condition.include_other_condition(sequence_condition)
1✔
162
            else:
163
                # duplicating condition and adding to user
164
                # Force getting it, as we are about to duplicate it and set the pk to
165
                # None
166
                old_condition = Condition.objects.get(id=sequence_condition.id)
1✔
167

168
                sequence_condition.pk = None
1✔
169
                sequence_condition.sequence = None
1✔
170
                sequence_condition.save()
1✔
171

172
                # Add condition to_dos
173
                sequence_condition.condition_to_do.set(
1✔
174
                    old_condition.condition_to_do.all()
175
                )
176

177
                for condition_admin_task in old_condition.condition_admin_tasks.all():
1✔
178
                    sequence_condition.condition_admin_tasks.add(condition_admin_task)
1✔
179
                # Add all the things that get triggered
180
                sequence_condition.include_other_condition(old_condition)
1✔
181

182
                # Add newly created condition back to user
183
                user.conditions.add(sequence_condition)
1✔
184

185
    def remove_from_user(self, new_hire):
1✔
186
        from admin.admin_tasks.models import AdminTask
1✔
187

188
        # get all items from all conditions
189
        to_dos = ToDo.objects.none()
1✔
190
        badges = Badge.objects.none()
1✔
191
        resources = Resource.objects.none()
1✔
192
        admin_tasks = AdminTask.objects.none()
1✔
193
        external_messages = ExternalMessage.objects.none()
1✔
194
        introductions = Introduction.objects.none()
1✔
195
        preboarding = Preboarding.objects.none()
1✔
196
        appointments = Appointment.objects.none()
1✔
197
        integration_configs = IntegrationConfig.objects.none()
1✔
198

199
        # TODO: this is going to make a lot of queries, should be optimized
200
        for condition in self.conditions.all():
1✔
201
            to_dos |= condition.to_do.all()
1✔
202
            badges |= condition.badges.all()
1✔
203
            resources |= condition.resources.all()
1✔
204
            admin_tasks |= condition.admin_tasks.all()
1✔
205
            external_messages |= condition.external_messages.all()
1✔
206
            introductions |= condition.introductions.all()
1✔
207
            preboarding |= condition.preboarding.all()
1✔
208
            appointments |= condition.appointments.all()
1✔
209
            integration_configs |= condition.integration_configs.all()
1✔
210

211
        # Cycle through new hire's item and remove the ones that aren't supposed to
212
        # be there
213
        new_hire.to_do.remove(*to_dos)
1✔
214
        new_hire.badges.remove(*badges)
1✔
215
        new_hire.appointments.remove(*appointments)
1✔
216
        new_hire.preboarding.remove(*preboarding)
1✔
217
        new_hire.introductions.remove(*introductions)
1✔
218

219
        # Do the same with the conditions
220
        conditions_to_be_deleted = []
1✔
221
        items = {
1✔
222
            "to_do": to_dos,
223
            "resources": resources,
224
            "badges": badges,
225
            "admin_tasks": admin_tasks,
226
            "external_messages": external_messages,
227
            "introductions": introductions,
228
            "preboarding": preboarding,
229
            "appointments": appointments,
230
            "integration_configs": integration_configs,
231
        }
232
        for condition in new_hire.conditions.all():
1✔
233
            for field in condition._meta.many_to_many:
1✔
234
                # We only want to remove assigned items, not triggers
235
                if field.name in ("condition_to_do", "condition_admin_tasks"):
1✔
236
                    continue
1✔
237
                getattr(condition, field.name).remove(*items[field.name])
1✔
238

239
            if condition.is_empty:
1✔
240
                conditions_to_be_deleted.append(condition.id)
1✔
241

242
        # Remove all empty conditions
243
        Condition.objects.filter(id__in=conditions_to_be_deleted).delete()
1✔
244
        # Delete sequence
245
        Notification.objects.order_by("-created").filter(
1✔
246
            notification_type=Notification.Type.ADDED_SEQUENCE
247
        ).first().delete()
248

249

250
class ExternalMessageManager(models.Manager):
1✔
251
    def for_new_hire(self):
1✔
252
        return self.get_queryset().filter(
1✔
253
            person_type=ExternalMessage.PersonType.NEWHIRE
254
        )
255

256
    def for_admins(self):
1✔
257
        return self.get_queryset().exclude(
1✔
258
            person_type=ExternalMessage.PersonType.NEWHIRE
259
        )
260

261

262
class ExternalMessage(ContentMixin, models.Model):
1✔
263
    class Type(models.IntegerChoices):
1✔
264
        EMAIL = 0, _("Email")
1✔
265
        SLACK = 1, _("Slack")
1✔
266
        TEXT = 2, _("Text")
1✔
267

268
    class PersonType(models.IntegerChoices):
1✔
269
        NEWHIRE = 0, _("New hire")
1✔
270
        MANAGER = 1, _("Manager")
1✔
271
        BUDDY = 2, _("Buddy")
1✔
272
        CUSTOM = 3, _("Custom")
1✔
273
        SLACK_CHANNEL = 4, _("Slack channel")
1✔
274

275
    name = models.CharField(verbose_name=_("Name"), max_length=240)
1✔
276
    content_json = ContentJSONField(default=dict, verbose_name=_("Content"))
1✔
277
    content = models.CharField(verbose_name=_("Content"), max_length=12000, blank=True)
1✔
278
    send_via = models.IntegerField(verbose_name=_("Send via"), choices=Type.choices)
1✔
279
    send_to = models.ForeignKey(
1✔
280
        settings.AUTH_USER_MODEL,
281
        verbose_name=_("Send to"),
282
        on_delete=models.CASCADE,
283
        blank=True,
284
        null=True,
285
    )
286
    send_to_channel = models.ForeignKey(
1✔
287
        SlackChannel,
288
        verbose_name=_("Slack channel"),
289
        on_delete=models.CASCADE,
290
        blank=True,
291
        null=True,
292
    )
293
    subject = models.CharField(
1✔
294
        verbose_name=_("Subject"),
295
        max_length=78,
296
        default=_("Here is an update!"),
297
        blank=True,
298
    )
299
    person_type = models.IntegerField(
1✔
300
        verbose_name=_("For"), choices=PersonType.choices, default=1
301
    )
302

303
    @property
1✔
304
    def is_email_message(self):
1✔
305
        return self.send_via == self.Type.EMAIL
1✔
306

307
    @property
1✔
308
    def is_slack_message(self):
1✔
309
        return self.send_via == self.Type.SLACK
1✔
310

311
    @property
1✔
312
    def is_text_message(self):
1✔
313
        return self.send_via == self.Type.TEXT
1✔
314

315
    @property
1✔
316
    def notification_add_type(self):
1✔
317
        if self.is_text_message:
1✔
318
            return Notification.Type.SENT_TEXT_MESSAGE
1✔
319
        if self.is_email_message:
1✔
320
            return Notification.Type.SENT_EMAIL_MESSAGE
1✔
321
        if self.is_slack_message:
1✔
322
            return Notification.Type.SENT_SLACK_MESSAGE
1✔
323

324
    @property
1✔
325
    def get_icon_template(self):
1✔
326
        if self.is_email_message:
1✔
327
            return render_to_string("_email_icon.html")
1✔
328
        if self.is_slack_message:
1✔
329
            return render_to_string("_slack_icon.html")
1✔
330
        if self.is_text_message:
1✔
331
            return render_to_string("_text_icon.html")
1✔
332

333
    def duplicate(self, change_name=False):
1✔
334
        self.pk = None
1✔
335
        self.save()
1✔
336
        return self
1✔
337

338
    def get_user(self, new_hire):
1✔
339
        if self.person_type == ExternalMessage.PersonType.NEWHIRE:
1✔
340
            return new_hire
1✔
341
        elif self.person_type == ExternalMessage.PersonType.MANAGER:
1✔
342
            return new_hire.manager
1✔
343
        elif self.person_type == ExternalMessage.PersonType.BUDDY:
1✔
344
            return new_hire.buddy
1✔
345
        else:
346
            return self.send_to
1✔
347

348
    def execute(self, user):
1✔
349
        if self.is_email_message:
1✔
350
            # Make sure there is actually an email
351
            if self.get_user(user) is None:
1✔
352
                Notification.objects.create(
1✔
353
                    notification_type=Notification.Type.FAILED_NO_EMAIL,
354
                    extra_text=self.subject,
355
                    created_for=user,
356
                )
357
                return
1✔
358

359
            send_sequence_message(
1✔
360
                user, self.get_user(user), self.content_json["blocks"], self.subject
361
            )
362
        elif self.is_slack_message:
1✔
363
            blocks = []
1✔
364
            # We don't have the model function on this model, so let's get it from a
365
            # different model. A bit hacky, but should be okay.
366
            blocks = ToDo(content=self.content_json).to_slack_block(user)
1✔
367

368
            # Send to channel instead of person?
369
            if self.person_type == ExternalMessage.PersonType.SLACK_CHANNEL:
1✔
370
                channel = (
1✔
371
                    None
372
                    if self.send_to_channel is None
373
                    else "#" + self.send_to_channel.name
374
                )
375
            else:
376
                channel = self.get_user(user).slack_user_id
1✔
377

378
            Slack().send_message(blocks=blocks, channel=channel)
1✔
379
        else:  # text message
380
            send_to = self.get_user(user)
1✔
381
            if send_to is None or send_to.phone == "":
1✔
382
                Notification.objects.create(
1✔
383
                    notification_type=Notification.Type.FAILED_NO_PHONE,
384
                    extra_text=self.name,
385
                    created_for=user,
386
                )
387
                return
1✔
388

389
            client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)
×
390
            client.messages.create(
×
391
                to=send_to.phone,
392
                from_=settings.TWILIO_FROM_NUMBER,
393
                body=self.get_user(user).personalize(self.content),
394
            )
395

396
        if not self.is_slack_message:
1✔
397
            # The Slack send_message function already registered this notification so
398
            # skip it in that case.
399
            Notification.objects.create(
1✔
400
                notification_type=self.notification_add_type,
401
                extra_text=self.name,
402
                created_for=user,
403
            )
404

405
    objects = ExternalMessageManager()
1✔
406

407

408
class PendingEmailMessage(ExternalMessage):
1✔
409
    # Email message model proxied from ExternalMessage
410

411
    def save(self, *args, **kwargs):
1✔
412
        self.send_via = self.Type.EMAIL
1✔
413
        return super(PendingEmailMessage, self).save(*args, **kwargs)
1✔
414

415
    class Meta:
1✔
416
        proxy = True
1✔
417

418

419
class PendingSlackMessage(ExternalMessage):
1✔
420
    # Slack message model proxied from ExternalMessage
421

422
    def save(self, *args, **kwargs):
1✔
423
        self.send_via = self.Type.SLACK
1✔
424
        return super(PendingSlackMessage, self).save(*args, **kwargs)
1✔
425

426
    class Meta:
1✔
427
        proxy = True
1✔
428

429

430
class PendingTextMessage(ExternalMessage):
1✔
431
    # Text message model proxied from ExternalMessage
432

433
    def save(self, *args, **kwargs):
1✔
434
        self.send_via = self.Type.TEXT
1✔
435
        return super(PendingTextMessage, self).save(*args, **kwargs)
1✔
436

437
    class Meta:
1✔
438
        proxy = True
1✔
439

440

441
class PendingAdminTask(models.Model):
1✔
442
    class PersonType(models.IntegerChoices):
1✔
443
        NEWHIRE = 0, _("New hire")
1✔
444
        MANAGER = 1, _("Manager")
1✔
445
        BUDDY = 2, _("Buddy")
1✔
446
        CUSTOM = 3, _("Custom")
1✔
447

448
    class Notification(models.IntegerChoices):
1✔
449
        NO = 0, _("No")
1✔
450
        EMAIL = 1, _("Email")
1✔
451
        SLACK = 2, _("Slack")
1✔
452

453
    name = models.CharField(verbose_name=_("Name"), max_length=500)
1✔
454
    comment = models.CharField(
1✔
455
        verbose_name=_("Comment"), max_length=12500, default="", blank=True
456
    )
457
    person_type = models.IntegerField(
1✔
458
        # Filter out new hire. Never assign an admin task to a new hire.
459
        verbose_name=_("Assigned to"),
460
        choices=[person for person in PersonType.choices if person[0] != 0],
461
        default=PersonType.MANAGER,
462
    )
463
    assigned_to = models.ForeignKey(
1✔
464
        settings.AUTH_USER_MODEL,
465
        verbose_name=_("Pick user"),
466
        on_delete=models.CASCADE,
467
        related_name="assigned_user",
468
        null=True,
469
    )
470
    option = models.IntegerField(
1✔
471
        verbose_name=_("Send email or Slack message to extra user?"),
472
        choices=Notification.choices,
473
        default=0,
474
    )
475
    slack_user = models.ForeignKey(
1✔
476
        settings.AUTH_USER_MODEL,
477
        verbose_name=_("Slack user"),
478
        on_delete=models.SET_NULL,
479
        related_name="pending_admin_task_slack_user",
480
        blank=True,
481
        null=True,
482
    )
483
    email = models.EmailField(
1✔
484
        verbose_name=_("Email"), max_length=12500, default="", blank=True
485
    )
486
    date = models.DateField(verbose_name=_("Due date"), blank=True, null=True)
1✔
487
    priority = models.IntegerField(
1✔
488
        verbose_name=_("Priority"),
489
        choices=AdminTask.Priority.choices,
490
        default=AdminTask.Priority.MEDIUM,
491
    )
492
    template = models.BooleanField(
1✔
493
        default=False,
494
        help_text=(
495
            "Should always be False, for now it's just here to comply with other "
496
            "functions (like duplicate)"
497
        ),
498
    )
499

500
    def __str__(self):
1✔
501
        return self.name
×
502

503
    def get_user(self, new_hire):
1✔
504
        if self.person_type == PendingAdminTask.PersonType.NEWHIRE:
1✔
505
            return new_hire
1✔
506
        elif self.person_type == PendingAdminTask.PersonType.MANAGER:
1✔
507
            return new_hire.manager
1✔
508
        elif self.person_type == PendingAdminTask.PersonType.BUDDY:
1✔
509
            return new_hire.buddy
1✔
510
        else:
511
            return self.assigned_to
1✔
512

513
    def execute(self, user):
1✔
514
        from admin.admin_tasks.models import AdminTask, AdminTaskComment
1✔
515

516
        if AdminTask.objects.filter(new_hire=user, based_on=self).exists():
1✔
517
            # if a task already exists, then skip
518
            return
×
519

520
        admin_task = AdminTask.objects.create(
1✔
521
            new_hire=user,
522
            assigned_to=self.get_user(user),
523
            name=self.name,
524
            option=self.option,
525
            slack_user=self.slack_user,
526
            email=self.email,
527
            date=self.date,
528
            priority=self.priority,
529
            based_on=self,
530
        )
531
        AdminTaskComment.objects.create(
1✔
532
            content=self.comment,
533
            comment_by=admin_task.assigned_to,
534
            admin_task=admin_task,
535
        )
536
        admin_task.send_notification_new_assigned()
1✔
537
        admin_task.send_notification_third_party()
1✔
538

539
        Notification.objects.create(
1✔
540
            notification_type=Notification.Type.ADDED_ADMIN_TASK,
541
            extra_text=self.name,
542
            created_for=self.assigned_to,
543
        )
544

545
    @property
1✔
546
    def get_icon_template(self):
1✔
547
        return render_to_string("_admin_task_icon.html")
1✔
548

549
    def duplicate(self, change_name=False):
1✔
550
        self.pk = None
1✔
551
        self.save()
1✔
552
        return self
1✔
553

554

555
class IntegrationConfig(models.Model):
1✔
556
    class PersonType(models.IntegerChoices):
1✔
557
        MANAGER = 1, _("Manager")
1✔
558
        BUDDY = 2, _("Buddy")
1✔
559
        CUSTOM = 3, _("Custom")
1✔
560

561
    integration = models.ForeignKey(Integration, on_delete=models.CASCADE, null=True)
1✔
562
    additional_data = EncryptedJSONField(default=dict)
1✔
563
    person_type = models.IntegerField(
1✔
564
        verbose_name=_("Assigned to"),
565
        choices=PersonType.choices,
566
        null=True,
567
        blank=True,
568
        help_text=_(
569
            "Leave empty to automatically check the integration as created/removed."
570
        ),
571
    )
572
    assigned_to = models.ForeignKey(
1✔
573
        settings.AUTH_USER_MODEL,
574
        verbose_name=_("Pick user"),
575
        on_delete=models.CASCADE,
576
        related_name="assigned_user_integration",
577
        null=True,
578
        blank=True,
579
    )
580

581
    @property
1✔
582
    def name(self):
1✔
583
        return self.integration.name
1✔
584

585
    @property
1✔
586
    def get_icon_template(self):
1✔
587
        return render_to_string("_integration_config.html")
1✔
588

589
    def duplicate(self, change_name=False):
1✔
590
        self.pk = None
1✔
591
        self.save()
1✔
592
        return self
1✔
593

594
    def execute(self, user):
1✔
595
        # avoid circular import
596
        from users.models import IntegrationUser
1✔
597

598
        if not self.integration.skip_user_provisioning:
1✔
599
            # it's an automated integration so just execute it
600
            self.integration.execute(user, self.additional_data)
1✔
601
            return
1✔
602

603
        is_offboarding = user.termination_date is not None
1✔
604

605
        if self.person_type is None:
1✔
606
            # doesn't need extra action, just log
607
            integration_user, created = IntegrationUser.objects.get_or_create(
1✔
608
                user=user,
609
                integration=self.integration,
610
                defaults={"revoked": is_offboarding},
611
            )
612
            if not created:
1✔
613
                # make sure revoked is set correctly
614
                integration_user.revoked = is_offboarding
×
615
                integration_user.save()
×
616

617
            Notification.objects.create(
1✔
618
                notification_type=Notification.Type.REMOVE_MANUAL_INTEGRATION
619
                if is_offboarding
620
                else Notification.Type.ADD_MANUAL_INTEGRATION,
621
                extra_text=self.integration.name,
622
                created_for=user,
623
                item_id=integration_user.id,
624
                notified_user=False,
625
                public_to_new_hire=False,
626
            )
627

628
        else:
629
            # we need an admin to create the item and then check it off
630
            if self.person_type == IntegrationConfig.PersonType.MANAGER:
×
631
                assigned_to = user.manager
×
632
            elif self.person_type == IntegrationConfig.PersonType.BUDDY:
×
633
                assigned_to = user.buddy
×
634
            else:
635
                assigned_to = self.assigned_to
×
636

637
            admin_task_name = _("Create integration: {self.integration.name}")
×
638

639
            admin_task = AdminTask.objects.create(
×
640
                new_hire=user,
641
                assigned_to=assigned_to,
642
                name=admin_task_name,
643
                option=AdminTask.Notification.NO,
644
                integration=self.integration,
645
                create_integration=not is_offboarding,
646
            )
647

648
            Notification.objects.create(
×
649
                notification_type=Notification.Type.ADDED_ADMIN_TASK,
650
                extra_text=admin_task_name,
651
                created_for=user,
652
                item_id=admin_task.id,
653
                notified_user=False,
654
                public_to_new_hire=False,
655
            )
656

657

658
class ConditionPrefetchManager(models.Manager):
1✔
659
    def prefetched(self):
1✔
660
        return (
1✔
661
            self.get_queryset()
662
            .prefetch_related(
663
                Prefetch("introductions", queryset=Introduction.objects.all()),
664
                Prefetch("to_do", queryset=ToDo.objects.all().defer("content")),
665
                Prefetch("resources", queryset=Resource.objects.all()),
666
                Prefetch(
667
                    "appointments", queryset=Appointment.objects.all().defer("content")
668
                ),
669
                Prefetch("badges", queryset=Badge.objects.all().defer("content")),
670
                Prefetch(
671
                    "external_messages",
672
                    queryset=ExternalMessage.objects.for_new_hire().defer(
673
                        "content", "content_json"
674
                    ),
675
                    to_attr="external_new_hire",
676
                ),
677
                Prefetch(
678
                    "external_messages",
679
                    queryset=ExternalMessage.objects.for_admins().defer(
680
                        "content", "content_json"
681
                    ),
682
                    to_attr="external_admin",
683
                ),
684
                Prefetch(
685
                    "condition_to_do", queryset=ToDo.objects.all().defer("content")
686
                ),
687
                Prefetch("admin_tasks", queryset=PendingAdminTask.objects.all()),
688
                Prefetch(
689
                    "preboarding", queryset=Preboarding.objects.all().defer("content")
690
                ),
691
                Prefetch(
692
                    "integration_configs", queryset=IntegrationConfig.objects.all()
693
                ),
694
            )
695
            .annotate(
696
                days_order=Case(
697
                    When(condition_type=Condition.Type.BEFORE, then=F("days") * -1),
698
                    When(condition_type=Condition.Type.TODO, then=99998),
699
                    When(condition_type=Condition.Type.ADMIN_TASK, then=99999),
700
                    default=F("days"),
701
                    output_field=IntegerField(),
702
                )
703
            )
704
            .order_by("days_order", "time")
705
        )
706

707

708
class Condition(models.Model):
1✔
709
    class Type(models.IntegerChoices):
1✔
710
        AFTER = 0, _("After new hire has started")
1✔
711
        TODO = 1, _("Based on one or more to do item(s)")
1✔
712
        BEFORE = 2, _("Before the new hire has started")
1✔
713
        WITHOUT = 3, _("Without trigger")
1✔
714
        ADMIN_TASK = 4, _("Based on one or more admin tasks")
1✔
715

716
    sequence = models.ForeignKey(
1✔
717
        Sequence, on_delete=models.CASCADE, null=True, related_name="conditions"
718
    )
719
    condition_type = models.IntegerField(
1✔
720
        verbose_name=_("Block type"), choices=Type.choices, default=Type.AFTER
721
    )
722
    days = models.IntegerField(verbose_name=_("Amount of days before/after"), default=1)
1✔
723
    time = models.TimeField(verbose_name=_("At"), default="08:00")
1✔
724
    condition_to_do = models.ManyToManyField(
1✔
725
        ToDo,
726
        verbose_name=_("Trigger after these to do items have been completed:"),
727
        related_name="condition_to_do",
728
    )
729
    condition_admin_tasks = models.ManyToManyField(
1✔
730
        PendingAdminTask,
731
        verbose_name=_("Trigger after these admin todo items have been completed:"),
732
        related_name="condition_triggers",
733
    )
734
    to_do = models.ManyToManyField(ToDo)
1✔
735
    badges = models.ManyToManyField(Badge)
1✔
736
    resources = models.ManyToManyField(Resource)
1✔
737
    admin_tasks = models.ManyToManyField(PendingAdminTask)
1✔
738
    external_messages = models.ManyToManyField(ExternalMessage)
1✔
739
    introductions = models.ManyToManyField(Introduction)
1✔
740
    preboarding = models.ManyToManyField(Preboarding)
1✔
741
    appointments = models.ManyToManyField(Appointment)
1✔
742
    integration_configs = models.ManyToManyField(IntegrationConfig)
1✔
743

744
    objects = ConditionPrefetchManager()
1✔
745

746
    @property
1✔
747
    def is_empty(self):
1✔
748
        return not (
1✔
749
            self.to_do.exists()
750
            or self.badges.exists()
751
            or self.resources.exists()
752
            or self.admin_tasks.exists()
753
            or self.introductions.exists()
754
            or self.external_messages.exists()
755
            or self.preboarding.exists()
756
            or self.appointments.exists()
757
            or self.integration_configs.exists()
758
        )
759

760
    @property
1✔
761
    def based_on_to_do(self):
1✔
762
        return self.condition_type == Condition.Type.TODO
1✔
763

764
    @property
1✔
765
    def based_on_admin_task(self):
1✔
766
        return self.condition_type == Condition.Type.ADMIN_TASK
1✔
767

768
    @property
1✔
769
    def based_on_time(self):
1✔
770
        return self.condition_type in [Condition.Type.AFTER, Condition.Type.BEFORE]
1✔
771

772
    def remove_item(self, model_item):
1✔
773
        # If any of the external messages, then get the root one
774
        if type(model_item)._meta.model_name in [
1✔
775
            "pendingemailmessage",
776
            "pendingslackmessage",
777
            "pendingtextmessage",
778
        ]:
779
            model_item = ExternalMessage.objects.get(pk=model_item.id)
×
780
        # model_item is a template item. I.e. a ToDo object.
781
        for field in self._meta.many_to_many:
1✔
782
            # We only want to remove assigned items, not triggers
783
            if field.name in ("condition_to_do", "condition_admin_tasks"):
1✔
784
                continue
1✔
785
            if (
1✔
786
                field.related_model._meta.model_name
787
                == type(model_item)._meta.model_name
788
            ):
789
                getattr(self, field.name).remove(model_item)
1✔
790

791
    def add_item(self, model_item):
1✔
792
        # model_item is a template item. I.e. a ToDo object.
793
        for field in self._meta.many_to_many:
1✔
794
            # We only want to add assigned items, not triggers
795
            if field.name in ("condition_to_do", "condition_admin_tasks"):
1✔
796
                continue
1✔
797
            if (
1✔
798
                field.related_model._meta.model_name
799
                == type(model_item)._meta.model_name
800
            ):
801
                getattr(self, field.name).add(model_item)
1✔
802

803
    def include_other_condition(self, condition):
1✔
804
        # this will put another condition into this one
805
        for field in self._meta.many_to_many:
1✔
806
            # We only want to add assigned items, not triggers
807
            if field.name in ("condition_to_do", "condition_admin_tasks"):
1✔
808
                continue
1✔
809

810
            condition_field = getattr(condition, field.name)
1✔
811
            for item in condition_field.all():
1✔
812
                getattr(self, field.name).add(item)
1✔
813

814
    def duplicate(self, admin_tasks):
1✔
815
        old_condition = Condition.objects.get(id=self.id)
1✔
816
        self.pk = None
1✔
817
        self.save()
1✔
818

819
        # This function is not being used except for duplicating sequences
820
        # It can't be triggered standalone (for now)
821
        for field in old_condition._meta.many_to_many:
1✔
822
            if field.name not in [
1✔
823
                "admin_tasks",
824
                "condition_admin_tasks",
825
                "external_messages",
826
                "integration_configs",
827
            ]:
828
                # Duplicate template items that have been customized. Those should be
829
                # unique again. (only items that have a `template` flag on the model)
830
                items = []
1✔
831
                old_custom_templates = getattr(old_condition, field.name).filter(
1✔
832
                    template=False
833
                )
834
                for old in old_custom_templates:
1✔
835
                    dup = old.duplicate(change_name=False)
1✔
836
                    items.append(dup)
1✔
837

838
                # Reassign items that are still unchanged templates, they should connect
839
                # to the same item
840
                old_templates = getattr(old_condition, field.name).filter(template=True)
1✔
841
                getattr(self, field.name).add(*old_templates, *items)
1✔
842

843
            else:
844
                # For items that do not have templates, just duplicate them
845
                items = []
1✔
846
                old_custom_templates = getattr(old_condition, field.name).all()
1✔
847
                # exception for condition_admin_tasks, those should be linked to
848
                # previously created items, so link old id to new object, for
849
                # future lookup
850
                if field.name == "condition_admin_tasks":
1✔
851
                    for item in old_custom_templates:
1✔
852
                        items.append(admin_tasks[item.id])
1✔
853

854
                else:
855
                    for old in old_custom_templates:
1✔
856
                        old_id = old.id
1✔
857
                        dup = old.duplicate(change_name=False)
1✔
858
                        items.append(dup)
1✔
859
                        if field.name == "admin_tasks":
1✔
860
                            # lookup old id to get newly created object
861
                            admin_tasks[old_id] = dup
1✔
862

863
                getattr(self, field.name).add(*items)
1✔
864

865
        # returning the new item
866
        return self, admin_tasks
1✔
867

868
    def process_condition(self, user, skip_notification=False):
1✔
869
        # avoid circular import
870

871
        # Loop over all m2m fields and add the ones that can be easily added
872
        for field in [
1✔
873
            "to_do",
874
            "resources",
875
            "badges",
876
            "appointments",
877
            "introductions",
878
            "preboarding",
879
        ]:
880
            for item in getattr(self, field).all():
1✔
881
                getattr(user, field).add(item)
1✔
882

883
                Notification.objects.create(
1✔
884
                    notification_type=item.notification_add_type,
885
                    extra_text=item.name,
886
                    created_for=user,
887
                    item_id=item.id,
888
                    notified_user=skip_notification,
889
                    public_to_new_hire=True,
890
                )
891

892
        # For the ones that aren't a quick copy/paste, follow back to their model and
893
        # execute them. It will also add an item to the notification model there.
894
        for field in ["admin_tasks", "external_messages", "integration_configs"]:
1✔
895
            for item in getattr(self, field).all():
1✔
896
                item.execute(user)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc