Skip to content

Commit

Permalink
Merge branch 'master' into download-upload-files-through-integration
Browse files Browse the repository at this point in the history
  • Loading branch information
GDay authored Oct 14, 2023
2 parents 1d6453a + ee84ddd commit 24917b4
Show file tree
Hide file tree
Showing 36 changed files with 1,275 additions and 351 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ See details here: [ChiefOnboarding on Docker Hub](https://hub.docker.com/r/chief

[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/chiefonboarding/chiefonboarding)

**Elestio**

[![Deploy](https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png)](https://elest.io/open-source/chiefonboarding)

## Support
This software is provided under an open source license and comes as is. If you have any questions, then you will have to open an issue on Github for that. If you want guaranteed, quick support, then we offer a paid support package for that (best effort - generally under 2 hours response time). Please see our [pricing page](https://chiefonboarding.com/pricing) for more details.
Expand Down
355 changes: 192 additions & 163 deletions back/Pipfile.lock

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions back/admin/admin_tasks/migrations/0011_admintask_based_on.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.5 on 2023-09-22 00:24

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
dependencies = [
("sequences", "0041_condition_condition_admin_tasks_and_more"),
("admin_tasks", "0010_remove_admintask_slack_user_old"),
]

operations = [
migrations.AddField(
model_name="admintask",
name="based_on",
field=models.ForeignKey(
help_text="If generated through a sequence, then this will be filled",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="sequences.pendingadmintask",
),
),
]
44 changes: 40 additions & 4 deletions back/admin/admin_tasks/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ class Notification(models.IntegerChoices):
completed = models.BooleanField(verbose_name=_("Completed"), default=False)
date = models.DateField(verbose_name=_("Date"), blank=True, null=True)
priority = models.IntegerField(
verbose_name=_("Priority"), choices=Priority.choices, default=2
verbose_name=_("Priority"), choices=Priority.choices, default=Priority.MEDIUM
)
based_on = models.ForeignKey(
"sequences.PendingAdminTask",
null=True,
on_delete=models.SET_NULL,
help_text="If generated through a sequence, then this will be filled",
)

@property
Expand All @@ -68,10 +74,9 @@ def send_notification_third_party(self):
# Only happens when a sequence adds this with "manager" or "buddy" is
# choosen option
return
if self.option == 1:
# through email
if self.option == AdminTask.Notification.EMAIL:
send_email_notification_to_external_person(self)
elif self.option == 2:
elif self.option == AdminTask.Notification.SLACK:
blocks = [
paragraph(
_(
Expand Down Expand Up @@ -136,6 +141,37 @@ def send_notification_new_assigned(self):
else:
send_email_new_assigned_admin(self)

def mark_completed(self):
from admin.sequences.tasks import process_condition

self.completed = True
self.save()

# Get conditions with this to do item as (part of the) condition
conditions = self.new_hire.conditions.filter(
condition_admin_tasks=self.based_on
)

for condition in conditions:
condition_admin_tasks_id = condition.condition_admin_tasks.values_list(
"id", flat=True
)

# Check if all admin to do items have been added to new hire and are
# completed. If not, then we know it should not be triggered yet
completed_tasks = AdminTask.objects.filter(
based_on_id__in=condition_admin_tasks_id,
new_hire=self.new_hire,
completed=True,
)

# If the amount matches, then we should process it
if completed_tasks.count() == len(condition_admin_tasks_id):
# Send notification only if user has a slack account
process_condition(
condition.id, self.new_hire.id, self.new_hire.has_slack_account
)

class Meta:
ordering = ["completed", "date"]

Expand Down
16 changes: 7 additions & 9 deletions back/admin/admin_tasks/templates/admin_tasks_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
{% load i18n %}
{% load crispy_forms_tags %}
{% block actions %}
<form method="post" action="{% url 'admin_tasks:completed' object.id %}">
{% csrf_token %}
<button type="submit" class="btn btn-primary">
{% if object.completed %}
{% translate "Reopen" %}
{% else %}
{% if not object.completed %}
<form method="post" action="{% url 'admin_tasks:completed' object.id %}">
{% csrf_token %}
<button type="submit" class="btn btn-primary">
{% translate "Complete" %}
{% endif %}
</button>
</form>
</button>
</form>
{% endif %}
{% endblock %}

{% block content %}
Expand Down
63 changes: 53 additions & 10 deletions back/admin/admin_tasks/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,16 +301,8 @@ def test_complete_admin_task(client, admin_factory, admin_task_factory):
assert "disabled" in response.content.decode()
# Cannot add new comment
assert "div_id_content" not in response.content.decode()
# Complete url is still there to make it open again
assert complete_url in response.content.decode()

assert task1.completed
assert not task2.completed

url = reverse("admin_tasks:mine")
response = client.get(url)
# Check button is now visible
assert "btn-success" in response.content.decode()
# Complete url is gone
assert complete_url not in response.content.decode()


@pytest.mark.django_db
Expand Down Expand Up @@ -416,3 +408,54 @@ def test_admin_task_comment_on_not_owned_task_slack_message(
],
},
]


@pytest.mark.django_db
def test_complete_admin_task_trigger_condition(
client,
admin_factory,
sequence_factory,
condition_admin_task_factory,
pending_admin_task_factory,
new_hire_factory,
):
admin = admin_factory()
client.force_login(admin)

task_to_complete1 = pending_admin_task_factory(assigned_to=admin)
task_to_complete2 = pending_admin_task_factory(assigned_to=admin)

# add tasks to sequence to be added to new hire directly
sequence = sequence_factory()
unconditioned_condition = sequence.conditions.first()
unconditioned_condition.admin_tasks.add(task_to_complete1, task_to_complete2)

# set up condition when both tasks are completed to create a third one
task_to_be_created = pending_admin_task_factory()
admin_task_condition = condition_admin_task_factory()
admin_task_condition.condition_admin_tasks.set(
[task_to_complete1, task_to_complete2]
)
admin_task_condition.admin_tasks.add(task_to_be_created)
sequence.conditions.add(admin_task_condition)

new_hire = new_hire_factory()

new_hire.add_sequences([sequence])

assert new_hire.conditions.count() == 1

# new hire has now two admin tasks
assert AdminTask.objects.filter(new_hire=new_hire).count() == 2

# first task gets completed
AdminTask.objects.get(based_on=task_to_complete1).mark_completed()

# still two tasks
assert AdminTask.objects.filter(new_hire=new_hire).count() == 2

# second task gets completed
AdminTask.objects.get(based_on=task_to_complete2).mark_completed()

# we now have 3 tasks
assert AdminTask.objects.filter(new_hire=new_hire).count() == 3
2 changes: 1 addition & 1 deletion back/admin/admin_tasks/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
path("all/", views.AllAdminTasksListView.as_view(), name="all"),
path("<int:pk>/", views.AdminTasksUpdateView.as_view(), name="detail"),
path(
"<int:pk>/completed/", views.AdminTaskToggleDoneView.as_view(), name="completed"
"<int:pk>/completed/", views.AdminTaskCompleteView.as_view(), name="completed"
),
path(
"<int:pk>/comment/", views.AdminTasksCommentCreateView.as_view(), name="comment"
Expand Down
5 changes: 2 additions & 3 deletions back/admin/admin_tasks/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,12 @@ def get_context_data(self, **kwargs):
return context


class AdminTaskToggleDoneView(LoginRequiredMixin, ManagerPermMixin, BaseDetailView):
class AdminTaskCompleteView(LoginRequiredMixin, ManagerPermMixin, BaseDetailView):
model = AdminTask

def post(self, request, *args, **kwargs):
admin_task = self.get_object()
admin_task.completed = not admin_task.completed
admin_task.save()
admin_task.mark_completed()
return redirect("admin_tasks:detail", pk=admin_task.id)


Expand Down
2 changes: 1 addition & 1 deletion back/admin/integrations/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class GettingUsersError(Exception):
class FailedPaginatedResponseError(Exception):
pass


Expand Down
4 changes: 1 addition & 3 deletions back/admin/integrations/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,8 @@ class CustomIntegrationFactory(IntegrationFactory):

class CustomUserImportIntegrationFactory(IntegrationFactory):
integration = Integration.Type.CUSTOM
manifest_type = Integration.ManifestType.USER_IMPORT
manifest_type = Integration.ManifestType.SYNC_USERS
manifest = {
"form": [],
"type": "import_users",
"execute": [
{
"url": "http://localhost/api/gateway.php/{{COMPANY_ID}}/v1/reports/{{REPORT_ID}}",
Expand Down
12 changes: 3 additions & 9 deletions back/admin/integrations/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
from crispy_forms.helper import FormHelper
from django import forms
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError

from admin.integrations.utils import get_value_from_notation

from .models import Integration
from .serializers import ManifestSerializer


class IntegrationConfigForm(forms.ModelForm):
Expand Down Expand Up @@ -131,13 +129,9 @@ class Meta:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["manifest_type"].required = True

def clean_manifest(self):
manifest = self.cleaned_data["manifest"]
manifest_serializer = ManifestSerializer(data=manifest)
if not manifest_serializer.is_valid():
raise ValidationError(json.dumps(manifest_serializer.errors))
return manifest
if self.instance.id:
# disable manifest_type when updating field
self.fields["manifest_type"].disabled = True


class IntegrationExtraArgsForm(forms.ModelForm):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.5 on 2023-10-10 00:21

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("integrations", "0020_integration_manifest_type"),
]

operations = [
migrations.AlterField(
model_name="integration",
name="manifest_type",
field=models.IntegerField(
blank=True,
choices=[
(0, "Provision user accounts or trigger webhooks"),
(1, "Sync users"),
],
null=True,
),
),
]
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
from admin.integrations.exceptions import KeyIsNotInDataError
from django.contrib.auth import get_user_model
from organization.models import Organization
from admin.integrations.exceptions import (
KeyIsNotInDataError,
FailedPaginatedResponseError,
)

from admin.integrations.utils import get_value_from_notation
from django.utils.translation import gettext_lazy as _
from admin.integrations.exceptions import GettingUsersError

import logging

class ImportUser:
logger = logging.getLogger(__name__)


class PaginatedResponse:
"""
Extension of the `Integration` model. This part is only used to get users
Extension of the `Integration` model. Generic mixin used for extracting data
from a third party API endpoint and format them in a way that we can proccess
them.
"""

def __init__(self, integration):
self.integration = integration

def extract_users_from_list_response(self, response):
def extract_data_from_list_response(self, response):
# Building list of users from response. Dig into response to get to the users.
data_from = self.integration.manifest["data_from"]

Expand All @@ -43,7 +47,7 @@ def extract_users_from_list_response(self, response):
except KeyError:
# This is unlikely to go wrong - only when api changes or when
# configs are being setup
raise KeyIsNotInDataError(
logger.info(
_("Notation '%(notation)s' not in %(response)s")
% {
"notation": notation,
Expand Down Expand Up @@ -88,12 +92,14 @@ def get_next_page(self, response):
self.integration.params["NEXT_PAGE_TOKEN"] = token
return self.integration._replace_vars(next_page)

def get_import_user_candidates(self, user):
success, response = self.integration.execute(user, {})
def get_data_from_paginated_response(self):
success, response = self.integration.execute()
if not success:
raise GettingUsersError(self.integration.clean_response(response))
raise FailedPaginatedResponseError(
self.integration.clean_response(response)
)

users = self.extract_users_from_list_response(response)
users = self.extract_data_from_list_response(response)

amount_pages_to_fetch = self.integration.manifest.get(
"amount_pages_to_fetch", 5
Expand All @@ -109,7 +115,7 @@ def get_import_user_candidates(self, user):
{"method": "GET", "url": next_page_url}
)
if not success:
raise GettingUsersError(
raise FailedPaginatedResponseError(
_("Paginated URL fetch: %(response)s")
% {"response": self.integration.clean_response(response)}
)
Expand All @@ -121,22 +127,7 @@ def get_import_user_candidates(self, user):
except KeyError:
break

users += self.extract_users_from_list_response(response)
users += self.extract_data_from_list_response(response)
fetched_pages += 1

# Remove users that are already in the system or have been ignored
existing_user_emails = list(
get_user_model().objects.all().values_list("email", flat=True)
)
ignored_user_emails = Organization.objects.get().ignored_user_emails
excluded_emails = (
existing_user_emails + ignored_user_emails + ["", None]
) # also add blank emails to ignore

user_candidates = [
user_data
for user_data in users
if user_data.get("email", "") not in excluded_emails
]

return user_candidates
return users
Loading

0 comments on commit 24917b4

Please sign in to comment.