• 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

97.98
back/admin/sequences/views.py
1
from django.contrib.messages.views import SuccessMessageMixin
1✔
2
from django.http import Http404, HttpResponse
1✔
3
from django.shortcuts import get_object_or_404, render
1✔
4
from django.urls import reverse_lazy
1✔
5
from django.utils.translation import gettext as _
1✔
6
from django.views.generic import View
1✔
7
from django.views.generic.base import RedirectView
1✔
8
from django.views.generic.detail import DetailView
1✔
9
from django.views.generic.edit import (
1✔
10
    DeleteView,
11
    BaseUpdateView,
12
    CreateView,
13
    UpdateView,
14
)
15
from django.views.generic.list import ListView
1✔
16

17
from admin.integrations.forms import IntegrationConfigForm
1✔
18
from admin.integrations.models import Integration
1✔
19
from admin.sequences.utils import get_sequence_model_form, get_sequence_templates_model
1✔
20
from admin.templates.utils import get_templates_model
1✔
21
from admin.to_do.models import ToDo
1✔
22
from users.mixins import LoginRequiredMixin, ManagerPermMixin
1✔
23

24
from .forms import (
1✔
25
    ConditionForm,
26
    OffboardingConditionForm,
27
    PendingEmailMessageForm,
28
    PendingSlackMessageForm,
29
    PendingTextMessageForm,
30
)
31
from admin.sequences.models import (
1✔
32
    Condition,
33
    ExternalMessage,
34
    IntegrationConfig,
35
    Sequence,
36
)
37

38

39
class SequenceListView(LoginRequiredMixin, ManagerPermMixin, ListView):
1✔
40
    """
41
    Lists all onboarding sequences in a table.
42
    """
43

44
    template_name = "templates.html"
1✔
45
    queryset = Sequence.onboarding.all().order_by("name")
1✔
46
    paginate_by = 10
1✔
47

48
    def get_context_data(self, **kwargs):
1✔
49
        context = super().get_context_data(**kwargs)
1✔
50
        context["title"] = _("Onboarding sequence items")
1✔
51
        context["subtitle"] = ""
1✔
52
        context["add_action"] = reverse_lazy("sequences:create")
1✔
53
        return context
1✔
54

55

56
class SequenceCreateView(LoginRequiredMixin, ManagerPermMixin, RedirectView):
1✔
57
    """
58
    Creates a new sequences, also adds a default (empty) sequence for unconditional
59
    items. Redirects user back to the newly created sequence.
60
    """
61

62
    permanent = False
1✔
63

64
    def get_redirect_url(self, *args, **kwargs):
1✔
65
        category = Sequence.Category.ONBOARDING
1✔
66
        if "offboarding" in self.request.path:
1✔
67
            category = Sequence.Category.OFFBOARDING
×
68

69
        seq = Sequence.objects.create(name="New sequence", category=category)
1✔
70
        seq.conditions.create(condition_type=Condition.Type.WITHOUT)
1✔
71
        return seq.update_url
1✔
72

73

74
class SequenceView(LoginRequiredMixin, ManagerPermMixin, DetailView):
1✔
75
    """
76
    Shows one sequence to the user. This includes a list of `ToDo` items for on the
77
    right side (the others will be loaded on click).
78
    """
79

80
    template_name = "sequence.html"
1✔
81
    model = Sequence
1✔
82

83
    def get_context_data(self, **kwargs):
1✔
84
        context = super().get_context_data(**kwargs)
1✔
85
        context["title"] = _("Sequence")
1✔
86
        context["subtitle"] = ""
1✔
87
        context["object_list"] = ToDo.templates.all().defer("content")
1✔
88
        context["condition_form"] = ConditionForm(sequence=self.object)
1✔
89
        context["create"] = True  # used for ConditionForm
1✔
90
        context["todos"] = ToDo.templates.all().defer("content")
1✔
91
        context["conditions"] = self.object.conditions.prefetched()
1✔
92
        return context
1✔
93

94

95
class SequenceNameUpdateView(LoginRequiredMixin, ManagerPermMixin, BaseUpdateView):
1✔
96
    """
97
    Updates the name of the sequence when the user ends typing.
98

99
    HTMX view.
100
    """
101

102
    template_name = "_sequence_templates_list.html"
1✔
103
    model = Sequence
1✔
104
    fields = [
1✔
105
        "name",
106
    ]
107

108
    def form_valid(self, form):
1✔
109
        form.save()
1✔
110
        return HttpResponse()
1✔
111

112

113
class SequenceConditionBase(LoginRequiredMixin, ManagerPermMixin):
1✔
114
    template_name = "_condition_form.html"
1✔
115
    model = Condition
1✔
116
    form_class = ConditionForm
1✔
117
    new = True
1✔
118

119
    def get_form(self, form_class=None):
1✔
120
        """Return an instance of the form to be used in this view."""
121
        if self.sequence.is_onboarding:
1✔
122
            form_class = ConditionForm
1✔
123
        else:
124
            form_class = OffboardingConditionForm
×
125
        return form_class(**self.get_form_kwargs())
1✔
126

127
    def dispatch(self, *args, **kwargs):
1✔
128
        if self.request.user.is_authenticated:
1✔
129
            self.sequence = get_object_or_404(
1✔
130
                Sequence, pk=self.kwargs.get("sequence_pk", -1)
131
            )
132
        return super().dispatch(*args, **kwargs)
1✔
133

134
    def get_form_kwargs(self):
1✔
135
        kwargs = super().get_form_kwargs()
1✔
136
        kwargs["sequence"] = self.sequence
1✔
137
        return kwargs
1✔
138

139
    def form_valid(self, form):
1✔
140
        # add condition to sequence
141
        form.instance.sequence = self.sequence
1✔
142
        form.save()
1✔
143
        return HttpResponse(headers={"HX-Trigger": "reload-sequence"})
1✔
144

145
    def get_context_data(self, **kwargs):
1✔
146
        context = super().get_context_data(**kwargs)
1✔
147
        context["object"] = self.sequence
1✔
148
        context["condition_form"] = context["form"]
1✔
149
        context["todos"] = ToDo.templates.all().defer("content")
1✔
150
        return context
1✔
151

152

153
class SequenceConditionCreateView(SequenceConditionBase, CreateView):
1✔
154
    """
155
    Add a new condition block to the sequence.
156
    When valid, it will reload the sequence timeline to make sure everything is in
157
    the correct order.
158

159
    HTMX view
160
    """
161

162
    pass
1✔
163

164

165
class SequenceConditionUpdateView(SequenceConditionBase, UpdateView):
1✔
166
    """
167
    Update a condition block in the sequence.
168
    When valid, it will reload the sequence timeline to make sure everything is in
169
    the correct order.
170

171
    HTMX view
172
    """
173

174
    pass
1✔
175

176

177
class SendTestMessageView(LoginRequiredMixin, ManagerPermMixin, View):
1✔
178
    def post(self, request, template_pk, *args, **kwargs):
1✔
179
        external_message = get_object_or_404(ExternalMessage, pk=template_pk)
1✔
180
        external_message.person_type = ExternalMessage.PersonType.CUSTOM
1✔
181
        external_message.send_to = request.user
1✔
182
        external_message.execute(request.user)
1✔
183

184
        return HttpResponse()
1✔
185

186

187
class SequenceFormView(LoginRequiredMixin, ManagerPermMixin, View):
1✔
188
    """
189
    Get form when clicking on a line in a condition or dragging non-template, either
190
    empty or filled in form.
191

192
    HTMX view, this will only get called when the frontend requests a form.
193
    """
194

195
    def get(self, request, template_type, template_pk, *args, **kwargs):
1✔
196
        # Get a filled custom form based on integration config model
197
        if template_type == "integrationconfig":
1✔
198
            template_item = get_object_or_404(IntegrationConfig, id=template_pk)
1✔
199
            form = template_item.integration.config_form(template_item.additional_data)
1✔
200
            return render(
1✔
201
                request,
202
                "_item_form.html",
203
                {"form": form},
204
            )
205

206
        form = get_sequence_model_form(template_type)
1✔
207

208
        if form is None:
1✔
209
            raise Http404
1✔
210

211
        template_item = None
1✔
212
        # If template_pk is 0, then it shows an empty form
213
        if template_pk != 0:
1✔
214
            templates_model = get_sequence_templates_model(template_type)
1✔
215
            template_item = get_object_or_404(templates_model, id=template_pk)
1✔
216

217
        # Get a EMPTY custom form (depending on what provision) when it's an integration
218
        # config like Slack, Asana, Google...
219
        if form == IntegrationConfigForm:
1✔
220
            form = template_item.config_form()
1✔
221
            return render(
1✔
222
                request,
223
                "_item_form.html",
224
                {"form": form},
225
            )
226

227
        return render(
1✔
228
            request, "_item_form.html", {"form": form(instance=template_item)}
229
        )
230

231

232
class SequenceFormUpdateView(LoginRequiredMixin, ManagerPermMixin, View):
1✔
233
    """
234
    Update or create a specific line item (template or not) in a condition item (excl.
235
    Integration config)
236

237
    :params str template_type: i.e. todo, resource, introduction
238
    :params int template_pk: the pk of the used template (0 if none)
239
    :params int condition: the pk of the condition (can never be 0)
240

241
    HTMX view, this will only get called when the frontend requests to update an item.
242
    """
243

244
    def post(self, request, template_type, template_pk, condition, *args, **kwargs):
1✔
245
        # Get form, if it doesn't exist, then 404
246
        form = get_sequence_model_form(template_type)
1✔
247
        if form is None:
1✔
248
            raise Http404
1✔
249

250
        # Get template item if id was not 0. 0 means that it doesn't exist
251
        template_item = None
1✔
252
        if template_pk != 0:
1✔
253
            templates_model = get_sequence_templates_model(template_type)
1✔
254
            template_item = get_object_or_404(templates_model, id=template_pk)
1✔
255

256
        # Push instance and data through form and save it
257
        # Check if original item was template or doesn't exist (is new), if so, then
258
        # create new
259
        if template_item is None or (
1✔
260
            hasattr(template_item, "template") and template_item.template
261
        ):
262
            item_form = form(request.POST)
1✔
263
        else:
264
            item_form = form(instance=template_item, data=request.POST)
1✔
265

266
        if item_form.is_valid():
1✔
267
            obj = item_form.save()
1✔
268
            obj.template = False
1✔
269
            obj.save()
1✔
270

271
            # Check if new item has been created. If it has, then remove the old
272
            # record and add the new one. If it hasn't created a new object, then
273
            # the old one is good enough.
274
            if obj.id != template_pk:
1✔
275
                condition = get_object_or_404(Condition, id=condition)
1✔
276

277
                # This can probably be cleaned up, we can't use proxy object. We need
278
                # the base one
279
                if form in [
1✔
280
                    PendingEmailMessageForm,
281
                    PendingSlackMessageForm,
282
                    PendingTextMessageForm,
283
                ]:
284
                    obj = ExternalMessage.objects.get(id=obj.id)
×
285

286
                condition.add_item(obj)
1✔
287
                # Delete the old item, if there is one
288
                if template_item is not None:
1✔
289
                    condition.remove_item(template_item)
1✔
290

291
        else:
292
            # Form is not valid, push back form with errors
293
            return render(request, "_item_form.html", {"form": item_form})
1✔
294

295
        # Succesfully created/updated item, request sequence reload
296
        return HttpResponse(headers={"HX-Trigger": "reload-sequence"})
1✔
297

298

299
class SequenceFormUpdateIntegrationConfigView(
1✔
300
    LoginRequiredMixin, ManagerPermMixin, View
301
):
302
    """
303
    This will update or create an integration config object
304

305
    :params str template_type: always `integrationconfig`
306
    :params int template_pk: either of `Integration` or `IntegrationConfig` depending if
307
    object exists (see exists param)
308
    :params int condition: the pk of the condition (can never be 0)
309
    :params int exists: either 1 or 0 - basically boolean
310

311
    HTMX view, this will only get called when the frontend requests to update or create
312
    a integration config item.
313
    """
314

315
    def post(
1✔
316
        self, request, template_type, template_pk, condition, exists, *args, **kwargs
317
    ):
318
        condition = get_object_or_404(Condition, id=condition)
1✔
319
        if exists == 0:
1✔
320
            # If this provision item does not exist yet, then create one
321
            integration = get_object_or_404(Integration, id=template_pk)
1✔
322
            form_class = integration.config_form
1✔
323
            existing_item = None
1✔
324
        else:
325
            # If this provision item exist, then get it, so we can update it
326
            existing_item = get_object_or_404(IntegrationConfig, id=template_pk)
1✔
327
            form_class = existing_item.integration.config_form
1✔
328

329
        item_form = form_class(request.POST)
1✔
330

331
        # if form is not valid, push back form with errors
332
        if not item_form.is_valid():
1✔
333
            return render(request, "_item_form.html", {"form": item_form})
×
334

335
        # Either create a provision item or update it
336
        if existing_item is None:
1✔
337
            integration_config = IntegrationConfig.objects.create(
1✔
338
                integration=integration,
339
                additional_data=item_form.cleaned_data,
340
            )
341
            condition.add_item(integration_config)
1✔
342
        else:
343
            existing_item.additional_data = item_form.cleaned_data
1✔
344
            existing_item.save()
1✔
345

346
        # Succesfully created/updated item, reload the sequence
347
        return HttpResponse(headers={"HX-Trigger": "reload-sequence"})
1✔
348

349

350
class SequenceConditionItemView(LoginRequiredMixin, ManagerPermMixin, View):
1✔
351
    """
352
    This will delete or add a template item to a condition
353

354
    :params int pk: Condition pk
355
    :params string type: template type, i.e. todo, resource...
356
    :params int template_pk: the pk of object in the template type
357

358
    HTMX view, this will only get called when the frontend requests to add or delete
359
    a template to a sequence (drag/drop).
360
    """
361

362
    def delete(self, request, pk, type, template_pk, *args, **kwargs):
1✔
363
        condition = get_object_or_404(Condition, id=pk)
1✔
364
        templates_model = get_sequence_templates_model(type)
1✔
365
        template_item = get_object_or_404(templates_model, id=template_pk)
1✔
366
        condition.remove_item(template_item)
1✔
367
        condition = Condition.objects.prefetched().filter(id=condition.id).first()
1✔
368
        return render(
1✔
369
            request,
370
            "_sequence_condition.html",
371
            {"condition": condition, "object": condition.sequence},
372
        )
373

374
    def post(self, request, pk, type, template_pk, *args, **kwargs):
1✔
375
        condition = get_object_or_404(Condition, id=pk)
1✔
376
        templates_model = get_sequence_templates_model(type)
1✔
377
        template_item = get_object_or_404(templates_model, id=template_pk)
1✔
378
        condition.add_item(template_item)
1✔
379
        condition = Condition.objects.prefetched().filter(id=condition.id).first()
1✔
380
        return render(
1✔
381
            request,
382
            "_sequence_condition.html",
383
            {"condition": condition, "object": condition.sequence},
384
        )
385

386

387
class SequenceConditionDeleteView(LoginRequiredMixin, ManagerPermMixin, View):
1✔
388
    """
389
    Delete an entire condition
390

391
    :params int pk: Sequence pk
392
    :params int condition_pk: Condition pk
393

394
    HTMX view, the cross in a condition
395
    """
396

397
    def delete(self, request, pk, condition_pk, *args, **kwargs):
1✔
398
        sequence = get_object_or_404(Sequence, id=pk)
1✔
399
        condition = get_object_or_404(Condition, id=condition_pk, sequence=sequence)
1✔
400
        # Can never delete the unconditioned condition
401
        if condition.condition_type == Condition.Type.WITHOUT:
1✔
402
            raise Http404
1✔
403
        condition.delete()
1✔
404
        return HttpResponse()
1✔
405

406

407
class SequenceDeleteView(
1✔
408
    LoginRequiredMixin, ManagerPermMixin, SuccessMessageMixin, DeleteView
409
):
410
    """
411
    Delete an entire sequence
412

413
    :params int pk: Sequence pk
414
    """
415

416
    queryset = Sequence.objects.all()
1✔
417
    success_url = reverse_lazy("sequences:list")
1✔
418
    success_message = _("Sequence item has been removed")
1✔
419

420

421
class SequenceDefaultTemplatesView(LoginRequiredMixin, ManagerPermMixin, ListView):
1✔
422
    """
423
    Get a list of all available template items to drop in the sequence
424

425
    :params str type: the template type
426

427
    HTMX view, whenever clicked on any of the template icons on the right side
428
    of the screen
429
    """
430

431
    template_name = "_sequence_templates_list.html"
1✔
432

433
    def get_queryset(self):
1✔
434
        template_type = self.request.GET.get("type", "")
1✔
435
        if template_type == "integration":
1✔
436
            return Integration.objects.sequence_integration_options()
1✔
437

438
        if get_templates_model(template_type) is None:
1✔
439
            # if type does not exist, then return empty queryset
440
            return Sequence.objects.none()
1✔
441

442
        templates_model = get_templates_model(template_type)
1✔
443
        return templates_model.templates.all()
1✔
444

445
    def get_context_data(self, **kwargs):
1✔
446
        context = super().get_context_data(**kwargs)
1✔
447
        context["sequence"] = get_object_or_404(Sequence, id=self.kwargs.get("pk", -1))
1✔
448
        context["active"] = self.request.GET.get("type", "")
1✔
449
        return context
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