diff --git a/coldfront/core/project/forms_/new_project_forms/request_forms.py b/coldfront/core/project/forms_/new_project_forms/request_forms.py index ddccb697bc..1039354c49 100644 --- a/coldfront/core/project/forms_/new_project_forms/request_forms.py +++ b/coldfront/core/project/forms_/new_project_forms/request_forms.py @@ -859,3 +859,145 @@ def clean_name(self): raise forms.ValidationError( f'A project with name {name} already exists.') return name + +# ============================================================================= +# STANDALONE CLUSTERS +# ============================================================================= + +class StandaloneClusterDetailsForm(forms.Form): + name = forms.CharField( + help_text=( + 'The unique name of the standalone cluster, which must contain only ' + 'lowercase letters and numbers.'), + label='Name', + max_length=12, + required=True, + validators=[ + MinLengthValidator(4), + RegexValidator( + r'^[0-9a-z]+$', + message=( + 'Name must contain only lowercase letters and numbers.')) + ]) + description = forms.CharField( + help_text='A few sentences describing your standalone cluster.', + label='Description', + validators=[MinLengthValidator(10)], + widget=forms.Textarea(attrs={'rows': 3})) + + def clean_name(self): + cleaned_data = super().clean() + name = cleaned_data['name'].lower() + if Project.objects.filter(name=name): + raise forms.ValidationError( + f'A project with name {name} already exists.') + return name + +class StandaloneClusterExistingPIForm(forms.Form): + PI = PIChoiceField( + label='Principal Investigator', + queryset=User.objects.none(), + required=False, + widget=DisabledChoicesSelectWidget()) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.disable_pi_choices() + self.exclude_pi_choices() + + def clean(self): + cleaned_data = super().clean() + pi = self.cleaned_data['PI'] + if pi is not None and pi not in self.fields['PI'].queryset: + raise forms.ValidationError(f'Invalid selection {pi.username}.') + return cleaned_data + + def disable_pi_choices(self): + """Prevent certain Users, who should be displayed, from being + selected as PIs.""" + disable_user_pks = set() + + if flag_enabled('LRC_ONLY'): + # On LRC, PIs must be LBL employees. + non_lbl_employees = set( + [user.pk for user in User.objects.all() + if not is_lbl_employee(user)]) + disable_user_pks.update(non_lbl_employees) + + self.fields['PI'].widget.disabled_choices = disable_user_pks + + def exclude_pi_choices(self): + """Exclude certain Users from being displayed as PI options.""" + # Exclude any user that does not have an email address or is inactive. + self.fields['PI'].queryset = User.objects.exclude( + Q(email__isnull=True) | Q(email__exact='') | Q(is_active=False)) + +class StandaloneClusterNewPIForm(forms.Form): + first_name = forms.CharField(max_length=30, required=True) + middle_name = forms.CharField(max_length=30, required=False) + last_name = forms.CharField(max_length=150, required=True) + email = forms.EmailField(max_length=100, required=True) + + def clean_email(self): + cleaned_data = super().clean() + email = cleaned_data['email'].lower() + if (User.objects.filter(username=email).exists() or + User.objects.filter(email=email).exists()): + raise forms.ValidationError( + 'A user with that email address already exists.') + + if flag_enabled('LRC_ONLY'): + if not email.endswith('@lbl.gov'): + raise forms.ValidationError( + 'New PI must be an LBL employee with an LBL email.') + + return email + +class StandaloneClusterExistingManagerForm(forms.Form): + manager = PIChoiceField( + label='Manager', + queryset=User.objects.none(), + required=False, + widget=DisabledChoicesSelectWidget()) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.exclude_manager_choices() + + def clean(self): + cleaned_data = super().clean() + pi = self.cleaned_data['manager'] + if pi is not None and pi not in self.fields['manager'].queryset: + raise forms.ValidationError(f'Invalid selection {pi.username}.') + return cleaned_data + + def exclude_manager_choices(self): + """Exclude certain Users from being displayed as PI options.""" + # Exclude any user that does not have an email address or is inactive. + self.fields['manager'].queryset = User.objects.exclude( + Q(email__isnull=True) | Q(email__exact='') | Q(is_active=False)) + +class StandaloneClusterNewManagerForm(forms.Form): + first_name = forms.CharField(max_length=30, required=True) + middle_name = forms.CharField(max_length=30, required=False) + last_name = forms.CharField(max_length=150, required=True) + email = forms.EmailField(max_length=100, required=True) + + def clean_email(self): + cleaned_data = super().clean() + email = cleaned_data['email'].lower() + if (User.objects.filter(username=email).exists() or + User.objects.filter(email=email).exists()): + raise forms.ValidationError( + 'A user with that email address already exists.') + + return email + +class StandaloneClusterReviewAndSubmitForm(forms.Form): + + confirmation = forms.BooleanField( + label=( + 'I have reviewed my selections and understand the changes ' + 'described above. Submit my request.'), + required=True) + \ No newline at end of file diff --git a/coldfront/core/project/management/commands/projects.py b/coldfront/core/project/management/commands/projects.py index d8c9eac7b1..aba0b87ddf 100644 --- a/coldfront/core/project/management/commands/projects.py +++ b/coldfront/core/project/management/commands/projects.py @@ -6,21 +6,16 @@ from flags.state import flag_enabled -from coldfront.core.allocation.models import Allocation from coldfront.core.allocation.models import AllocationPeriod -from coldfront.core.allocation.models import AllocationAttribute -from coldfront.core.allocation.models import AllocationAttributeType -from coldfront.core.allocation.models import AllocationStatusChoice from coldfront.core.allocation.models import AllocationRenewalRequest from coldfront.core.allocation.models import AllocationRenewalRequestStatusChoice from coldfront.core.allocation.utils import calculate_service_units_to_allocate -from coldfront.core.resource.utils_.allowance_utils.interface import ComputingAllowanceInterface from coldfront.core.project.models import Project -from coldfront.core.project.models import ProjectStatusChoice from coldfront.core.project.models import ProjectUser from coldfront.core.project.models import ProjectUserRoleChoice from coldfront.core.project.models import ProjectUserStatusChoice from coldfront.core.project.utils import is_primary_cluster_project +from coldfront.core.project.utils_.new_project_utils import create_project_with_compute_allocation from coldfront.core.project.utils_.new_project_user_utils import NewProjectUserRunnerFactory from coldfront.core.project.utils_.new_project_user_utils import NewProjectUserSource from coldfront.core.project.utils_.renewal_utils import AllocationRenewalApprovalRunner @@ -29,9 +24,8 @@ from coldfront.core.resource.models import Resource from coldfront.core.resource.utils import get_primary_compute_resource_name from coldfront.core.resource.utils_.allowance_utils.computing_allowance import ComputingAllowance -from coldfront.core.statistics.models import ProjectTransaction +from coldfront.core.resource.utils_.allowance_utils.interface import ComputingAllowanceInterface from coldfront.core.utils.common import add_argparse_dry_run_argument -from coldfront.core.utils.common import display_time_zone_current_date from coldfront.core.utils.common import utc_now_offset_aware from coldfront.core.utils.email.email_strategy import DropEmailStrategy @@ -141,10 +135,9 @@ def _create_project_with_compute_allocation_and_pis(project_name, specified. """ with transaction.atomic(): - project = Project.objects.create( - name=project_name, - title=project_name, - status=ProjectStatusChoice.objects.get(name='Active')) + project = create_project_with_compute_allocation(project_name, + compute_resource, + settings.ALLOCATION_MAX) project_users = [] for pi_user in pi_users: @@ -156,25 +149,6 @@ def _create_project_with_compute_allocation_and_pis(project_name, status=ProjectUserStatusChoice.objects.get(name='Active')) project_users.append(project_user) - allocation = Allocation.objects.create( - project=project, - status=AllocationStatusChoice.objects.get(name='Active'), - start_date=display_time_zone_current_date(), - end_date=None) - allocation.resources.add(compute_resource) - - num_service_units = settings.ALLOCATION_MAX - AllocationAttribute.objects.create( - allocation_attribute_type=AllocationAttributeType.objects.get( - name='Service Units'), - allocation=allocation, - value=str(num_service_units)) - - ProjectTransaction.objects.create( - project=project, - date_time=utc_now_offset_aware(), - allocation=num_service_units) - runner_factory = NewProjectUserRunnerFactory() for project_user in project_users: runner = runner_factory.get_runner( diff --git a/coldfront/core/project/templates/project/project_request/standalone_cluster/project_details.html b/coldfront/core/project/templates/project/project_request/standalone_cluster/project_details.html new file mode 100644 index 0000000000..47690bae49 --- /dev/null +++ b/coldfront/core/project/templates/project/project_request/standalone_cluster/project_details.html @@ -0,0 +1,60 @@ +{% extends "common/base.html" %} +{% load crispy_forms_tags %} +{% load static %} + + +{% block title %} +New Standalone Cluster - Project Details +{% endblock %} + + +{% block head %} +{{ wizard.form.media }} +{% endblock %} + + +{% block content %} + + +
You are creating a new standalone cluster. Please provide the following details:
+ + +Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}
+ +{% endblock %} diff --git a/coldfront/core/project/templates/project/project_request/standalone_cluster/project_existing_manager.html b/coldfront/core/project/templates/project/project_request/standalone_cluster/project_existing_manager.html new file mode 100644 index 0000000000..4da281f39e --- /dev/null +++ b/coldfront/core/project/templates/project/project_request/standalone_cluster/project_existing_manager.html @@ -0,0 +1,70 @@ +{% extends "common/base.html" %} +{% load feature_flags %} +{% load crispy_forms_tags %} +{% load static %} + + +{% block title %} +New Standalone Cluster - Existing Manager +{% endblock %} + + +{% block head %} +{{ wizard.form.media }} +{% endblock %} + + +{% block content %} + + + + +Select an existing user to be a Manager of the project. You may search for the user in the selection field. If the desired user is not listed, you may skip this step and specify information for a new Manager in the next step.
+ + +Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}
+ + + +{% endblock %} diff --git a/coldfront/core/project/templates/project/project_request/standalone_cluster/project_existing_pi.html b/coldfront/core/project/templates/project/project_request/standalone_cluster/project_existing_pi.html new file mode 100644 index 0000000000..697ef8deb1 --- /dev/null +++ b/coldfront/core/project/templates/project/project_request/standalone_cluster/project_existing_pi.html @@ -0,0 +1,78 @@ +{% extends "common/base.html" %} +{% load feature_flags %} +{% load crispy_forms_tags %} +{% load static %} + + +{% block title %} +New Standalone Cluster - Existing PI +{% endblock %} + + +{% block head %} +{{ wizard.form.media }} +{% endblock %} + + +{% block content %} + + + + +Select an existing user to be a Principal Investigator of the project. You may search for the user in the selection field. If the desired user is not listed, you may skip this step and specify information for a new PI in the next step.
+{% flag_enabled 'LRC_ONLY' as lrc_only %} +{% if lrc_only %} +Note: Only LBL employees, users with an "@lbl.gov" email, can be selected as a PI.
+{% endif %} + +{% if allowance_is_one_per_pi %} +Note: Each PI may only have one {{ computing_allowance.name }} at a time, so any that have pending requests or active allocations are not selectable.
+{% endif %} + + +Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}
+ + + +{% endblock %} diff --git a/coldfront/core/project/templates/project/project_request/standalone_cluster/project_new_manager.html b/coldfront/core/project/templates/project/project_request/standalone_cluster/project_new_manager.html new file mode 100644 index 0000000000..8f993e9d93 --- /dev/null +++ b/coldfront/core/project/templates/project/project_request/standalone_cluster/project_new_manager.html @@ -0,0 +1,60 @@ +{% extends "common/base.html" %} +{% load feature_flags %} +{% load crispy_forms_tags %} +{% load static %} + + +{% block title %} +New Standalone Cluster - New Manager +{% endblock %} + + +{% block head %} +{{ wizard.form.media }} +{% endblock %} + +{% block content %} + +Provide details about a new user, who will be a Manager for the project. An existing user should have been selected in the previous step.
+ + +Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}
+ +{% endblock %} diff --git a/coldfront/core/project/templates/project/project_request/standalone_cluster/project_new_pi.html b/coldfront/core/project/templates/project/project_request/standalone_cluster/project_new_pi.html new file mode 100644 index 0000000000..5f49b5ed0d --- /dev/null +++ b/coldfront/core/project/templates/project/project_request/standalone_cluster/project_new_pi.html @@ -0,0 +1,65 @@ +{% extends "common/base.html" %} +{% load feature_flags %} +{% load crispy_forms_tags %} +{% load static %} + + +{% block title %} +New Standalone Cluster - New PI +{% endblock %} + + +{% block head %} +{{ wizard.form.media }} +{% endblock %} + +{% block content %} + +Provide details about a new user, who will be a Principal Investigator for the project. An existing user should have been selected in the previous step.
+ +{% flag_enabled 'LRC_ONLY' as lrc_only %} +{% if lrc_only %} +Note: Only LBL employees, users with an "@lbl.gov" email, can be a PI.
+{% endif %} + + +Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}
+ +{% endblock %} diff --git a/coldfront/core/project/templates/project/project_request/standalone_cluster/review_and_submit.html b/coldfront/core/project/templates/project/project_request/standalone_cluster/review_and_submit.html new file mode 100644 index 0000000000..390f17440d --- /dev/null +++ b/coldfront/core/project/templates/project/project_request/standalone_cluster/review_and_submit.html @@ -0,0 +1,93 @@ +{% extends "common/base.html" %} +{% load crispy_forms_tags %} +{% load static %} + + +{% block title %} +Project Renewal - Review and Submit +{% endblock %} + + +{% block head %} +{{ wizard.form.media }} +{% endblock %} + + +{% block content %} + ++ Review your selections and submit. +
+ +| Standalone Cluster Name | ++ {{ breadcrumb_project }} + | +
|---|---|
| Principal Investigator (PI) | ++ {{ pi }} + | +
| Manager | +{{ manager }} | +
Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}
+ +{% endblock %} diff --git a/coldfront/core/project/urls.py b/coldfront/core/project/urls.py index 14cb672df4..4ea4416bfe 100644 --- a/coldfront/core/project/urls.py +++ b/coldfront/core/project/urls.py @@ -66,6 +66,11 @@ condition_dict=new_project_request_views.SavioProjectRequestWizard.condition_dict(), ), name='new-project-request'), + path('new-standalone-cluster-request/', + new_project_request_views.StandaloneClusterRequestWizard.as_view( + condition_dict=new_project_request_views.StandaloneClusterRequestWizard.condition_dict(), + ), + name='new-standalone-cluster-request'), path('new-project-pending-request-list/', new_project_approval_views.SavioProjectRequestListView.as_view( completed=False), diff --git a/coldfront/core/project/utils_/new_project_utils.py b/coldfront/core/project/utils_/new_project_utils.py index ffcf8fdd14..563c797a8e 100644 --- a/coldfront/core/project/utils_/new_project_utils.py +++ b/coldfront/core/project/utils_/new_project_utils.py @@ -1,10 +1,12 @@ from coldfront.api.statistics.utils import set_project_user_allocation_value +from coldfront.core.allocation.models import Allocation from coldfront.core.allocation.models import AllocationAttribute from coldfront.core.allocation.models import AllocationAttributeType from coldfront.core.allocation.models import AllocationPeriod from coldfront.core.allocation.models import AllocationStatusChoice from coldfront.core.allocation.utils import get_project_compute_allocation from coldfront.core.project.models import ProjectAllocationRequestStatusChoice +from coldfront.core.project.models import Project from coldfront.core.project.models import ProjectUser from coldfront.core.project.models import ProjectStatusChoice from coldfront.core.project.models import SavioProjectAllocationRequest @@ -733,3 +735,44 @@ def vector_request_state_status(vector_request): # finally activated. return ProjectAllocationRequestStatusChoice.objects.get( name='Approved - Processing') + +def create_project_with_compute_allocation(project_name, + compute_resource, + num_service_units): + """Create a Project with the given name, with an Allocation to + the given compute Resource with given number of service units. + Return the Project. + + Some fields are set by default: + - The Project's status is 'Active'. + - The Allocation's status is 'Active'. + - The Allocation's start_date is today. + - The Allocation's end_date is None. + + TODO: When the command is generalized, allow these to be + specified. + """ + project = Project.objects.create( + name=project_name, + title=project_name, + status=ProjectStatusChoice.objects.get(name='Active')) + + allocation = Allocation.objects.create( + project=project, + status=AllocationStatusChoice.objects.get(name='Active'), + start_date=display_time_zone_current_date(), + end_date=None) + allocation.resources.add(compute_resource) + + AllocationAttribute.objects.create( + allocation_attribute_type=AllocationAttributeType.objects.get( + name='Service Units'), + allocation=allocation, + value=str(num_service_units)) + + ProjectTransaction.objects.create( + project=project, + date_time=utc_now_offset_aware(), + allocation=num_service_units) + + return project diff --git a/coldfront/core/project/views_/new_project_views/request_views.py b/coldfront/core/project/views_/new_project_views/request_views.py index 2a8d41d12a..87214e8567 100644 --- a/coldfront/core/project/views_/new_project_views/request_views.py +++ b/coldfront/core/project/views_/new_project_views/request_views.py @@ -15,7 +15,13 @@ from coldfront.core.project.forms_.new_project_forms.request_forms import SavioProjectRechargeExtraFieldsForm from coldfront.core.project.forms_.new_project_forms.request_forms import SavioProjectSurveyForm from coldfront.core.project.forms_.new_project_forms.request_forms import VectorProjectDetailsForm -from coldfront.core.project.models import Project +from coldfront.core.project.forms_.new_project_forms.request_forms import StandaloneClusterDetailsForm +from coldfront.core.project.forms_.new_project_forms.request_forms import StandaloneClusterExistingPIForm +from coldfront.core.project.forms_.new_project_forms.request_forms import StandaloneClusterNewPIForm +from coldfront.core.project.forms_.new_project_forms.request_forms import StandaloneClusterExistingManagerForm +from coldfront.core.project.forms_.new_project_forms.request_forms import StandaloneClusterNewManagerForm +from coldfront.core.project.forms_.new_project_forms.request_forms import StandaloneClusterReviewAndSubmitForm +from coldfront.core.project.models import Project, ProjectUser, ProjectUserRoleChoice, ProjectUserStatusChoice from coldfront.core.project.models import ProjectAllocationRequestStatusChoice from coldfront.core.project.models import ProjectStatusChoice from coldfront.core.project.models import SavioProjectAllocationRequest @@ -26,7 +32,11 @@ from coldfront.core.project.models import VectorProjectAllocationRequest from coldfront.core.project.utils_.new_project_utils import send_new_project_request_admin_notification_email from coldfront.core.project.utils_.new_project_utils import send_new_project_request_pi_notification_email +from coldfront.core.project.utils_.new_project_utils import create_project_with_compute_allocation +from coldfront.core.project.utils_.new_project_user_utils import NewProjectUserRunnerFactory +from coldfront.core.project.utils_.new_project_user_utils import NewProjectUserSource from coldfront.core.resource.models import Resource +from coldfront.core.resource.models import ResourceType from coldfront.core.resource.utils import get_primary_compute_resource from coldfront.core.resource.utils_.allowance_utils.computing_allowance import ComputingAllowance from coldfront.core.resource.utils_.allowance_utils.interface import ComputingAllowanceInterface @@ -34,6 +44,7 @@ from coldfront.core.user.utils import access_agreement_signed from coldfront.core.utils.common import session_wizard_all_form_data from coldfront.core.utils.common import utc_now_offset_aware +from coldfront.core.utils.email.email_strategy import DropEmailStrategy from django.conf import settings from django.contrib import messages @@ -740,3 +751,320 @@ def __handle_create_new_project(self, data): allocation.save() return project + +# ============================================================================= +# STANDALONE CLUSTER +# ============================================================================= + + +class StandaloneClusterRequestWizard(LoginRequiredMixin, UserPassesTestMixin, + SessionWizardView): + FORMS = [ + + ('details', StandaloneClusterDetailsForm), + ('existing_pi', StandaloneClusterExistingPIForm), + ('new_pi', StandaloneClusterNewPIForm), + ('existing_manager', StandaloneClusterExistingManagerForm), + ('new_manager', StandaloneClusterNewManagerForm), + ('review_and_submit', StandaloneClusterReviewAndSubmitForm), + ] + + TEMPLATES = { + 'details': 'project/project_request/standalone_cluster/project_details.html', + 'existing_pi': + 'project/project_request/standalone_cluster/project_existing_pi.html', + 'new_pi': + 'project/project_request/standalone_cluster/project_new_pi.html', + 'existing_manager': + 'project/project_request/standalone_cluster/project_existing_manager.html', + 'new_manager': + 'project/project_request/standalone_cluster/project_new_manager.html', + 'review_and_submit': + 'project/project_request/standalone_cluster/review_and_submit.html', + } + + form_list = [ + StandaloneClusterDetailsForm, + StandaloneClusterExistingPIForm, + StandaloneClusterNewPIForm, + StandaloneClusterExistingManagerForm, + StandaloneClusterNewManagerForm, + StandaloneClusterReviewAndSubmitForm, + ] + + logger = logging.getLogger(__name__) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Define a lookup table from form name to step number. + self.step_numbers_by_form_name = { + name: i for i, (name, _) in enumerate(self.FORMS)} + + def test_func(self): + if self.request.user.is_superuser: + return True + message = ( + 'You do not have permission to create a standalone cluster.') + messages.error(self.request, message) + + def get_context_data(self, form, **kwargs): + context = super().get_context_data(form=form, **kwargs) + current_step = int(self.steps.current) + self.__set_data_from_previous_steps(current_step, context) + return context + + def get_template_names(self): + return [self.TEMPLATES[self.FORMS[int(self.steps.current)][0]]] + + def done(self, form_list, **kwargs): + """Perform processing and store information in a request + object.""" + redirect_url = '/' + try: + form_data = session_wizard_all_form_data( + form_list, kwargs['form_dict'], len(self.form_list)) + with transaction.atomic(): + pi = self.__handle_pi_data(form_data) + manager = self.__handle_manager_data(form_data) + project = self.__handle_create_new_standalone_cluster( + form_data, pi, manager) + redirect_url = '/project/' + str(project.pk) + '/' + except Exception as e: + self.logger.exception(e) + message = 'Unexpected failure. Please contact an administrator.' + messages.error(self.request, message) + else: + # TODO: More informative message? + message = ( + 'Thank you for your submission. It will be reviewed and ' + 'processed by administrators.') + messages.success(self.request, message) + + return HttpResponseRedirect(redirect_url) + + @staticmethod + def condition_dict(): + view = StandaloneClusterRequestWizard + return { + '2': view.show_new_pi_form_condition, + '4': view.show_new_manager_form_condition, + } + + def show_new_pi_form_condition(self): + step_name = 'existing_pi' + step = str(self.step_numbers_by_form_name[step_name]) + cleaned_data = self.get_cleaned_data_for_step(step) or {} + return cleaned_data.get('PI', None) is None + + def show_new_manager_form_condition(self): + step_name = 'existing_manager' + step = str(self.step_numbers_by_form_name[step_name]) + cleaned_data = self.get_cleaned_data_for_step(step) or {} + return not cleaned_data.get('manager', False) + + def __handle_pi_data(self, form_data): + """Return the requested PI. If the PI did not exist, create a + new User and UserProfile.""" + # If an existing PI was selected, return the existing User object. + step_number = self.step_numbers_by_form_name['existing_pi'] + data = form_data[step_number] + if data['PI']: + return data['PI'] + + # Create a new User object intended to be a new PI. + step_number = self.step_numbers_by_form_name['new_pi'] + data = form_data[step_number] + email = data['email'] + try: + pi = User.objects.create( + username=email, + first_name=data['first_name'], + last_name=data['last_name'], + email=email, + is_active=True) + except IntegrityError as e: + self.logger.error(f'User {email} unexpectedly exists.') + raise e + + # Set the user's middle name in the UserProfile; generate a PI request. + try: + pi_profile = pi.userprofile + except UserProfile.DoesNotExist as e: + self.logger.error( + f'User {email} unexpectedly has no UserProfile.') + raise e + pi_profile.middle_name = data['middle_name'] + pi_profile.upgrade_request = utc_now_offset_aware() + pi_profile.save() + + # Create an unverified, primary EmailAddress for the new User object. + try: + EmailAddress.objects.create( + user=pi, + email=email, + verified=False, + primary=True) + except IntegrityError as e: + self.logger.error( + f'EmailAddress {email} unexpectedly already exists.') + raise e + + return pi + + def __handle_manager_data(self, form_data): + """Return the requested manager. If the manager did not exist, create a + new User and UserProfile.""" + # If an existing manager was selected, return the existing User object. + step_number = self.step_numbers_by_form_name['existing_manager'] + data = form_data[step_number] + if data['manager']: + return data['manager'] + + # Create a new User object intended to be a new manager. + step_number = self.step_numbers_by_form_name['new_manager'] + data = form_data[step_number] + email = data['email'] + try: + manager = User.objects.create( + username=email, + first_name=data['first_name'], + last_name=data['last_name'], + email=email, + is_active=True) + except IntegrityError as e: + self.logger.error(f'User {email} unexpectedly exists.') + raise e + + # Set the user's middle name in the UserProfile; + # generate a manager request. + try: + manager_profile = manager.userprofile + except UserProfile.DoesNotExist as e: + self.logger.error( + f'User {email} unexpectedly has no UserProfile.') + raise e + manager_profile.middle_name = data['middle_name'] + manager_profile.upgrade_request = utc_now_offset_aware() + manager_profile.save() + + # Create an unverified, primary EmailAddress for the new User object. + try: + EmailAddress.objects.create( + user=manager, + email=email, + verified=False, + primary=True) + except IntegrityError as e: + self.logger.error( + f'EmailAddress {email} unexpectedly already exists.') + raise e + + return manager + + def __handle_create_new_standalone_cluster(self, form_data, pi, manager): + """Create a new project and an allocation to the primary Compute + resource.""" + step_number = self.step_numbers_by_form_name['details'] + data = self.__set_data_from_previous_steps(step_number, form_data) + project_data = form_data[step_number] + print(data) + + resource_type, _ = ResourceType.objects.get_or_create( + name='Cluster', description='Cluster servers') + + try: + resource_name = project_data["name"].upper() + " Compute" + resource = Resource.objects.create( + name=resource_name, resource_type=resource_type + ) + resource.description = project_data["description"] + resource.save() + except IntegrityError as e: + self.logger.error( + f'Resource {project_data["name"]} unexpectedly already exists.') + raise e + + # TODO: How many units should I allocate? + num_service_units = settings.ALLOCATION_MAX + project = create_project_with_compute_allocation(project_data['name'], + resource, + num_service_units) + + pi_role = ProjectUserRoleChoice.objects.get(name='Principal Investigator') + manager_role = ProjectUserRoleChoice.objects.get(name='Manager') + active_status = ProjectUserStatusChoice.objects.get(name='Active') + pi_project_user = ProjectUser.objects.create( + user=pi, project=project, role=pi_role, status=active_status) + manager_project_user = ProjectUser.objects.create( + user=manager, project=project, role=manager_role, + status=active_status) + + project_users = [pi_project_user, manager_project_user] + + runner_factory = NewProjectUserRunnerFactory() + for project_user in project_users: + runner = runner_factory.get_runner( + project_user, NewProjectUserSource.AUTO_ADDED, + email_strategy=DropEmailStrategy()) + runner.run() + + return project + + def __set_data_from_previous_steps(self, step, dictionary): + """Update the given dictionary with data from previous steps.""" + details_step = self.step_numbers_by_form_name['details'] + existing_pi_step = self.step_numbers_by_form_name['existing_pi'] + new_pi_step = self.step_numbers_by_form_name['new_pi'] + existing_manager_step = self.step_numbers_by_form_name['existing_manager'] + new_manager_step = self.step_numbers_by_form_name['new_manager'] + if step > new_pi_step: + existing_pi_form_data = self.get_cleaned_data_for_step( + str(existing_pi_step)) + new_pi_form_data = self.get_cleaned_data_for_step(str(new_pi_step)) + if existing_pi_form_data['PI'] is not None: + pi = existing_pi_form_data['PI'] + pi_string = f'{pi.first_name} {pi.last_name} ({pi.email})' + dictionary.update({ + 'breadcrumb_pi': ( + f'Existing PI: {pi_string}'), + 'pi': pi_string + }) + else: + first_name = new_pi_form_data['first_name'] + last_name = new_pi_form_data['last_name'] + email = new_pi_form_data['email'] + pi_string = f'{first_name} {last_name} ({email})' + dictionary.update({ + 'breadcrumb_pi': ( + f'New PI: {pi_string}'), + 'pi': pi_string + }) + + if step > new_manager_step: + existing_manager_form_data = self.get_cleaned_data_for_step( + str(existing_manager_step)) + new_manager_form_data = self.get_cleaned_data_for_step(str(new_manager_step)) + if existing_manager_form_data['manager'] is not None: + manager = existing_manager_form_data['manager'] + dictionary.update({ + 'breadcrumb_manager': ( + f'Existing manager: {manager.first_name} {manager.last_name} ' + f'({manager.email})'), + 'manager': (f'{manager.first_name} {manager.last_name} ({manager.email})') + }) + else: + first_name = new_manager_form_data['first_name'] + last_name = new_manager_form_data['last_name'] + email = new_manager_form_data['email'] + manager_string = f'{first_name} {last_name} ({email})' + dictionary.update({ + 'breadcrumb_manager': ( + f'New manager: {manager_string}'), + 'manager': manager_string + }) + + if step > details_step: + details_form_data = self.get_cleaned_data_for_step( + str(details_step)) + name = details_form_data['name'] + dictionary.update({'breadcrumb_project': name.title()}) diff --git a/coldfront/templates/common/navbar_admin.html b/coldfront/templates/common/navbar_admin.html index b7e678b56c..82fda0e793 100644 --- a/coldfront/templates/common/navbar_admin.html +++ b/coldfront/templates/common/navbar_admin.html @@ -15,6 +15,7 @@ Project Reviews {% endcomment %}