-
Notifications
You must be signed in to change notification settings - Fork 73
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3651 from teovin/bulk-org-user-creation
Bulk organization user creation/addition feature
- Loading branch information
Showing
13 changed files
with
470 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# Generated by Django 4.2.16 on 2024-11-20 18:37 | ||
|
||
from django.db import migrations | ||
|
||
FLAG_NAME = "bulk-organization-user-creation" | ||
|
||
def create_bulk_organization_user_creation_feature_flag(apps, schema_editor): | ||
Flag = apps.get_model("waffle", "Flag") | ||
flag = Flag( | ||
name=FLAG_NAME, | ||
testing=True | ||
) | ||
flag.save() | ||
|
||
def delete_bulk_organization_user_creation_feature_flag(apps, schema_editor): | ||
Flag = apps.get_model("waffle", "Flag") | ||
flags = Flag.objects.filter(name=FLAG_NAME) | ||
flags.delete() | ||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('perma', '0056_delete_historicallink'), | ||
('waffle', '0004_update_everyone_nullbooleanfield'), | ||
] | ||
|
||
operations = [ | ||
migrations.RunPython(create_bulk_organization_user_creation_feature_flag, delete_bulk_organization_user_creation_feature_flag), | ||
] |
2 changes: 1 addition & 1 deletion
2
perma_web/perma/templates/email/new_user_added_to_org_by_other.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
TITLE: A Perma.cc account has been created for you | ||
{% with form.cleaned_data.organizations.0 as org %} | ||
{% with form.cleaned_data.organizations as org %} | ||
A Perma.cc account has been created for you by {{ request.user.get_full_name }} on behalf of {{ org }}.{% endwith %} | ||
|
||
{% include 'email/includes/activation.txt' %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
perma_web/perma/templates/user_management/add_multiple_users_to_org.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
{% extends "admin-layout.html" %} | ||
|
||
{% block title %} | Add users to organization{% endblock %} | ||
|
||
{% block adminContent %} | ||
<h3 class="body-bh">Add multiple users to organization via CSV</h3> | ||
|
||
{% if not form %} | ||
<p>{{ error_message }}</p> | ||
{% else %} | ||
|
||
<form class="add-user" method="post" enctype="multipart/form-data"> | ||
{% csrf_token %} | ||
{% include "includes/fieldset.html" %} | ||
<button type="submit" class="btn">Add organization users</button> | ||
<a href="{% url 'user_management_manage_organization_user' %}" class="btn cancel">Cancel</a> | ||
</form> | ||
|
||
<script> | ||
const checkbox = document.getElementById("id_a-indefinite_affiliation"); | ||
const datetimeField = document.getElementById("id_a-expires_at"); | ||
const datetimeFieldLabel = document.querySelector('label[for="id_a-expires_at"]'); | ||
|
||
const toggleExpirationDateField = () => { | ||
const displayStyle = checkbox.checked ? "none" : "block"; | ||
datetimeField.style.display = displayStyle; | ||
datetimeFieldLabel.style.display = displayStyle; | ||
}; | ||
|
||
document.addEventListener("DOMContentLoaded", toggleExpirationDateField); | ||
checkbox.addEventListener("change", toggleExpirationDateField); | ||
</script> | ||
|
||
{% endif %} | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,12 +10,15 @@ | |
from django.http import HttpResponse, JsonResponse | ||
from django.urls import reverse | ||
from django.core import mail | ||
from django.core.files.uploadedfile import SimpleUploadedFile | ||
from django.conf import settings | ||
from django.db import IntegrityError | ||
from django.test import override_settings | ||
from django.test.client import RequestFactory | ||
|
||
from perma.models import LinkUser, Organization, Registrar, Sponsorship, UserOrganizationAffiliation | ||
from perma.tests.utils import PermaTestCase | ||
from perma.forms import MultipleUsersFormWithOrganization | ||
|
||
|
||
class UserManagementViewsTestCase(PermaTestCase): | ||
|
@@ -689,6 +692,80 @@ def add_org_user(self): | |
organizations=self.organization | ||
).exists()) | ||
|
||
def test_add_multiple_org_users_via_csv(self): | ||
def create_csv_file(filename, content): | ||
return SimpleUploadedFile(filename, content.encode('utf-8'), content_type='text/csv') | ||
|
||
def initialize_form(csv_file, data=None): | ||
data = {'organizations': selected_organization.pk, 'indefinite_affiliation': True} | ||
return MultipleUsersFormWithOrganization(request=request, data=data, files={'csv_file': csv_file}) | ||
|
||
# --- initialize data --- | ||
csv_data = 'first_name,last_name,email\nJohn,Doe,[email protected]\nJane,Smith,[email protected]' | ||
another_csv_data = 'first_name,last_name,email\nJohn2,Doe,[email protected]\nJane2,Smith,[email protected]' | ||
invalid_csv_data = 'name\nJohn Doe' | ||
another_invalid_csv_data = 'first_name,last_name,email\nJohn,Doe,\nJane,Smith,[email protected]' | ||
|
||
valid_csv_file = create_csv_file('users.csv', csv_data) | ||
another_valid_csv_file = create_csv_file('another_valid_users.csv', another_csv_data) | ||
one_more_valid_csv_file = create_csv_file('one_more_valid_users.csv', csv_data) | ||
invalid_csv_file = create_csv_file('invalid_users.csv', invalid_csv_data) | ||
another_invalid_csv_file = create_csv_file('another_invalid_users.csv', another_invalid_csv_data) | ||
|
||
request = RequestFactory().get('/') | ||
request.user = self.registrar_user | ||
selected_organization = self.another_organization | ||
|
||
# --- test form initialization --- | ||
form = MultipleUsersFormWithOrganization(request=request) | ||
# the registrar user has 3 organizations tied to it as verified in the users.json sample data | ||
self.assertEqual(form.fields['organizations'].queryset.count(), 3) | ||
# confirm that the first item in organization selection field matches the first organization of the registrar | ||
self.assertEqual(form.fields['organizations'].queryset.first(), request.user.registrar.organizations | ||
.order_by('name').first()) | ||
|
||
# --- test csv validation --- | ||
# valid csv | ||
form1 = initialize_form(valid_csv_file) | ||
self.assertTrue(form1.is_valid()) | ||
|
||
# invalid csv - missing headers | ||
form2 = initialize_form(invalid_csv_file) | ||
self.assertFalse(form2.is_valid()) | ||
self.assertTrue("CSV file must contain a header row with first_name, last_name and email columns." | ||
in form2.errors['csv_file']) | ||
|
||
# invalid csv - missing email field | ||
form3 = initialize_form(another_invalid_csv_file) | ||
self.assertFalse(form3.is_valid()) | ||
self.assertTrue("Each row in the CSV file must contain email." | ||
in form3.errors['csv_file']) | ||
|
||
# --- test user creation --- | ||
self.assertTrue(form1.is_valid()) | ||
form1.save(commit=True) | ||
created_user_ids = [user.id for user in form1.created_users.values()] | ||
self.assertEqual(len(created_user_ids), 2) | ||
self.assertEqual(UserOrganizationAffiliation.objects.filter(user_id__in=created_user_ids).count(), 2) | ||
|
||
# --- test user update --- | ||
existing_user = LinkUser.objects.create(email="[email protected]", first_name="John2", last_name="Doe") | ||
form4 = initialize_form(another_valid_csv_file) | ||
self.assertTrue(form4.is_valid()) | ||
form4.save(commit=True) | ||
self.assertEqual(len(form4.updated_users), 1) | ||
self.assertTrue(existing_user in form4.updated_users.values()) | ||
self.assertEqual(len(form4.created_users), 1) | ||
self.assertEqual(next(iter(form4.updated_users)), "[email protected]") | ||
|
||
# --- test validation errors --- | ||
LinkUser.objects.filter(raw_email="[email protected]").update(is_staff=True) | ||
form5 = initialize_form(one_more_valid_csv_file) | ||
self.assertTrue(form5.is_valid()) | ||
form5.save(commit=True) | ||
self.assertEqual(len(form5.ineligible_users), 1) | ||
self.assertEqual("[email protected]", next(iter(form5.ineligible_users))) | ||
|
||
def test_admin_user_can_add_new_user_to_org(self): | ||
self.log_in_user(self.admin_user) | ||
self.add_org_user() | ||
|
Oops, something went wrong.