From 88b86e19eeafdc5073e96c26a529cedb7416b2ee Mon Sep 17 00:00:00 2001 From: Rae Xin Date: Fri, 14 Jun 2024 13:53:44 -0700 Subject: [PATCH 01/13] Add 'Create a standalone cluster' button to /project page. Added to urls.py --- coldfront/core/project/templates/project/project_list.html | 6 ++++++ coldfront/core/project/urls.py | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/coldfront/core/project/templates/project/project_list.html b/coldfront/core/project/templates/project/project_list.html index 0a6fa68af7..bc87924c7e 100644 --- a/coldfront/core/project/templates/project/project_list.html +++ b/coldfront/core/project/templates/project/project_list.html @@ -16,6 +16,12 @@ Create a project + {% if user.is_superuser %} + + + Create a standalone cluster + + {% endif %} Join a project 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), From fe40ffe075b2cb6f453667c540a314e5d757d76d Mon Sep 17 00:00:00 2001 From: Rae Xin Date: Fri, 14 Jun 2024 13:54:29 -0700 Subject: [PATCH 02/13] Created StandaloneClusterRequestWizard view, currently just copies Savio request wizard --- .../views_/new_project_views/request_views.py | 532 ++++++++++++++++++ 1 file changed, 532 insertions(+) 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 e4bfa52d85..587be57c5d 100644 --- a/coldfront/core/project/views_/new_project_views/request_views.py +++ b/coldfront/core/project/views_/new_project_views/request_views.py @@ -737,3 +737,535 @@ def __handle_create_new_project(self, data): allocation.save() return project + +class StandaloneClusterRequestWizard(LoginRequiredMixin, UserPassesTestMixin, + SessionWizardView): + FORMS = [ + ('computing_allowance', ComputingAllowanceForm), + ('allocation_period', SavioProjectAllocationPeriodForm), + ('existing_pi', SavioProjectExistingPIForm), + ('new_pi', SavioProjectNewPIForm), + ('ica_extra_fields', SavioProjectICAExtraFieldsForm), + ('recharge_extra_fields', SavioProjectRechargeExtraFieldsForm), + ('pool_allocations', SavioProjectPoolAllocationsForm), + ('pooled_project_selection', SavioProjectPooledProjectSelectionForm), + ('details', SavioProjectDetailsForm), + ('billing_id', BillingIDValidationForm), + ('survey', SavioProjectSurveyForm), + ] + + TEMPLATES = { + 'computing_allowance': + 'project/project_request/savio/project_computing_allowance.html', + 'allocation_period': + 'project/project_request/savio/project_allocation_period.html', + 'existing_pi': + 'project/project_request/savio/project_existing_pi.html', + 'new_pi': + 'project/project_request/savio/project_new_pi.html', + 'ica_extra_fields': + 'project/project_request/savio/project_ica_extra_fields.html', + 'recharge_extra_fields': + 'project/project_request/savio/project_recharge_extra_fields.html', + 'pool_allocations': + 'project/project_request/savio/project_pool_allocations.html', + 'pooled_project_selection': + ('project/project_request/savio/' + 'project_pooled_project_selection.html'), + 'details': 'project/project_request/savio/project_details.html', + 'billing_id': 'project/project_request/savio/project_billing_id.html', + 'survey': 'project/project_request/savio/project_survey.html', + } + + form_list = [ + ComputingAllowanceForm, + SavioProjectAllocationPeriodForm, + SavioProjectExistingPIForm, + SavioProjectNewPIForm, + SavioProjectICAExtraFieldsForm, + SavioProjectRechargeExtraFieldsForm, + SavioProjectPoolAllocationsForm, + SavioProjectPooledProjectSelectionForm, + SavioProjectDetailsForm, + BillingIDValidationForm, + SavioProjectSurveyForm, + ] + + 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 + signed_date = ( + self.request.user.userprofile.access_agreement_signed_date) + if signed_date is not None: + return True + message = ( + 'You must sign the User Access Agreement before you can create a ' + 'new project.') + 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_form_kwargs(self, step=None): + # For each step, a list of kwargs required by the corresponding form. + required_keys_by_step_name = { + 'allocation_period': ['computing_allowance'], + 'existing_pi': ['computing_allowance', 'allocation_period'], + 'pooled_project_selection': ['computing_allowance'], + 'details': ['computing_allowance'], + 'survey': ['computing_allowance'], + } + # For each step number, the corresponding step name. + step_names_by_step_number = { + self.step_numbers_by_form_name[name]: name + for name in required_keys_by_step_name + } + + step_number = int(step) + if step_number not in step_names_by_step_number: + return {} + + data = {} + self.__set_data_from_previous_steps(step_number, data) + + kwargs = {} + step_name = step_names_by_step_number[step_number] + for key in required_keys_by_step_name[step_name]: + kwargs[key] = data.get(key, None) + return kwargs + + 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)) + + request_kwargs = { + 'requester': self.request.user, + } + computing_allowance = self.__get_computing_allowance(form_data) + computing_allowance_wrapper = ComputingAllowance( + computing_allowance) + + with transaction.atomic(): + allocation_period = self.__get_allocation_period(form_data) + pi = self.__handle_pi_data(form_data) + + if computing_allowance_wrapper.is_instructional(): + self.__handle_ica_allowance( + form_data, computing_allowance_wrapper, request_kwargs) + elif computing_allowance_wrapper.is_recharge(): + self.__handle_recharge_allowance( + form_data, computing_allowance_wrapper, request_kwargs) + + pooling_requested = self.__get_pooling_requested(form_data) + if pooling_requested: + project = self.__handle_pool_with_existing_project( + form_data) + else: + project = self.__handle_create_new_project(form_data) + if self.__billing_id_required(): + self.__handle_billing_id(form_data, request_kwargs) + + survey_data = self.__get_survey_data(form_data) + + # Store transformed form data in a request. + # TODO: allocation_type will eventually be removed from the + # TODO: model. + computing_allowance_interface = ComputingAllowanceInterface() + request_kwargs['allocation_type'] = \ + computing_allowance_interface.name_short_from_name( + computing_allowance_wrapper.get_name()) + request_kwargs['computing_allowance'] = computing_allowance + request_kwargs['allocation_period'] = allocation_period + request_kwargs['pi'] = pi + request_kwargs['project'] = project + request_kwargs['pool'] = pooling_requested + request_kwargs['survey_answers'] = survey_data + request_kwargs['status'] = \ + ProjectAllocationRequestStatusChoice.objects.get( + name='Under Review') + request_kwargs['request_time'] = utc_now_offset_aware() + request = SavioProjectAllocationRequest.objects.create( + **request_kwargs) + + # Send a notification email to admins. + try: + send_new_project_request_admin_notification_email(request) + except Exception as e: + self.logger.error( + 'Failed to send notification email. Details:\n') + self.logger.exception(e) + # Send a notification email to the PI if the requester differs. + if request.requester != request.pi: + try: + send_new_project_request_pi_notification_email(request) + except Exception as e: + self.logger.error( + 'Failed to send notification email. Details:\n') + self.logger.exception(e) + except Exception as e: + self.logger.exception(e) + message = 'Unexpected failure. Please contact an administrator.' + messages.error(self.request, message) + else: + 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 { + '1': view.show_allocation_period_form_condition, + '3': view.show_new_pi_form_condition, + '4': view.show_ica_extra_fields_form_condition, + '5': view.show_recharge_extra_fields_form_condition, + '6': view.show_pool_allocations_form_condition, + '7': view.show_pooled_project_selection_form_condition, + '8': view.show_details_form_condition, + '9': view.show_billing_id_form_condition, + } + + def show_allocation_period_form_condition(self): + """Only show the form for selecting an AllocationPeriod for + periodic allowances.""" + step_name = 'computing_allowance' + step = str(self.step_numbers_by_form_name[step_name]) + cleaned_data = self.get_cleaned_data_for_step(step) or {} + computing_allowance = cleaned_data.get('computing_allowance', None) + if not computing_allowance: + return False + return ComputingAllowance(computing_allowance).is_periodic() + + def show_billing_id_form_condition(self): + """Only show the form for providing a billing ID when it is + required, and when pooling is not requested.""" + if not self.__billing_id_required(): + return False + step_name = 'pool_allocations' + 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('pool', False) + + def show_details_form_condition(self): + step_name = 'pool_allocations' + 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('pool', False) + + def show_ica_extra_fields_form_condition(self): + step_name = 'computing_allowance' + step = str(self.step_numbers_by_form_name[step_name]) + cleaned_data = self.get_cleaned_data_for_step(step) or {} + computing_allowance = cleaned_data.get('computing_allowance', None) + if not computing_allowance: + return False + computing_allowance = ComputingAllowance(computing_allowance) + return ( + computing_allowance.is_instructional() and + computing_allowance.requires_extra_information()) + + 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_pool_allocations_form_condition(self): + step_name = 'computing_allowance' + step = str(self.step_numbers_by_form_name[step_name]) + cleaned_data = self.get_cleaned_data_for_step(step) or {} + computing_allowance = cleaned_data.get('computing_allowance', None) + if not computing_allowance: + return False + return ComputingAllowance(computing_allowance).is_poolable() + + def show_pooled_project_selection_form_condition(self): + step_name = 'pool_allocations' + step = str(self.step_numbers_by_form_name[step_name]) + cleaned_data = self.get_cleaned_data_for_step(step) or {} + return cleaned_data.get('pool', False) + + def show_recharge_extra_fields_form_condition(self): + step_name = 'computing_allowance' + step = str(self.step_numbers_by_form_name[step_name]) + cleaned_data = self.get_cleaned_data_for_step(step) or {} + computing_allowance = cleaned_data.get('computing_allowance', None) + if not computing_allowance: + return False + computing_allowance = ComputingAllowance(computing_allowance) + return ( + computing_allowance.is_recharge() and + computing_allowance.requires_extra_information()) + + @staticmethod + def __billing_id_required(): + """Return whether a billing ID should be requested from the + user. Ultimately, the form will only be included if pooling is + not requested.""" + return flag_enabled('LRC_ONLY') + + def __get_allocation_period(self, form_data): + """Return the AllocationPeriod the user selected.""" + step_number = self.step_numbers_by_form_name['allocation_period'] + data = form_data[step_number] + return data.get('allocation_period', None) + + def __get_computing_allowance(self, form_data): + """Return the computing allowance (Resource) the user + selected.""" + step_number = self.step_numbers_by_form_name['computing_allowance'] + data = form_data[step_number] + return data['computing_allowance'] + + def __get_pooling_requested(self, form_data): + """Return whether pooling was requested.""" + step_number = self.step_numbers_by_form_name['pool_allocations'] + data = form_data[step_number] + return data.get('pool', False) + + def __get_survey_data(self, form_data): + """Return provided survey data.""" + step_number = self.step_numbers_by_form_name['survey'] + return form_data[step_number] + + def __handle_billing_id(self, form_data, request_kwargs): + """Store the User-provided billing ID in the given dictionary to + be used during request creation.""" + step_number = self.step_numbers_by_form_name['billing_id'] + data = form_data[step_number] + billing_id = data['billing_id'] + request_kwargs['billing_activity'] = \ + get_or_create_billing_activity_from_full_id(billing_id) + + def __handle_ica_allowance(self, form_data, computing_allowance_wrapper, + request_kwargs): + """Perform ICA-specific handling. + + In particular, set fields in the given dictionary to be used + during request creation. Set the extra_fields field from the + given form data and set the state field to include an additional + step.""" + if computing_allowance_wrapper.requires_extra_information(): + step_number = self.step_numbers_by_form_name['ica_extra_fields'] + data = form_data[step_number] + extra_fields = savio_project_request_ica_extra_fields_schema() + for field in extra_fields: + extra_fields[field] = data[field] + request_kwargs['extra_fields'] = extra_fields + request_kwargs['state'] = savio_project_request_ica_state_schema() + + 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_recharge_allowance(self, form_data, + computing_allowance_wrapper, + request_kwargs): + """Perform Recharge-specific handling. + + In particular, set fields in the given dictionary to be used + during request creation. If required, set the extra_fields field + from the given form data. In general, set the state field to + include an additional step.""" + if computing_allowance_wrapper.requires_extra_information(): + step_number = self.step_numbers_by_form_name[ + 'recharge_extra_fields'] + data = form_data[step_number] + extra_fields = savio_project_request_recharge_extra_fields_schema() + for field in extra_fields: + extra_fields[field] = data[field] + request_kwargs['extra_fields'] = extra_fields + request_kwargs['state'] = savio_project_request_recharge_state_schema() + + def __handle_create_new_project(self, form_data): + """Create a new project and an allocation to the primary Compute + resource.""" + step_number = self.step_numbers_by_form_name['details'] + data = form_data[step_number] + + # Create the new Project. + status = ProjectStatusChoice.objects.get(name='New') + try: + project = Project.objects.create( + name=data['name'], + status=status, + title=data['title'], + description=data['description']) + #field_of_science=data['field_of_science']) + except IntegrityError as e: + self.logger.error( + f'Project {data["name"]} unexpectedly already exists.') + raise e + + # Create an allocation to the primary compute resource. + status = AllocationStatusChoice.objects.get(name='New') + allocation = Allocation.objects.create(project=project, status=status) + resource = get_primary_compute_resource() + allocation.resources.add(resource) + allocation.save() + + return project + + def __handle_pool_with_existing_project(self, form_data): + """Return the requested project to pool with.""" + step_number = \ + self.step_numbers_by_form_name['pooled_project_selection'] + data = form_data[step_number] + project = data['project'] + + # Validate that the project has exactly one allocation to the "Savio + # Compute" resource. + resource = get_primary_compute_resource() + allocations = Allocation.objects.filter( + project=project, resources__pk__exact=resource.pk) + try: + assert allocations.count() == 1 + except AssertionError as e: + number = 'no' if allocations.count() == 0 else 'more than one' + self.logger.error( + f'Project {project.name} unexpectedly has {number} Allocation ' + f'to Resource {resource.name}') + raise e + + return project + + def __set_data_from_previous_steps(self, step, dictionary): + """Update the given dictionary with data from previous steps.""" + computing_allowance_form_step = \ + self.step_numbers_by_form_name['computing_allowance'] + if step > computing_allowance_form_step: + computing_allowance_form_data = self.get_cleaned_data_for_step( + str(computing_allowance_form_step)) + if computing_allowance_form_data: + dictionary.update(computing_allowance_form_data) + computing_allowance_wrapper = ComputingAllowance( + computing_allowance_form_data['computing_allowance']) + dictionary['allowance_is_one_per_pi'] = \ + computing_allowance_wrapper.is_one_per_pi() + + allocation_period_form_step = \ + self.step_numbers_by_form_name['allocation_period'] + if step > allocation_period_form_step: + allocation_period_form_data = self.get_cleaned_data_for_step( + str(allocation_period_form_step)) + if allocation_period_form_data: + dictionary.update(allocation_period_form_data) + + existing_pi_step = self.step_numbers_by_form_name['existing_pi'] + new_pi_step = self.step_numbers_by_form_name['new_pi'] + 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'] + dictionary.update({ + 'breadcrumb_pi': ( + f'Existing PI: {pi.first_name} {pi.last_name} ' + f'({pi.email})') + }) + else: + first_name = new_pi_form_data['first_name'] + last_name = new_pi_form_data['last_name'] + email = new_pi_form_data['email'] + dictionary.update({ + 'breadcrumb_pi': ( + f'New PI: {first_name} {last_name} ({email})') + }) + + pool_allocations_step = \ + self.step_numbers_by_form_name['pool_allocations'] + if step > pool_allocations_step: + computing_allowance = ComputingAllowance( + dictionary['computing_allowance']) + if computing_allowance.is_poolable(): + pool_allocations_form_data = self.get_cleaned_data_for_step( + str(pool_allocations_step)) + pooling_requested = pool_allocations_form_data['pool'] + else: + pooling_requested = False + dictionary.update({'breadcrumb_pooling': pooling_requested}) + + pooled_project_selection_step = \ + self.step_numbers_by_form_name['pooled_project_selection'] + details_step = self.step_numbers_by_form_name['details'] + if step > details_step: + if pooling_requested: + pooled_project_selection_form_data = \ + self.get_cleaned_data_for_step( + str(pooled_project_selection_step)) + project = pooled_project_selection_form_data['project'] + dictionary.update({ + 'breadcrumb_project': f'Project: {project.name}' + }) + else: + details_form_data = self.get_cleaned_data_for_step( + str(details_step)) + name = details_form_data['name'] + dictionary.update({'breadcrumb_project': f'Project: {name}'}) From 60c8c0354d371925a772ac47939e5a0e47776c61 Mon Sep 17 00:00:00 2001 From: Rae Xin Date: Thu, 20 Jun 2024 17:22:57 -0500 Subject: [PATCH 03/13] Add base StandaloneCluster forms, views, and templates --- .../forms_/new_project_forms/request_forms.py | 120 +++++ .../standalone_cluster/project_details.html | 60 +++ .../project_existing_manager.html | 70 +++ .../project_existing_pi.html | 78 +++ .../project_new_manager.html | 60 +++ .../standalone_cluster/project_new_pi.html | 65 +++ .../views_/new_project_views/request_views.py | 452 +++++------------- 7 files changed, 560 insertions(+), 345 deletions(-) create mode 100644 coldfront/core/project/templates/project/project_request/standalone_cluster/project_details.html create mode 100644 coldfront/core/project/templates/project/project_request/standalone_cluster/project_existing_manager.html create mode 100644 coldfront/core/project/templates/project/project_request/standalone_cluster/project_existing_pi.html create mode 100644 coldfront/core/project/templates/project/project_request/standalone_cluster/project_new_manager.html create mode 100644 coldfront/core/project/templates/project/project_request/standalone_cluster/project_new_pi.html 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 7029299ddf..39ea1c01ac 100644 --- a/coldfront/core/project/forms_/new_project_forms/request_forms.py +++ b/coldfront/core/project/forms_/new_project_forms/request_forms.py @@ -850,3 +850,123 @@ 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 project, 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.')) + ]) + +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 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 %} + + +

New Standalone Cluster Details


+ +

You are creating a new standalone cluster. Please provide the following details:

+ +
+ {% csrf_token %} + + {{ wizard.management_form }} + {% if wizard.form.forms %} + {{ wizard.form.management_form }} + {% for form in wizard.form.forms %} + {{ form|crispy }} + {% endfor %} + {% else %} + {{ wizard.form|crispy }} + {% endif %} +
+ {% if wizard.steps.prev %} + + + {% endif %} + +
+
+ +

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 %} + + + + +

New Standalone Cluster: Manager


+ +

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.

+ +
+ {% csrf_token %} + + {{ wizard.management_form }} + {% if wizard.form.forms %} + {{ wizard.form.management_form }} + {% for form in wizard.form.forms %} + {{ form|crispy }} + {% endfor %} + {% else %} + {{ wizard.form|crispy }} + {% endif %} +
+ {% if wizard.steps.prev %} + + + {% endif %} + +
+
+ +

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 %} + + + + +

New Standalone Cluster: Principal Investigator


+ +

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 %} + +
+ {% csrf_token %} + + {{ wizard.management_form }} + {% if wizard.form.forms %} + {{ wizard.form.management_form }} + {% for form in wizard.form.forms %} + {{ form|crispy }} + {% endfor %} + {% else %} + {{ wizard.form|crispy }} + {% endif %} +
+ {% if wizard.steps.prev %} + + + {% 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 %} + +

New Standalone Cluster: Manager


+ +

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.

+ +
+ {% csrf_token %} + + {{ wizard.management_form }} + {% if wizard.form.forms %} + {{ wizard.form.management_form }} + {% for form in wizard.form.forms %} + {{ form|crispy }} +
+ {% endfor %} + {% else %} + {{ wizard.form|crispy }} + {% endif %} +
+ {% if wizard.steps.prev %} + + + {% endif %} + +
+
+ +

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 %} + +

New Standalone Cluster: Principal Investigator


+ +

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 %} + +
+ {% csrf_token %} + + {{ wizard.management_form }} + {% if wizard.form.forms %} + {{ wizard.form.management_form }} + {% for form in wizard.form.forms %} + {{ form|crispy }} +
+ {% endfor %} + {% else %} + {{ wizard.form|crispy }} + {% endif %} +
+ {% if wizard.steps.prev %} + + + {% endif %} + +
+
+ +

Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}

+ +{% endblock %} 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 587be57c5d..d4f82a78c6 100644 --- a/coldfront/core/project/views_/new_project_views/request_views.py +++ b/coldfront/core/project/views_/new_project_views/request_views.py @@ -15,6 +15,11 @@ 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.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.models import Project from coldfront.core.project.models import ProjectAllocationRequestStatusChoice from coldfront.core.project.models import ProjectStatusChoice @@ -741,54 +746,32 @@ def __handle_create_new_project(self, data): class StandaloneClusterRequestWizard(LoginRequiredMixin, UserPassesTestMixin, SessionWizardView): FORMS = [ - ('computing_allowance', ComputingAllowanceForm), - ('allocation_period', SavioProjectAllocationPeriodForm), - ('existing_pi', SavioProjectExistingPIForm), - ('new_pi', SavioProjectNewPIForm), - ('ica_extra_fields', SavioProjectICAExtraFieldsForm), - ('recharge_extra_fields', SavioProjectRechargeExtraFieldsForm), - ('pool_allocations', SavioProjectPoolAllocationsForm), - ('pooled_project_selection', SavioProjectPooledProjectSelectionForm), - ('details', SavioProjectDetailsForm), - ('billing_id', BillingIDValidationForm), - ('survey', SavioProjectSurveyForm), + + ('details', StandaloneClusterDetailsForm), + ('existing_pi', StandaloneClusterExistingPIForm), + ('new_pi', StandaloneClusterNewPIForm), + ('existing_manager', StandaloneClusterExistingManagerForm), + ('new_manager', StandaloneClusterNewManagerForm), ] TEMPLATES = { - 'computing_allowance': - 'project/project_request/savio/project_computing_allowance.html', - 'allocation_period': - 'project/project_request/savio/project_allocation_period.html', + 'details': 'project/project_request/standalone_cluster/project_details.html', 'existing_pi': - 'project/project_request/savio/project_existing_pi.html', + 'project/project_request/standalone_cluster/project_existing_pi.html', 'new_pi': - 'project/project_request/savio/project_new_pi.html', - 'ica_extra_fields': - 'project/project_request/savio/project_ica_extra_fields.html', - 'recharge_extra_fields': - 'project/project_request/savio/project_recharge_extra_fields.html', - 'pool_allocations': - 'project/project_request/savio/project_pool_allocations.html', - 'pooled_project_selection': - ('project/project_request/savio/' - 'project_pooled_project_selection.html'), - 'details': 'project/project_request/savio/project_details.html', - 'billing_id': 'project/project_request/savio/project_billing_id.html', - 'survey': 'project/project_request/savio/project_survey.html', + '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', } form_list = [ - ComputingAllowanceForm, - SavioProjectAllocationPeriodForm, - SavioProjectExistingPIForm, - SavioProjectNewPIForm, - SavioProjectICAExtraFieldsForm, - SavioProjectRechargeExtraFieldsForm, - SavioProjectPoolAllocationsForm, - SavioProjectPooledProjectSelectionForm, - SavioProjectDetailsForm, - BillingIDValidationForm, - SavioProjectSurveyForm, + StandaloneClusterDetailsForm, + StandaloneClusterExistingPIForm, + StandaloneClusterNewPIForm, + StandaloneClusterExistingManagerForm, + StandaloneClusterNewManagerForm, ] logger = logging.getLogger(__name__) @@ -802,13 +785,8 @@ def __init__(self, *args, **kwargs): def test_func(self): if self.request.user.is_superuser: return True - signed_date = ( - self.request.user.userprofile.access_agreement_signed_date) - if signed_date is not None: - return True message = ( - 'You must sign the User Access Agreement before you can create a ' - 'new project.') + 'You do not have permission to create a standalone cluster.') messages.error(self.request, message) def get_context_data(self, form, **kwargs): @@ -817,34 +795,6 @@ def get_context_data(self, form, **kwargs): self.__set_data_from_previous_steps(current_step, context) return context - def get_form_kwargs(self, step=None): - # For each step, a list of kwargs required by the corresponding form. - required_keys_by_step_name = { - 'allocation_period': ['computing_allowance'], - 'existing_pi': ['computing_allowance', 'allocation_period'], - 'pooled_project_selection': ['computing_allowance'], - 'details': ['computing_allowance'], - 'survey': ['computing_allowance'], - } - # For each step number, the corresponding step name. - step_names_by_step_number = { - self.step_numbers_by_form_name[name]: name - for name in required_keys_by_step_name - } - - step_number = int(step) - if step_number not in step_names_by_step_number: - return {} - - data = {} - self.__set_data_from_previous_steps(step_number, data) - - kwargs = {} - step_name = step_names_by_step_number[step_number] - for key in required_keys_by_step_name[step_name]: - kwargs[key] = data.get(key, None) - return kwargs - def get_template_names(self): return [self.TEMPLATES[self.FORMS[int(self.steps.current)][0]]] @@ -855,71 +805,14 @@ def done(self, form_list, **kwargs): try: form_data = session_wizard_all_form_data( form_list, kwargs['form_dict'], len(self.form_list)) - request_kwargs = { 'requester': self.request.user, } - computing_allowance = self.__get_computing_allowance(form_data) - computing_allowance_wrapper = ComputingAllowance( - computing_allowance) - with transaction.atomic(): - allocation_period = self.__get_allocation_period(form_data) pi = self.__handle_pi_data(form_data) - - if computing_allowance_wrapper.is_instructional(): - self.__handle_ica_allowance( - form_data, computing_allowance_wrapper, request_kwargs) - elif computing_allowance_wrapper.is_recharge(): - self.__handle_recharge_allowance( - form_data, computing_allowance_wrapper, request_kwargs) - - pooling_requested = self.__get_pooling_requested(form_data) - if pooling_requested: - project = self.__handle_pool_with_existing_project( - form_data) - else: - project = self.__handle_create_new_project(form_data) - if self.__billing_id_required(): - self.__handle_billing_id(form_data, request_kwargs) - - survey_data = self.__get_survey_data(form_data) - - # Store transformed form data in a request. - # TODO: allocation_type will eventually be removed from the - # TODO: model. - computing_allowance_interface = ComputingAllowanceInterface() - request_kwargs['allocation_type'] = \ - computing_allowance_interface.name_short_from_name( - computing_allowance_wrapper.get_name()) - request_kwargs['computing_allowance'] = computing_allowance - request_kwargs['allocation_period'] = allocation_period - request_kwargs['pi'] = pi - request_kwargs['project'] = project - request_kwargs['pool'] = pooling_requested - request_kwargs['survey_answers'] = survey_data - request_kwargs['status'] = \ - ProjectAllocationRequestStatusChoice.objects.get( - name='Under Review') - request_kwargs['request_time'] = utc_now_offset_aware() - request = SavioProjectAllocationRequest.objects.create( - **request_kwargs) - - # Send a notification email to admins. - try: - send_new_project_request_admin_notification_email(request) - except Exception as e: - self.logger.error( - 'Failed to send notification email. Details:\n') - self.logger.exception(e) - # Send a notification email to the PI if the requester differs. - if request.requester != request.pi: - try: - send_new_project_request_pi_notification_email(request) - except Exception as e: - self.logger.error( - 'Failed to send notification email. Details:\n') - self.logger.exception(e) + manager = self.__handle_manager_data(form_data) + project = self.__handle_create_new_standalone_cluster(form_data) + # TODO: create Resource object to represent the cluster. except Exception as e: self.logger.exception(e) message = 'Unexpected failure. Please contact an administrator.' @@ -936,145 +829,29 @@ def done(self, form_list, **kwargs): def condition_dict(): view = StandaloneClusterRequestWizard return { - '1': view.show_allocation_period_form_condition, - '3': view.show_new_pi_form_condition, - '4': view.show_ica_extra_fields_form_condition, - '5': view.show_recharge_extra_fields_form_condition, - '6': view.show_pool_allocations_form_condition, - '7': view.show_pooled_project_selection_form_condition, - '8': view.show_details_form_condition, - '9': view.show_billing_id_form_condition, + '2': view.show_new_pi_form_condition, + '4': view.show_new_manager_form_condition, } - def show_allocation_period_form_condition(self): - """Only show the form for selecting an AllocationPeriod for - periodic allowances.""" - step_name = 'computing_allowance' - step = str(self.step_numbers_by_form_name[step_name]) - cleaned_data = self.get_cleaned_data_for_step(step) or {} - computing_allowance = cleaned_data.get('computing_allowance', None) - if not computing_allowance: - return False - return ComputingAllowance(computing_allowance).is_periodic() - - def show_billing_id_form_condition(self): - """Only show the form for providing a billing ID when it is - required, and when pooling is not requested.""" - if not self.__billing_id_required(): - return False - step_name = 'pool_allocations' - 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('pool', False) - def show_details_form_condition(self): - step_name = 'pool_allocations' + step_name = 'details' 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('pool', False) - def show_ica_extra_fields_form_condition(self): - step_name = 'computing_allowance' - step = str(self.step_numbers_by_form_name[step_name]) - cleaned_data = self.get_cleaned_data_for_step(step) or {} - computing_allowance = cleaned_data.get('computing_allowance', None) - if not computing_allowance: - return False - computing_allowance = ComputingAllowance(computing_allowance) - return ( - computing_allowance.is_instructional() and - computing_allowance.requires_extra_information()) - 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_pool_allocations_form_condition(self): - step_name = 'computing_allowance' - step = str(self.step_numbers_by_form_name[step_name]) - cleaned_data = self.get_cleaned_data_for_step(step) or {} - computing_allowance = cleaned_data.get('computing_allowance', None) - if not computing_allowance: - return False - return ComputingAllowance(computing_allowance).is_poolable() - - def show_pooled_project_selection_form_condition(self): - step_name = 'pool_allocations' - step = str(self.step_numbers_by_form_name[step_name]) - cleaned_data = self.get_cleaned_data_for_step(step) or {} - return cleaned_data.get('pool', False) - - def show_recharge_extra_fields_form_condition(self): - step_name = 'computing_allowance' + + 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 {} - computing_allowance = cleaned_data.get('computing_allowance', None) - if not computing_allowance: - return False - computing_allowance = ComputingAllowance(computing_allowance) - return ( - computing_allowance.is_recharge() and - computing_allowance.requires_extra_information()) + return not cleaned_data.get('manager', False) @staticmethod - def __billing_id_required(): - """Return whether a billing ID should be requested from the - user. Ultimately, the form will only be included if pooling is - not requested.""" - return flag_enabled('LRC_ONLY') - - def __get_allocation_period(self, form_data): - """Return the AllocationPeriod the user selected.""" - step_number = self.step_numbers_by_form_name['allocation_period'] - data = form_data[step_number] - return data.get('allocation_period', None) - - def __get_computing_allowance(self, form_data): - """Return the computing allowance (Resource) the user - selected.""" - step_number = self.step_numbers_by_form_name['computing_allowance'] - data = form_data[step_number] - return data['computing_allowance'] - - def __get_pooling_requested(self, form_data): - """Return whether pooling was requested.""" - step_number = self.step_numbers_by_form_name['pool_allocations'] - data = form_data[step_number] - return data.get('pool', False) - - def __get_survey_data(self, form_data): - """Return provided survey data.""" - step_number = self.step_numbers_by_form_name['survey'] - return form_data[step_number] - - def __handle_billing_id(self, form_data, request_kwargs): - """Store the User-provided billing ID in the given dictionary to - be used during request creation.""" - step_number = self.step_numbers_by_form_name['billing_id'] - data = form_data[step_number] - billing_id = data['billing_id'] - request_kwargs['billing_activity'] = \ - get_or_create_billing_activity_from_full_id(billing_id) - - def __handle_ica_allowance(self, form_data, computing_allowance_wrapper, - request_kwargs): - """Perform ICA-specific handling. - - In particular, set fields in the given dictionary to be used - during request creation. Set the extra_fields field from the - given form data and set the state field to include an additional - step.""" - if computing_allowance_wrapper.requires_extra_information(): - step_number = self.step_numbers_by_form_name['ica_extra_fields'] - data = form_data[step_number] - extra_fields = savio_project_request_ica_extra_fields_schema() - for field in extra_fields: - extra_fields[field] = data[field] - request_kwargs['extra_fields'] = extra_fields - request_kwargs['state'] = savio_project_request_ica_state_schema() - def __handle_pi_data(self, form_data): """Return the requested PI. If the PI did not exist, create a new User and UserProfile.""" @@ -1123,27 +900,57 @@ def __handle_pi_data(self, form_data): 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'] - def __handle_recharge_allowance(self, form_data, - computing_allowance_wrapper, - request_kwargs): - """Perform Recharge-specific handling. + # 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 - In particular, set fields in the given dictionary to be used - during request creation. If required, set the extra_fields field - from the given form data. In general, set the state field to - include an additional step.""" - if computing_allowance_wrapper.requires_extra_information(): - step_number = self.step_numbers_by_form_name[ - 'recharge_extra_fields'] - data = form_data[step_number] - extra_fields = savio_project_request_recharge_extra_fields_schema() - for field in extra_fields: - extra_fields[field] = data[field] - request_kwargs['extra_fields'] = extra_fields - request_kwargs['state'] = savio_project_request_recharge_state_schema() + # 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() - def __handle_create_new_project(self, form_data): + # 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): """Create a new project and an allocation to the primary Compute resource.""" step_number = self.step_numbers_by_form_name['details'] @@ -1172,53 +979,13 @@ def __handle_create_new_project(self, form_data): return project - def __handle_pool_with_existing_project(self, form_data): - """Return the requested project to pool with.""" - step_number = \ - self.step_numbers_by_form_name['pooled_project_selection'] - data = form_data[step_number] - project = data['project'] - - # Validate that the project has exactly one allocation to the "Savio - # Compute" resource. - resource = get_primary_compute_resource() - allocations = Allocation.objects.filter( - project=project, resources__pk__exact=resource.pk) - try: - assert allocations.count() == 1 - except AssertionError as e: - number = 'no' if allocations.count() == 0 else 'more than one' - self.logger.error( - f'Project {project.name} unexpectedly has {number} Allocation ' - f'to Resource {resource.name}') - raise e - - return project - def __set_data_from_previous_steps(self, step, dictionary): """Update the given dictionary with data from previous steps.""" - computing_allowance_form_step = \ - self.step_numbers_by_form_name['computing_allowance'] - if step > computing_allowance_form_step: - computing_allowance_form_data = self.get_cleaned_data_for_step( - str(computing_allowance_form_step)) - if computing_allowance_form_data: - dictionary.update(computing_allowance_form_data) - computing_allowance_wrapper = ComputingAllowance( - computing_allowance_form_data['computing_allowance']) - dictionary['allowance_is_one_per_pi'] = \ - computing_allowance_wrapper.is_one_per_pi() - - allocation_period_form_step = \ - self.step_numbers_by_form_name['allocation_period'] - if step > allocation_period_form_step: - allocation_period_form_data = self.get_cleaned_data_for_step( - str(allocation_period_form_step)) - if allocation_period_form_data: - dictionary.update(allocation_period_form_data) - + 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)) @@ -1239,33 +1006,28 @@ def __set_data_from_previous_steps(self, step, dictionary): f'New PI: {first_name} {last_name} ({email})') }) - pool_allocations_step = \ - self.step_numbers_by_form_name['pool_allocations'] - if step > pool_allocations_step: - computing_allowance = ComputingAllowance( - dictionary['computing_allowance']) - if computing_allowance.is_poolable(): - pool_allocations_form_data = self.get_cleaned_data_for_step( - str(pool_allocations_step)) - pooling_requested = pool_allocations_form_data['pool'] - else: - pooling_requested = False - dictionary.update({'breadcrumb_pooling': pooling_requested}) - - pooled_project_selection_step = \ - self.step_numbers_by_form_name['pooled_project_selection'] - details_step = self.step_numbers_by_form_name['details'] - if step > details_step: - if pooling_requested: - pooled_project_selection_form_data = \ - self.get_cleaned_data_for_step( - str(pooled_project_selection_step)) - project = pooled_project_selection_form_data['project'] + 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_project': f'Project: {project.name}' + 'breadcrumb_manager': ( + f'Existing manager: {manager.first_name} {manager.last_name} ' + f'({manager.email})') }) else: - details_form_data = self.get_cleaned_data_for_step( - str(details_step)) - name = details_form_data['name'] - dictionary.update({'breadcrumb_project': f'Project: {name}'}) + first_name = new_manager_form_data['first_name'] + last_name = new_manager_form_data['last_name'] + email = new_manager_form_data['email'] + dictionary.update({ + 'breadcrumb_manager': ( + f'New manager: {first_name} {last_name} ({email})') + }) + + 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': f'Project: {name}'}) From acf759d7a4b5f77184b33987bc25b346eca809cf Mon Sep 17 00:00:00 2001 From: Rae Xin Date: Thu, 20 Jun 2024 18:23:31 -0500 Subject: [PATCH 04/13] Add description and title fields --- .../forms_/new_project_forms/request_forms.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 39ea1c01ac..6f6d830792 100644 --- a/coldfront/core/project/forms_/new_project_forms/request_forms.py +++ b/coldfront/core/project/forms_/new_project_forms/request_forms.py @@ -870,6 +870,27 @@ class StandaloneClusterDetailsForm(forms.Form): message=( 'Name must contain only lowercase letters and numbers.')) ]) + title = forms.CharField( + help_text='A unique, human-readable title for the project.', + label='Title', + max_length=255, + required=True, + validators=[ + MinLengthValidator(4), + ]) + description = forms.CharField( + help_text='A few sentences describing your project.', + 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( From dd9ab7a3d8d7b2726d546d880df4eb107947eedb Mon Sep 17 00:00:00 2001 From: Rae Xin Date: Mon, 24 Jun 2024 11:27:53 -0500 Subject: [PATCH 05/13] Fix PI and manager handling, create project and resource objects --- .../views_/new_project_views/request_views.py | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) 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 d4f82a78c6..3deb40069d 100644 --- a/coldfront/core/project/views_/new_project_views/request_views.py +++ b/coldfront/core/project/views_/new_project_views/request_views.py @@ -20,7 +20,7 @@ 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.models import Project +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 @@ -32,6 +32,7 @@ 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.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 @@ -742,6 +743,11 @@ def __handle_create_new_project(self, data): allocation.save() return project + +# ============================================================================= +# STANDALONE CLUSTER +# ============================================================================= + class StandaloneClusterRequestWizard(LoginRequiredMixin, UserPassesTestMixin, SessionWizardView): @@ -805,14 +811,18 @@ def done(self, form_list, **kwargs): try: form_data = session_wizard_all_form_data( form_list, kwargs['form_dict'], len(self.form_list)) - request_kwargs = { - 'requester': self.request.user, - } 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) - # TODO: create Resource object to represent the cluster. + project, resource = self.__handle_create_new_standalone_cluster(form_data) + pi_role = ProjectUserRoleChoice.objects.get(name='Principal Investigator') + manager_role = ProjectUserRoleChoice.objects.get(name='Manager') + active_status = ProjectUserStatusChoice.objects.get(name='Active') + ProjectUser.objects.create( + user=pi, project=project, role=pi_role, status=active_status) + ProjectUser.objects.create( + user=manager, project=project, role=manager_role, + status=active_status) except Exception as e: self.logger.exception(e) message = 'Unexpected failure. Please contact an administrator.' @@ -833,12 +843,6 @@ def condition_dict(): '4': view.show_new_manager_form_condition, } - def show_details_form_condition(self): - step_name = 'details' - 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('pool', False) - def show_new_pi_form_condition(self): step_name = 'existing_pi' step = str(self.step_numbers_by_form_name[step_name]) @@ -851,7 +855,6 @@ def show_new_manager_form_condition(self): cleaned_data = self.get_cleaned_data_for_step(step) or {} return not cleaned_data.get('manager', False) - @staticmethod def __handle_pi_data(self, form_data): """Return the requested PI. If the PI did not exist, create a new User and UserProfile.""" @@ -954,30 +957,38 @@ def __handle_create_new_standalone_cluster(self, form_data): """Create a new project and an allocation to the primary Compute resource.""" step_number = self.step_numbers_by_form_name['details'] - data = form_data[step_number] + 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') # Create the new Project. status = ProjectStatusChoice.objects.get(name='New') try: project = Project.objects.create( - name=data['name'], + name=project_data['name'], status=status, - title=data['title'], - description=data['description']) - #field_of_science=data['field_of_science']) + title=project_data['title'], + description=project_data['description']) except IntegrityError as e: self.logger.error( - f'Project {data["name"]} unexpectedly already exists.') + f'Project {project_data["name"]} unexpectedly already exists.') raise e - # Create an allocation to the primary compute resource. - status = AllocationStatusChoice.objects.get(name='New') - allocation = Allocation.objects.create(project=project, status=status) - resource = get_primary_compute_resource() - allocation.resources.add(resource) - allocation.save() + try: + resource = Resource.objects.create( + name=project_data["title"].title(), 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 - return project + return project, resource def __set_data_from_previous_steps(self, step, dictionary): """Update the given dictionary with data from previous steps.""" From 4c43ff8b28e78dda4502c611602c35c51ee1cbb1 Mon Sep 17 00:00:00 2001 From: Rae Xin Date: Mon, 24 Jun 2024 15:55:31 -0500 Subject: [PATCH 06/13] Add review_and_submit page for standalone clusters. Changed 'project' wording. Removed 'title,' is now only a 'title()ed' version of 'name'. --- .../forms_/new_project_forms/request_forms.py | 21 +++-- .../standalone_cluster/review_and_submit.html | 93 +++++++++++++++++++ .../views_/new_project_views/request_views.py | 27 ++++-- 3 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 coldfront/core/project/templates/project/project_request/standalone_cluster/review_and_submit.html 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 6f6d830792..a25260524f 100644 --- a/coldfront/core/project/forms_/new_project_forms/request_forms.py +++ b/coldfront/core/project/forms_/new_project_forms/request_forms.py @@ -858,7 +858,7 @@ def clean_name(self): class StandaloneClusterDetailsForm(forms.Form): name = forms.CharField( help_text=( - 'The unique name of the project, which must contain only ' + 'The unique name of the standalone cluster, which must contain only ' 'lowercase letters and numbers.'), label='Name', max_length=12, @@ -870,16 +870,8 @@ class StandaloneClusterDetailsForm(forms.Form): message=( 'Name must contain only lowercase letters and numbers.')) ]) - title = forms.CharField( - help_text='A unique, human-readable title for the project.', - label='Title', - max_length=255, - required=True, - validators=[ - MinLengthValidator(4), - ]) description = forms.CharField( - help_text='A few sentences describing your project.', + help_text='A few sentences describing your standalone cluster.', label='Description', validators=[MinLengthValidator(10)], widget=forms.Textarea(attrs={'rows': 3})) @@ -991,3 +983,12 @@ def clean_email(self): '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/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 and Submit


+ + + +

+ Review your selections and submit. +

+ +
+ + + + + + + + + + + + + + + +
Standalone Cluster Name + {{ breadcrumb_project }} +
Principal Investigator (PI) + {{ pi }} +
Manager{{ manager }}
+
+ + +
+ {% csrf_token %} + + {{ wizard.management_form }} + {% if wizard.form.forms %} + {{ wizard.form.management_form }} + {% for form in wizard.form.forms %} + {{ form|crispy }} + {% endfor %} + {% else %} + {{ wizard.form|crispy }} + {% endif %} +
+ {% if wizard.steps.prev %} + + + {% endif %} + +
+
+ +

Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}

+ +{% endblock %} 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 3deb40069d..0f19322190 100644 --- a/coldfront/core/project/views_/new_project_views/request_views.py +++ b/coldfront/core/project/views_/new_project_views/request_views.py @@ -20,6 +20,7 @@ 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 @@ -758,6 +759,7 @@ class StandaloneClusterRequestWizard(LoginRequiredMixin, UserPassesTestMixin, ('new_pi', StandaloneClusterNewPIForm), ('existing_manager', StandaloneClusterExistingManagerForm), ('new_manager', StandaloneClusterNewManagerForm), + ('review_and_submit', StandaloneClusterReviewAndSubmitForm), ] TEMPLATES = { @@ -770,6 +772,8 @@ class StandaloneClusterRequestWizard(LoginRequiredMixin, UserPassesTestMixin, '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 = [ @@ -778,6 +782,7 @@ class StandaloneClusterRequestWizard(LoginRequiredMixin, UserPassesTestMixin, StandaloneClusterNewPIForm, StandaloneClusterExistingManagerForm, StandaloneClusterNewManagerForm, + StandaloneClusterReviewAndSubmitForm, ] logger = logging.getLogger(__name__) @@ -970,7 +975,7 @@ def __handle_create_new_standalone_cluster(self, form_data): project = Project.objects.create( name=project_data['name'], status=status, - title=project_data['title'], + title=project_data['name'].title(), description=project_data['description']) except IntegrityError as e: self.logger.error( @@ -979,7 +984,7 @@ def __handle_create_new_standalone_cluster(self, form_data): try: resource = Resource.objects.create( - name=project_data["title"].title(), resource_type=resource_type + name=project_data["name"].title(), resource_type=resource_type ) resource.description = project_data["description"] resource.save() @@ -1003,18 +1008,21 @@ def __set_data_from_previous_steps(self, step, dictionary): 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.first_name} {pi.last_name} ' - f'({pi.email})') + 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: {first_name} {last_name} ({email})') + f'New PI: {pi_string}'), + 'pi': pi_string }) if step > new_manager_step: @@ -1026,19 +1034,22 @@ def __set_data_from_previous_steps(self, step, dictionary): dictionary.update({ 'breadcrumb_manager': ( f'Existing manager: {manager.first_name} {manager.last_name} ' - f'({manager.email})') + 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: {first_name} {last_name} ({email})') + 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': f'Project: {name}'}) + dictionary.update({'breadcrumb_project': name.title()}) From 6e29b25ae88d6a22db10c7840f7d99d7cb083324 Mon Sep 17 00:00:00 2001 From: Alastair Deng Date: Fri, 5 Jul 2024 16:19:20 -0700 Subject: [PATCH 07/13] fix resource name creation --- .../core/project/views_/new_project_views/request_views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 0f19322190..223cf5cd31 100644 --- a/coldfront/core/project/views_/new_project_views/request_views.py +++ b/coldfront/core/project/views_/new_project_views/request_views.py @@ -983,8 +983,9 @@ def __handle_create_new_standalone_cluster(self, form_data): raise e try: + resource_name = project_data["name"].upper() + " Compute" resource = Resource.objects.create( - name=project_data["name"].title(), resource_type=resource_type + name=resource_name, resource_type=resource_type ) resource.description = project_data["description"] resource.save() From 716fcd47cc5d10edca3ada4a0f399f4f7f49e25e Mon Sep 17 00:00:00 2001 From: Alastair Deng Date: Mon, 8 Jul 2024 11:43:22 -0700 Subject: [PATCH 08/13] refactor new project creation logic --- .../project/management/commands/projects.py | 27 ++---------- .../core/project/utils_/new_project_utils.py | 41 +++++++++++++++++++ 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/coldfront/core/project/management/commands/projects.py b/coldfront/core/project/management/commands/projects.py index dd441f9824..9d94c6d3ff 100644 --- a/coldfront/core/project/management/commands/projects.py +++ b/coldfront/core/project/management/commands/projects.py @@ -16,6 +16,7 @@ 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.resource.models import Resource @@ -102,10 +103,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: @@ -117,25 +117,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/utils_/new_project_utils.py b/coldfront/core/project/utils_/new_project_utils.py index ffcf8fdd14..2edf6f3652 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,42 @@ 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 From 336df7337d4f31bd4c36d5d8c05d8a5dffa550d2 Mon Sep 17 00:00:00 2001 From: Alastair Deng Date: Mon, 8 Jul 2024 11:44:37 -0700 Subject: [PATCH 09/13] fix project creation and allocation creation --- .../views_/new_project_views/request_views.py | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) 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 223cf5cd31..65553cdfe5 100644 --- a/coldfront/core/project/views_/new_project_views/request_views.py +++ b/coldfront/core/project/views_/new_project_views/request_views.py @@ -1,6 +1,8 @@ from allauth.account.models import EmailAddress 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 AllocationStatusChoice from coldfront.core.billing.forms import BillingIDValidationForm from coldfront.core.billing.utils.queries import get_or_create_billing_activity_from_full_id @@ -32,6 +34,9 @@ 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 @@ -41,6 +46,8 @@ 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.common import display_time_zone_current_date +from coldfront.core.utils.email.email_strategy import DropEmailStrategy from django.conf import settings from django.contrib import messages @@ -819,15 +826,7 @@ def done(self, form_list, **kwargs): with transaction.atomic(): pi = self.__handle_pi_data(form_data) manager = self.__handle_manager_data(form_data) - project, resource = self.__handle_create_new_standalone_cluster(form_data) - pi_role = ProjectUserRoleChoice.objects.get(name='Principal Investigator') - manager_role = ProjectUserRoleChoice.objects.get(name='Manager') - active_status = ProjectUserStatusChoice.objects.get(name='Active') - ProjectUser.objects.create( - user=pi, project=project, role=pi_role, status=active_status) - ProjectUser.objects.create( - user=manager, project=project, role=manager_role, - status=active_status) + project, resource = self.__handle_create_new_standalone_cluster(form_data, pi, manager) except Exception as e: self.logger.exception(e) message = 'Unexpected failure. Please contact an administrator.' @@ -958,7 +957,7 @@ def __handle_manager_data(self, form_data): return manager - def __handle_create_new_standalone_cluster(self, form_data): + 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'] @@ -968,20 +967,7 @@ def __handle_create_new_standalone_cluster(self, form_data): resource_type, _ = ResourceType.objects.get_or_create( name='Cluster', description='Cluster servers') - - # Create the new Project. - status = ProjectStatusChoice.objects.get(name='New') - try: - project = Project.objects.create( - name=project_data['name'], - status=status, - title=project_data['name'].title(), - description=project_data['description']) - except IntegrityError as e: - self.logger.error( - f'Project {project_data["name"]} unexpectedly already exists.') - raise e - + try: resource_name = project_data["name"].upper() + " Compute" resource = Resource.objects.create( @@ -993,6 +979,28 @@ def __handle_create_new_standalone_cluster(self, form_data): 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, resource From 67682453cf1fc7f7459841dac735348c7254a5b6 Mon Sep 17 00:00:00 2001 From: Alastair Deng Date: Mon, 8 Jul 2024 13:30:47 -0700 Subject: [PATCH 10/13] remove unnecessary imports --- coldfront/core/project/management/commands/projects.py | 8 -------- .../project/views_/new_project_views/request_views.py | 3 --- 2 files changed, 11 deletions(-) diff --git a/coldfront/core/project/management/commands/projects.py b/coldfront/core/project/management/commands/projects.py index 9d94c6d3ff..55d4ef2d5c 100644 --- a/coldfront/core/project/management/commands/projects.py +++ b/coldfront/core/project/management/commands/projects.py @@ -6,12 +6,7 @@ from flags.state import flag_enabled -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 AllocationStatusChoice 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 @@ -21,10 +16,7 @@ from coldfront.core.project.utils_.new_project_user_utils import NewProjectUserSource from coldfront.core.resource.models import Resource from coldfront.core.resource.utils import get_primary_compute_resource_name -from coldfront.core.statistics.models import ProjectTransaction 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 import logging 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 65553cdfe5..f42a20ce92 100644 --- a/coldfront/core/project/views_/new_project_views/request_views.py +++ b/coldfront/core/project/views_/new_project_views/request_views.py @@ -1,8 +1,6 @@ from allauth.account.models import EmailAddress 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 AllocationStatusChoice from coldfront.core.billing.forms import BillingIDValidationForm from coldfront.core.billing.utils.queries import get_or_create_billing_activity_from_full_id @@ -46,7 +44,6 @@ 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.common import display_time_zone_current_date from coldfront.core.utils.email.email_strategy import DropEmailStrategy from django.conf import settings From 34834f648b02ab20163ad9518f3bd097a7198ec8 Mon Sep 17 00:00:00 2001 From: Alastair Deng Date: Mon, 8 Jul 2024 13:39:53 -0700 Subject: [PATCH 11/13] redirect to project detail page --- .../core/project/views_/new_project_views/request_views.py | 2 ++ 1 file changed, 2 insertions(+) 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 f42a20ce92..5ba0687c3b 100644 --- a/coldfront/core/project/views_/new_project_views/request_views.py +++ b/coldfront/core/project/views_/new_project_views/request_views.py @@ -824,11 +824,13 @@ def done(self, form_list, **kwargs): pi = self.__handle_pi_data(form_data) manager = self.__handle_manager_data(form_data) project, resource = 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.') From 51a7205eb9479c052acb1254d70980184ccb1cca Mon Sep 17 00:00:00 2001 From: Alastair Deng Date: Mon, 8 Jul 2024 14:30:24 -0700 Subject: [PATCH 12/13] move button to admin menu --- coldfront/core/project/templates/project/project_list.html | 6 ------ coldfront/templates/common/navbar_admin.html | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/coldfront/core/project/templates/project/project_list.html b/coldfront/core/project/templates/project/project_list.html index bc87924c7e..0a6fa68af7 100644 --- a/coldfront/core/project/templates/project/project_list.html +++ b/coldfront/core/project/templates/project/project_list.html @@ -16,12 +16,6 @@ Create a project
- {% if user.is_superuser %} - - - Create a standalone cluster - - {% endif %} Join a project diff --git a/coldfront/templates/common/navbar_admin.html b/coldfront/templates/common/navbar_admin.html index 51d6a93c64..fac6d6bc94 100644 --- a/coldfront/templates/common/navbar_admin.html +++ b/coldfront/templates/common/navbar_admin.html @@ -13,6 +13,7 @@ Project Reviews {% endcomment %}
  • User Search
  • +
  • Create Standalone Cluster
  • {% flag_enabled 'LRC_ONLY' as lrc_only %} {% if lrc_only %}
  • LBL Project IDs
  • From 60a5ce20f29f40c533845ace7af3529af4992078 Mon Sep 17 00:00:00 2001 From: Alastair Deng Date: Thu, 18 Jul 2024 12:10:39 -0700 Subject: [PATCH 13/13] slight style changes --- coldfront/core/project/utils_/new_project_utils.py | 4 +++- .../views_/new_project_views/request_views.py | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/coldfront/core/project/utils_/new_project_utils.py b/coldfront/core/project/utils_/new_project_utils.py index 2edf6f3652..563c797a8e 100644 --- a/coldfront/core/project/utils_/new_project_utils.py +++ b/coldfront/core/project/utils_/new_project_utils.py @@ -736,7 +736,9 @@ def vector_request_state_status(vector_request): return ProjectAllocationRequestStatusChoice.objects.get( name='Approved - Processing') -def create_project_with_compute_allocation(project_name, compute_resource, num_service_units): +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. 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 5ba0687c3b..620ecec4ec 100644 --- a/coldfront/core/project/views_/new_project_views/request_views.py +++ b/coldfront/core/project/views_/new_project_views/request_views.py @@ -823,7 +823,8 @@ def done(self, form_list, **kwargs): with transaction.atomic(): pi = self.__handle_pi_data(form_data) manager = self.__handle_manager_data(form_data) - project, resource = self.__handle_create_new_standalone_cluster(form_data, pi, manager) + 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) @@ -931,7 +932,8 @@ def __handle_manager_data(self, form_data): self.logger.error(f'User {email} unexpectedly exists.') raise e - # Set the user's middle name in the UserProfile; generate a manager request. + # Set the user's middle name in the UserProfile; + # generate a manager request. try: manager_profile = manager.userprofile except UserProfile.DoesNotExist as e: @@ -981,7 +983,9 @@ def __handle_create_new_standalone_cluster(self, form_data, pi, manager): # 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) + 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') @@ -1001,7 +1005,7 @@ def __handle_create_new_standalone_cluster(self, form_data, pi, manager): email_strategy=DropEmailStrategy()) runner.run() - return project, resource + return project def __set_data_from_previous_steps(self, step, dictionary): """Update the given dictionary with data from previous steps."""