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

chiefonboarding / ChiefOnboarding / 18481185920

13 Oct 2025 11:53PM UTC coverage: 89.613% (-0.006%) from 89.619%
18481185920

Pull #572

github

web-flow
Merge f5b8970dc into f01e5ecbf
Pull Request #572: Add permissions based on department

7057 of 7875 relevant lines covered (89.61%)

0.9 hits per line

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

98.18
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 Prefetch
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.hardware.models import Hardware
1✔
13
from admin.integrations.models import Integration
1✔
14
from admin.introductions.models import Introduction
1✔
15
from admin.preboarding.models import Preboarding
1✔
16
from admin.resources.models import Resource
1✔
17
from admin.sequences.emails import send_sequence_message
1✔
18
from admin.sequences.querysets import ConditionQuerySet
1✔
19
from admin.to_do.models import ToDo
1✔
20
from misc.fields import ContentJSONField, EncryptedJSONField
1✔
21
from misc.mixins import ContentMixin
1✔
22
from organization.models import FilteredForManagerQuerySet, Notification
1✔
23
from slack_bot.models import SlackChannel
1✔
24
from slack_bot.utils import Slack
1✔
25

26

27
class SequenceManager(models.Manager):
1✔
28
    def get_queryset(self):
1✔
29
        return FilteredForManagerQuerySet(self.model, using=self._db)
1✔
30

31
    def for_user(self, user):
1✔
32
        return self.get_queryset().for_user(user)
1✔
33

34

35
class OnboardingSequenceManager(SequenceManager):
1✔
36
    def get_queryset(self):
1✔
37
        return super().get_queryset().filter(category=Sequence.Category.ONBOARDING)
1✔
38

39

40
class OffboardingSequenceManager(SequenceManager):
1✔
41
    def get_queryset(self):
1✔
42
        return super().get_queryset().filter(category=Sequence.Category.OFFBOARDING)
1✔
43

44

45
class Sequence(models.Model):
1✔
46
    class Category(models.IntegerChoices):
1✔
47
        ONBOARDING = 0, _("Onboarding sequence")
1✔
48
        OFFBOARDING = 1, _("Offboarding sequence")
1✔
49

50
    name = models.CharField(verbose_name=_("Name"), max_length=240)
1✔
51
    auto_add = models.BooleanField(default=False)
1✔
52
    category = models.IntegerField(choices=Category.choices)
1✔
53
    departments = models.ManyToManyField(
1✔
54
        "users.Department",
55
        blank=True,
56
        help_text=_("Leave empty to make it available to all managers"),
57
    )
58

59
    objects = SequenceManager()
1✔
60
    onboarding = OnboardingSequenceManager()
1✔
61
    offboarding = OffboardingSequenceManager()
1✔
62

63
    class Meta:
1✔
64
        ordering = ("name",)
1✔
65

66
    def __str__(self):
1✔
67
        return self.name
1✔
68

69
    @property
1✔
70
    def update_url(self):
1✔
71
        return reverse("sequences:update", args=[self.id])
1✔
72

73
    def get_icon_template(self):
1✔
74
        return render_to_string("_sequence_icon.html")
1✔
75

76
    @property
1✔
77
    def is_onboarding(self):
1✔
78
        return self.category == Sequence.Category.ONBOARDING
1✔
79

80
    @property
1✔
81
    def is_offboarding(self):
1✔
82
        return self.category != Sequence.Category.ONBOARDING
1✔
83

84
    def class_name(self):
1✔
85
        return self.__class__.__name__
1✔
86

87
    def duplicate(self):
1✔
88
        old_sequence = Sequence.objects.get(pk=self.pk)
1✔
89
        self.pk = None
1✔
90
        self.name = _("%(name)s (duplicate)") % {"name": self.name}
1✔
91
        self.auto_add = False
1✔
92
        self.save()
1✔
93

94
        admin_tasks = {}
1✔
95
        for condition in old_sequence.conditions.all():
1✔
96
            new_condition, admin_tasks = condition.duplicate(admin_tasks=admin_tasks)
1✔
97
            self.conditions.add(new_condition)
1✔
98
        return self
1✔
99

100
    def assign_to_user(self, user):
1✔
101
        # adding conditions
102
        for sequence_condition in self.conditions.all():
1✔
103
            user_condition = None
1✔
104

105
            # Check what kind of condition it is
106
            if sequence_condition.condition_type in [
1✔
107
                Condition.Type.BEFORE,
108
                Condition.Type.AFTER,
109
            ]:
110
                # Get the timed based condition or return None if not exist
111
                user_condition = user.conditions.filter(
1✔
112
                    condition_type=sequence_condition.condition_type,
113
                    days=sequence_condition.days,
114
                    time=sequence_condition.time,
115
                ).first()
116

117
            elif sequence_condition.condition_type == Condition.Type.TODO:
1✔
118
                # For to_do items, filter all condition items to find if one matches
119
                # Both the amount and the todos itself need to match exactly
120
                conditions = user.conditions.filter(condition_type=Condition.Type.TODO)
1✔
121
                original_condition_to_do_ids = (
1✔
122
                    sequence_condition.condition_to_do.all().values_list(
123
                        "id", flat=True
124
                    )
125
                )
126

127
                for condition in conditions:
1✔
128
                    # Quickly check if the amount of items match - if not match, drop
129
                    if condition.condition_to_do.all().count() != len(
1✔
130
                        original_condition_to_do_ids
131
                    ):
132
                        continue
×
133

134
                    found_to_do_items = condition.condition_to_do.filter(
1✔
135
                        id__in=original_condition_to_do_ids
136
                    ).count()
137

138
                    if found_to_do_items == len(original_condition_to_do_ids):
1✔
139
                        # We found our match. Amount matches AND the todos match
140
                        user_condition = condition
1✔
141
                        break
1✔
142

143
            elif sequence_condition.condition_type == Condition.Type.ADMIN_TASK:
1✔
144
                # For admin to do items, filter all condition items to find if one
145
                # matches. Both the amount and the admin to do itself need to match
146
                # exactly
147
                conditions = user.conditions.filter(
1✔
148
                    condition_type=Condition.Type.ADMIN_TASK
149
                )
150
                original_condition_admin_tasks_ids = (
1✔
151
                    sequence_condition.condition_admin_tasks.all().values_list(
152
                        "id", flat=True
153
                    )
154
                )
155

156
                for condition in conditions:
1✔
157
                    # Quickly check if the amount of items match - if not match, drop
158
                    if condition.condition_admin_tasks.all().count() != len(  # noqa
1✔
159
                        original_condition_admin_tasks_ids
160
                    ):
161
                        continue
1✔
162

163
                    found_admin_tasks = condition.condition_admin_tasks.filter(
1✔
164
                        id__in=original_condition_admin_tasks_ids
165
                    ).count()
166

167
                    if found_admin_tasks == len(original_condition_admin_tasks_ids):
1✔
168
                        # We found our match. Amount matches AND the admin_tasks match
169
                        user_condition = condition
1✔
170
                        break
1✔
171

172
            elif (
1✔
173
                sequence_condition.condition_type == Condition.Type.INTEGRATIONS_REVOKED
174
            ):
175
                user_condition = user.conditions.filter(
1✔
176
                    condition_type=Condition.Type.INTEGRATIONS_REVOKED
177
                ).first()
178

179
            else:
180
                # Condition (always just one) that will be assigned directly (type == 3)
181
                # Just run the condition with the new hire
182
                sequence_condition.process_condition(user)
1✔
183
                continue
1✔
184

185
            # Let's add the condition to the new hire. Either through adding it to the
186
            # exising one or creating a new one
187
            if user_condition is not None:
1✔
188
                # adding items to existing condition
189
                user_condition.include_other_condition(sequence_condition)
1✔
190
            else:
191
                # duplicating condition and adding to user
192
                # Force getting it, as we are about to duplicate it and set the pk to
193
                # None
194
                old_condition = Condition.objects.get(id=sequence_condition.id)
1✔
195

196
                sequence_condition.pk = None
1✔
197
                sequence_condition.sequence = None
1✔
198
                sequence_condition.save()
1✔
199

200
                # Add condition to_dos
201
                sequence_condition.condition_to_do.set(
1✔
202
                    old_condition.condition_to_do.all()
203
                )
204

205
                for condition_admin_task in old_condition.condition_admin_tasks.all():
1✔
206
                    sequence_condition.condition_admin_tasks.add(condition_admin_task)
1✔
207
                # Add all the things that get triggered
208
                sequence_condition.include_other_condition(old_condition)
1✔
209

210
                # Add newly created condition back to user
211
                user.conditions.add(sequence_condition)
1✔
212

213
    def remove_from_user(self, new_hire):
1✔
214
        from admin.admin_tasks.models import AdminTask
1✔
215

216
        # get all items from all conditions
217
        to_dos = ToDo.objects.none()
1✔
218
        badges = Badge.objects.none()
1✔
219
        resources = Resource.objects.none()
1✔
220
        admin_tasks = AdminTask.objects.none()
1✔
221
        external_messages = ExternalMessage.objects.none()
1✔
222
        introductions = Introduction.objects.none()
1✔
223
        preboarding = Preboarding.objects.none()
1✔
224
        appointments = Appointment.objects.none()
1✔
225
        integration_configs = IntegrationConfig.objects.none()
1✔
226
        hardware = Hardware.objects.none()
1✔
227

228
        # TODO: this is going to make a lot of queries, should be optimized
229
        for condition in self.conditions.all():
1✔
230
            to_dos |= condition.to_do.all()
1✔
231
            badges |= condition.badges.all()
1✔
232
            resources |= condition.resources.all()
1✔
233
            admin_tasks |= condition.admin_tasks.all()
1✔
234
            external_messages |= condition.external_messages.all()
1✔
235
            introductions |= condition.introductions.all()
1✔
236
            preboarding |= condition.preboarding.all()
1✔
237
            appointments |= condition.appointments.all()
1✔
238
            integration_configs |= condition.integration_configs.all()
1✔
239
            hardware |= condition.hardware.all()
1✔
240

241
        # Cycle through new hire's item and remove the ones that aren't supposed to
242
        # be there
243
        new_hire.to_do.remove(*to_dos)
1✔
244
        new_hire.badges.remove(*badges)
1✔
245
        new_hire.appointments.remove(*appointments)
1✔
246
        new_hire.preboarding.remove(*preboarding)
1✔
247
        new_hire.introductions.remove(*introductions)
1✔
248
        new_hire.hardware.remove(*hardware)
1✔
249

250
        # Do the same with the conditions
251
        conditions_to_be_deleted = []
1✔
252
        items = {
1✔
253
            "to_do": to_dos,
254
            "resources": resources,
255
            "badges": badges,
256
            "admin_tasks": admin_tasks,
257
            "external_messages": external_messages,
258
            "introductions": introductions,
259
            "preboarding": preboarding,
260
            "appointments": appointments,
261
            "integration_configs": integration_configs,
262
            "hardware": hardware,
263
        }
264
        for condition in new_hire.conditions.all():
1✔
265
            for field in condition._meta.many_to_many:
1✔
266
                # We only want to remove assigned items, not triggers
267
                if field.name in ("condition_to_do", "condition_admin_tasks"):
1✔
268
                    continue
1✔
269
                getattr(condition, field.name).remove(*items[field.name])
1✔
270

271
            if condition.is_empty:
1✔
272
                conditions_to_be_deleted.append(condition.id)
1✔
273

274
        # Remove all empty conditions
275
        Condition.objects.filter(id__in=conditions_to_be_deleted).delete()
1✔
276
        # Delete sequence
277
        Notification.objects.order_by("-created").filter(
1✔
278
            notification_type=Notification.Type.ADDED_SEQUENCE
279
        ).first().delete()
280

281

282
class ExternalMessageManager(models.Manager):
1✔
283
    def for_new_hire(self):
1✔
284
        return self.get_queryset().filter(
1✔
285
            person_type=ExternalMessage.PersonType.NEWHIRE
286
        )
287

288
    def for_user(self, user):
1✔
289
        # just return all as we filter on sequence
290
        return self.get_queryset()
1✔
291

292
    def for_admins(self):
1✔
293
        return self.get_queryset().exclude(
1✔
294
            person_type=ExternalMessage.PersonType.NEWHIRE
295
        )
296

297

298
class ExternalMessage(ContentMixin, models.Model):
1✔
299
    class Type(models.IntegerChoices):
1✔
300
        EMAIL = 0, _("Email")
1✔
301
        SLACK = 1, _("Slack")
1✔
302
        TEXT = 2, _("Text")
1✔
303

304
    class PersonType(models.IntegerChoices):
1✔
305
        NEWHIRE = 0, _("New hire/User to be offboarded")
1✔
306
        MANAGER = 1, _("Manager")
1✔
307
        BUDDY = 2, _("Buddy")
1✔
308
        CUSTOM = 3, _("Custom")
1✔
309
        SLACK_CHANNEL = 4, _("Slack channel")
1✔
310

311
    name = models.CharField(verbose_name=_("Name"), max_length=240)
1✔
312
    content_json = ContentJSONField(default=dict, verbose_name=_("Content"))
1✔
313
    content = models.CharField(verbose_name=_("Content"), max_length=12000, blank=True)
1✔
314
    send_via = models.IntegerField(verbose_name=_("Send via"), choices=Type.choices)
1✔
315
    send_to = models.ForeignKey(
1✔
316
        settings.AUTH_USER_MODEL,
317
        verbose_name=_("Send to"),
318
        on_delete=models.CASCADE,
319
        blank=True,
320
        null=True,
321
    )
322
    send_to_channel = models.ForeignKey(
1✔
323
        SlackChannel,
324
        verbose_name=_("Slack channel"),
325
        on_delete=models.CASCADE,
326
        blank=True,
327
        null=True,
328
    )
329
    subject = models.CharField(
1✔
330
        verbose_name=_("Subject"),
331
        max_length=78,
332
        default=_("Here is an update!"),
333
        blank=True,
334
    )
335
    person_type = models.IntegerField(
1✔
336
        verbose_name=_("For"), choices=PersonType.choices, default=1
337
    )
338

339
    @property
1✔
340
    def is_email_message(self):
1✔
341
        return self.send_via == self.Type.EMAIL
1✔
342

343
    @property
1✔
344
    def is_slack_message(self):
1✔
345
        return self.send_via == self.Type.SLACK
1✔
346

347
    @property
1✔
348
    def is_text_message(self):
1✔
349
        return self.send_via == self.Type.TEXT
1✔
350

351
    @property
1✔
352
    def notification_add_type(self):
1✔
353
        if self.is_text_message:
1✔
354
            return Notification.Type.SENT_TEXT_MESSAGE
1✔
355
        if self.is_email_message:
1✔
356
            return Notification.Type.SENT_EMAIL_MESSAGE
1✔
357
        if self.is_slack_message:
1✔
358
            return Notification.Type.SENT_SLACK_MESSAGE
1✔
359

360
    def get_icon_template(self):
1✔
361
        if self.is_email_message:
1✔
362
            return render_to_string("_email_icon.html")
1✔
363
        if self.is_slack_message:
1✔
364
            return render_to_string("_slack_icon.html")
1✔
365
        if self.is_text_message:
1✔
366
            return render_to_string("_text_icon.html")
1✔
367

368
    @property
1✔
369
    def requires_assigned_manager_or_buddy(self):
1✔
370
        # returns manager, buddy
371
        return (
×
372
            self.person_type == ExternalMessage.PersonType.MANAGER,
373
            self.person_type == ExternalMessage.PersonType.BUDDY,
374
        )
375

376
    def duplicate(self, change_name=False):
1✔
377
        self.pk = None
1✔
378
        self.save()
1✔
379
        return self
1✔
380

381
    def get_user(self, new_hire):
1✔
382
        if self.person_type == ExternalMessage.PersonType.NEWHIRE:
1✔
383
            return new_hire
1✔
384
        elif self.person_type == ExternalMessage.PersonType.MANAGER:
1✔
385
            return new_hire.manager
1✔
386
        elif self.person_type == ExternalMessage.PersonType.BUDDY:
1✔
387
            return new_hire.buddy
1✔
388
        else:
389
            return self.send_to
1✔
390

391
    def execute(self, user):
1✔
392
        if self.is_email_message:
1✔
393
            # Make sure there is actually an email
394
            if self.get_user(user) is None:
1✔
395
                Notification.objects.create(
1✔
396
                    notification_type=Notification.Type.FAILED_NO_EMAIL,
397
                    extra_text=self.subject,
398
                    created_for=user,
399
                )
400
                return
1✔
401

402
            send_sequence_message(
1✔
403
                user, self.get_user(user), self.content_json["blocks"], self.subject
404
            )
405
        elif self.is_slack_message:
1✔
406
            blocks = []
1✔
407
            # We don't have the model function on this model, so let's get it from a
408
            # different model. A bit hacky, but should be okay.
409
            blocks = ToDo(content=self.content_json).to_slack_block(user)
1✔
410

411
            # Send to channel instead of person?
412
            if self.person_type == ExternalMessage.PersonType.SLACK_CHANNEL:
1✔
413
                channel = (
1✔
414
                    None
415
                    if self.send_to_channel is None
416
                    else "#" + self.send_to_channel.name
417
                )
418
            else:
419
                channel = self.get_user(user).slack_user_id
1✔
420

421
            Slack().send_message(blocks=blocks, channel=channel)
1✔
422
        else:  # text message
423
            send_to = self.get_user(user)
1✔
424
            if send_to is None or send_to.phone == "":
1✔
425
                Notification.objects.create(
1✔
426
                    notification_type=Notification.Type.FAILED_NO_PHONE,
427
                    extra_text=self.name,
428
                    created_for=user,
429
                )
430
                return
1✔
431

432
            client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)
×
433
            client.messages.create(
×
434
                to=send_to.phone,
435
                from_=settings.TWILIO_FROM_NUMBER,
436
                body=self.get_user(user).personalize(self.content),
437
            )
438

439
        if not self.is_slack_message:
1✔
440
            # The Slack send_message function already registered this notification so
441
            # skip it in that case.
442
            Notification.objects.create(
1✔
443
                notification_type=self.notification_add_type,
444
                extra_text=self.name,
445
                created_for=user,
446
            )
447

448
    objects = ExternalMessageManager()
1✔
449

450

451
class PendingEmailMessage(ExternalMessage):
1✔
452
    # Email message model proxied from ExternalMessage
453

454
    def save(self, *args, **kwargs):
1✔
455
        self.send_via = self.Type.EMAIL
1✔
456
        return super(PendingEmailMessage, self).save(*args, **kwargs)
1✔
457

458
    class Meta:
1✔
459
        proxy = True
1✔
460

461

462
class PendingSlackMessage(ExternalMessage):
1✔
463
    # Slack message model proxied from ExternalMessage
464

465
    def save(self, *args, **kwargs):
1✔
466
        self.send_via = self.Type.SLACK
1✔
467
        return super(PendingSlackMessage, self).save(*args, **kwargs)
1✔
468

469
    class Meta:
1✔
470
        proxy = True
1✔
471

472

473
class PendingTextMessage(ExternalMessage):
1✔
474
    # Text message model proxied from ExternalMessage
475

476
    def save(self, *args, **kwargs):
1✔
477
        self.send_via = self.Type.TEXT
1✔
478
        return super(PendingTextMessage, self).save(*args, **kwargs)
1✔
479

480
    class Meta:
1✔
481
        proxy = True
1✔
482

483

484
class PendingAdminTaskManager(models.Manager):
1✔
485
    def for_user(self, user):
1✔
486
        # just return all as we filter on sequence
487
        return self.get_queryset()
1✔
488

489

490
class PendingAdminTask(models.Model):
1✔
491
    class PersonType(models.IntegerChoices):
1✔
492
        NEWHIRE = 0, _("New hire/User to be offboarded")
1✔
493
        MANAGER = 1, _("Manager")
1✔
494
        BUDDY = 2, _("Buddy")
1✔
495
        CUSTOM = 3, _("Custom")
1✔
496

497
    class Notification(models.IntegerChoices):
1✔
498
        NO = 0, _("No")
1✔
499
        EMAIL = 1, _("Email")
1✔
500
        SLACK = 2, _("Slack")
1✔
501

502
    name = models.CharField(verbose_name=_("Name"), max_length=500)
1✔
503
    comment = models.CharField(
1✔
504
        verbose_name=_("Comment"), max_length=12500, default="", blank=True
505
    )
506
    person_type = models.IntegerField(
1✔
507
        # Filter out new hire. Never assign an admin task to a new hire.
508
        verbose_name=_("Assigned to"),
509
        choices=[person for person in PersonType.choices if person[0] != 0],
510
        default=PersonType.MANAGER,
511
    )
512
    assigned_to = models.ForeignKey(
1✔
513
        settings.AUTH_USER_MODEL,
514
        verbose_name=_("Pick user"),
515
        on_delete=models.CASCADE,
516
        related_name="assigned_user",
517
        null=True,
518
    )
519
    option = models.IntegerField(
1✔
520
        verbose_name=_("Send email or Slack message to extra user?"),
521
        choices=Notification.choices,
522
        default=0,
523
    )
524
    slack_user = models.ForeignKey(
1✔
525
        settings.AUTH_USER_MODEL,
526
        verbose_name=_("Slack user"),
527
        on_delete=models.SET_NULL,
528
        related_name="pending_admin_task_slack_user",
529
        blank=True,
530
        null=True,
531
    )
532
    email = models.EmailField(
1✔
533
        verbose_name=_("Email"), max_length=12500, default="", blank=True
534
    )
535
    date = models.DateField(verbose_name=_("Due date"), blank=True, null=True)
1✔
536
    priority = models.IntegerField(
1✔
537
        verbose_name=_("Priority"),
538
        choices=AdminTask.Priority.choices,
539
        default=AdminTask.Priority.MEDIUM,
540
    )
541
    template = models.BooleanField(
1✔
542
        default=False,
543
        help_text=(
544
            "Should always be False, for now it's just here to comply with other "
545
            "functions (like duplicate)"
546
        ),
547
    )
548

549
    objects = PendingAdminTaskManager()
1✔
550

551
    def __str__(self):
1✔
552
        return self.name
×
553

554
    @property
1✔
555
    def requires_assigned_manager_or_buddy(self):
1✔
556
        # returns manager, buddy
557
        return (
×
558
            self.person_type == PendingAdminTask.PersonType.MANAGER,
559
            self.person_type == PendingAdminTask.PersonType.BUDDY,
560
        )
561

562
    def get_user(self, new_hire):
1✔
563
        if self.person_type == PendingAdminTask.PersonType.NEWHIRE:
1✔
564
            return new_hire
1✔
565
        elif self.person_type == PendingAdminTask.PersonType.MANAGER:
1✔
566
            return new_hire.manager
1✔
567
        elif self.person_type == PendingAdminTask.PersonType.BUDDY:
1✔
568
            return new_hire.buddy
1✔
569
        else:
570
            return self.assigned_to
1✔
571

572
    def execute(self, user):
1✔
573
        from admin.admin_tasks.models import AdminTask
1✔
574

575
        if AdminTask.objects.filter(new_hire=user, based_on=self).exists():
1✔
576
            # if a task already exists, then skip
577
            return
×
578

579
        AdminTask.objects.create_admin_task(
1✔
580
            new_hire=user,
581
            assigned_to=self.get_user(user),
582
            name=self.name,
583
            option=self.option,
584
            slack_user=self.slack_user,
585
            email=self.email,
586
            date=self.date,
587
            priority=self.priority,
588
            pending_admin_task=self,
589
            comment=self.comment,
590
        )
591

592
    def get_icon_template(self):
1✔
593
        return render_to_string("_admin_task_icon.html")
1✔
594

595
    def duplicate(self, change_name=False):
1✔
596
        self.pk = None
1✔
597
        self.save()
1✔
598
        return self
1✔
599

600

601
class IntegrationConfig(models.Model):
1✔
602
    class PersonType(models.IntegerChoices):
1✔
603
        MANAGER = 1, _("Manager")
1✔
604
        BUDDY = 2, _("Buddy")
1✔
605
        CUSTOM = 3, _("Custom")
1✔
606

607
    integration = models.ForeignKey(Integration, on_delete=models.CASCADE, null=True)
1✔
608
    additional_data = EncryptedJSONField(default=dict)
1✔
609
    person_type = models.IntegerField(
1✔
610
        verbose_name=_("Assigned to"),
611
        choices=PersonType.choices,
612
        null=True,
613
        blank=True,
614
        help_text=_(
615
            "Leave empty to automatically check the integration as created/removed."
616
        ),
617
    )
618
    assigned_to = models.ForeignKey(
1✔
619
        settings.AUTH_USER_MODEL,
620
        verbose_name=_("Pick user"),
621
        on_delete=models.CASCADE,
622
        related_name="assigned_user_integration",
623
        null=True,
624
        blank=True,
625
    )
626

627
    @property
1✔
628
    def requires_assigned_manager_or_buddy(self):
1✔
629
        # returns manager, buddy
630
        return (
1✔
631
            self.person_type == IntegrationConfig.PersonType.MANAGER,
632
            self.person_type == IntegrationConfig.PersonType.BUDDY,
633
        )
634

635
    @property
1✔
636
    def name(self):
1✔
637
        return self.integration.name
1✔
638

639
    def get_icon_template(self):
1✔
640
        return render_to_string("_integration_config.html")
1✔
641

642
    def duplicate(self, change_name=False):
1✔
643
        self.pk = None
1✔
644
        self.save()
1✔
645
        return self
1✔
646

647
    def execute(self, user):
1✔
648
        # avoid circular import
649
        from users.models import IntegrationUser
1✔
650

651
        if not self.integration.skip_user_provisioning:
1✔
652
            # it's an automated integration so just execute it
653
            if user.is_offboarding and self.integration.can_revoke_access:
1✔
654
                self.integration.revoke_user(user)
1✔
655
            else:
656
                self.integration.execute(
1✔
657
                    user, self.additional_data, retry_on_failure=True
658
                )
659
            return
1✔
660

661
        if self.person_type is None:
1✔
662
            # doesn't need extra action, just log
663
            integration_user, created = IntegrationUser.objects.update_or_create(
1✔
664
                user=user,
665
                integration=self.integration,
666
                defaults={"revoked": user.is_offboarding},
667
            )
668

669
            Notification.objects.create(
1✔
670
                notification_type=(
671
                    Notification.Type.REMOVE_MANUAL_INTEGRATION
672
                    if user.is_offboarding
673
                    else Notification.Type.ADD_MANUAL_INTEGRATION
674
                ),
675
                extra_text=self.integration.name,
676
                created_for=user,
677
                item_id=integration_user.id,
678
                notified_user=False,
679
                public_to_new_hire=False,
680
            )
681

682
        else:
683
            # we need an admin to create the item and then check it off
684
            if self.person_type == IntegrationConfig.PersonType.MANAGER:
1✔
685
                assigned_to = user.manager
1✔
686
            elif self.person_type == IntegrationConfig.PersonType.BUDDY:
1✔
687
                assigned_to = user.buddy
1✔
688
            else:
689
                assigned_to = self.assigned_to
1✔
690

691
            admin_task_name = _("Create integration: %(integration_name)s") % {
1✔
692
                "integration_name": self.integration.name
693
            }
694

695
            AdminTask.objects.create_admin_task(
1✔
696
                new_hire=user,
697
                assigned_to=assigned_to,
698
                name=admin_task_name,
699
                manual_integration=self.integration,
700
            )
701

702

703
class ConditionPrefetchManager(models.Manager):
1✔
704
    def prefetched(self):
1✔
705
        return (
1✔
706
            self.get_queryset()
707
            .prefetch_related(
708
                Prefetch("introductions", queryset=Introduction.objects.all()),
709
                Prefetch("to_do", queryset=ToDo.objects.all().defer("content")),
710
                Prefetch("resources", queryset=Resource.objects.all()),
711
                Prefetch(
712
                    "appointments", queryset=Appointment.objects.all().defer("content")
713
                ),
714
                Prefetch("badges", queryset=Badge.objects.all().defer("content")),
715
                Prefetch(
716
                    "external_messages",
717
                    queryset=ExternalMessage.objects.for_new_hire().defer(
718
                        "content", "content_json"
719
                    ),
720
                    to_attr="external_new_hire",
721
                ),
722
                Prefetch(
723
                    "external_messages",
724
                    queryset=ExternalMessage.objects.for_admins().defer(
725
                        "content", "content_json"
726
                    ),
727
                    to_attr="external_admin",
728
                ),
729
                Prefetch(
730
                    "condition_to_do", queryset=ToDo.objects.all().defer("content")
731
                ),
732
                Prefetch("admin_tasks", queryset=PendingAdminTask.objects.all()),
733
                Prefetch(
734
                    "preboarding", queryset=Preboarding.objects.all().defer("content")
735
                ),
736
                Prefetch(
737
                    "integration_configs", queryset=IntegrationConfig.objects.all()
738
                ),
739
                Prefetch("hardware", queryset=Hardware.objects.all()),
740
            )
741
            .alias_days_order()
742
            .order_by("days_order", "time")
743
        )
744

745

746
class Condition(models.Model):
1✔
747
    class Type(models.IntegerChoices):
1✔
748
        AFTER = 0, _("After new hire has started")
1✔
749
        TODO = 1, _("Based on one or more to do item(s)")
1✔
750
        BEFORE = 2, _("Before the new hire has started")
1✔
751
        WITHOUT = 3, _("Without trigger")
1✔
752
        ADMIN_TASK = 4, _("Based on one or more admin tasks")
1✔
753
        INTEGRATIONS_REVOKED = 5, _("When all integrations have been revoked")
1✔
754

755
    sequence = models.ForeignKey(
1✔
756
        Sequence, on_delete=models.CASCADE, null=True, related_name="conditions"
757
    )
758
    condition_type = models.IntegerField(
1✔
759
        verbose_name=_("Block type"), choices=Type.choices, default=Type.AFTER
760
    )
761
    days = models.IntegerField(verbose_name=_("Amount of days before/after"), default=1)
1✔
762
    time = models.TimeField(verbose_name=_("At"), default="08:00")
1✔
763
    condition_to_do = models.ManyToManyField(
1✔
764
        ToDo,
765
        verbose_name=_("Trigger after these to do items have been completed:"),
766
        related_name="condition_to_do",
767
    )
768
    condition_admin_tasks = models.ManyToManyField(
1✔
769
        PendingAdminTask,
770
        verbose_name=_("Trigger after these admin todo items have been completed:"),
771
        related_name="condition_triggers",
772
    )
773
    to_do = models.ManyToManyField(ToDo)
1✔
774
    badges = models.ManyToManyField(Badge)
1✔
775
    resources = models.ManyToManyField(Resource)
1✔
776
    admin_tasks = models.ManyToManyField(PendingAdminTask)
1✔
777
    external_messages = models.ManyToManyField(ExternalMessage)
1✔
778
    introductions = models.ManyToManyField(Introduction)
1✔
779
    preboarding = models.ManyToManyField(Preboarding)
1✔
780
    appointments = models.ManyToManyField(Appointment)
1✔
781
    integration_configs = models.ManyToManyField(IntegrationConfig)
1✔
782
    hardware = models.ManyToManyField(Hardware)
1✔
783

784
    objects = ConditionPrefetchManager.from_queryset(ConditionQuerySet)()
1✔
785

786
    @property
1✔
787
    def is_empty(self):
1✔
788
        return not (
1✔
789
            self.to_do.exists()
790
            or self.badges.exists()
791
            or self.resources.exists()
792
            or self.admin_tasks.exists()
793
            or self.introductions.exists()
794
            or self.external_messages.exists()
795
            or self.preboarding.exists()
796
            or self.appointments.exists()
797
            or self.integration_configs.exists()
798
            or self.hardware.exists()
799
        )
800

801
    @property
1✔
802
    def based_on_to_do(self):
1✔
803
        return self.condition_type == Condition.Type.TODO
1✔
804

805
    @property
1✔
806
    def based_on_admin_task(self):
1✔
807
        return self.condition_type == Condition.Type.ADMIN_TASK
1✔
808

809
    @property
1✔
810
    def based_on_time(self):
1✔
811
        return self.condition_type in [Condition.Type.AFTER, Condition.Type.BEFORE]
1✔
812

813
    def remove_item(self, model_item):
1✔
814
        # If any of the external messages, then get the root one
815
        if type(model_item)._meta.model_name in [
1✔
816
            "pendingemailmessage",
817
            "pendingslackmessage",
818
            "pendingtextmessage",
819
        ]:
820
            model_item = ExternalMessage.objects.get(pk=model_item.id)
×
821
        # model_item is a template item. I.e. a ToDo object.
822
        for field in self._meta.many_to_many:
1✔
823
            # We only want to remove assigned items, not triggers
824
            if field.name in ("condition_to_do", "condition_admin_tasks"):
1✔
825
                continue
1✔
826
            if (
1✔
827
                field.related_model._meta.model_name
828
                == type(model_item)._meta.model_name
829
            ):
830
                getattr(self, field.name).remove(model_item)
1✔
831

832
    def add_item(self, model_item):
1✔
833
        # model_item is a template item. I.e. a ToDo object.
834
        for field in self._meta.many_to_many:
1✔
835
            # We only want to add assigned items, not triggers
836
            if field.name in ("condition_to_do", "condition_admin_tasks"):
1✔
837
                continue
1✔
838
            if (
1✔
839
                field.related_model._meta.model_name
840
                == type(model_item)._meta.model_name
841
            ):
842
                getattr(self, field.name).add(model_item)
1✔
843

844
    def include_other_condition(self, condition):
1✔
845
        # this will put another condition into this one
846
        for field in self._meta.many_to_many:
1✔
847
            # We only want to add assigned items, not triggers
848
            if field.name in ("condition_to_do", "condition_admin_tasks"):
1✔
849
                continue
1✔
850

851
            condition_field = getattr(condition, field.name)
1✔
852
            for item in condition_field.all():
1✔
853
                getattr(self, field.name).add(item)
1✔
854

855
    def duplicate(self, admin_tasks):
1✔
856
        old_condition = Condition.objects.get(id=self.id)
1✔
857
        self.pk = None
1✔
858
        self.save()
1✔
859

860
        # This function is not being used except for duplicating sequences
861
        # It can't be triggered standalone (for now)
862
        for field in old_condition._meta.many_to_many:
1✔
863
            if field.name not in [
1✔
864
                "admin_tasks",
865
                "condition_admin_tasks",
866
                "external_messages",
867
                "integration_configs",
868
            ]:
869
                # Duplicate template items that have been customized. Those should be
870
                # unique again. (only items that have a `template` flag on the model)
871
                items = []
1✔
872
                old_custom_templates = getattr(old_condition, field.name).filter(
1✔
873
                    template=False
874
                )
875
                for old in old_custom_templates:
1✔
876
                    dup = old.duplicate(change_name=False)
1✔
877
                    items.append(dup)
1✔
878

879
                # Reassign items that are still unchanged templates, they should connect
880
                # to the same item
881
                old_templates = getattr(old_condition, field.name).filter(template=True)
1✔
882
                getattr(self, field.name).add(*old_templates, *items)
1✔
883

884
            else:
885
                # For items that do not have templates, just duplicate them
886
                items = []
1✔
887
                old_custom_templates = getattr(old_condition, field.name).all()
1✔
888
                # exception for condition_admin_tasks, those should be linked to
889
                # previously created items, so link old id to new object, for
890
                # future lookup
891
                if field.name == "condition_admin_tasks":
1✔
892
                    for item in old_custom_templates:
1✔
893
                        items.append(admin_tasks[item.id])
1✔
894

895
                else:
896
                    for old in old_custom_templates:
1✔
897
                        old_id = old.id
1✔
898
                        dup = old.duplicate(change_name=False)
1✔
899
                        items.append(dup)
1✔
900
                        if field.name == "admin_tasks":
1✔
901
                            # lookup old id to get newly created object
902
                            admin_tasks[old_id] = dup
1✔
903

904
                getattr(self, field.name).add(*items)
1✔
905

906
        # returning the new item
907
        return self, admin_tasks
1✔
908

909
    def process_condition(self, user, skip_notification=False):
1✔
910
        # Loop over all m2m fields and add the ones that can be easily added
911
        for field in [
1✔
912
            "to_do",
913
            "resources",
914
            "badges",
915
            "appointments",
916
            "introductions",
917
            "preboarding",
918
        ]:
919
            for item in getattr(self, field).all():
1✔
920
                getattr(user, field).add(item)
1✔
921

922
                Notification.objects.create(
1✔
923
                    notification_type=item.notification_add_type,
924
                    extra_text=item.name,
925
                    created_for=user,
926
                    item_id=item.id,
927
                    notified_user=skip_notification,
928
                    public_to_new_hire=True,
929
                )
930

931
        # For the ones that aren't a quick copy/paste, follow back to their model and
932
        # execute them. It will also add an item to the notification model there.
933
        for field in [
1✔
934
            "admin_tasks",
935
            "external_messages",
936
            "integration_configs",
937
            "hardware",
938
        ]:
939
            for item in getattr(self, field).all():
1✔
940
                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