diff --git a/bootstrap/ansible/wsgi_conf_ssl.tmpl b/bootstrap/ansible/wsgi_conf_ssl.tmpl index a09551a7e..0b79f377f 100644 --- a/bootstrap/ansible/wsgi_conf_ssl.tmpl +++ b/bootstrap/ansible/wsgi_conf_ssl.tmpl @@ -62,7 +62,7 @@ WSGIPassAuthorization On - + {% if cloudflare_enabled | bool %} ServerName {{ cloudflare_local_server_name }} diff --git a/coldfront/api/allocation/tests/allocation_users/test_allocation_users.py b/coldfront/api/allocation/tests/allocation_users/test_allocation_users.py index d08bb919a..d53660910 100644 --- a/coldfront/api/allocation/tests/allocation_users/test_allocation_users.py +++ b/coldfront/api/allocation/tests/allocation_users/test_allocation_users.py @@ -99,7 +99,7 @@ def test_user_filter(self): def test_project_filter(self): """Test that querying by project filters results properly.""" - project = self.project0.name + project = self.fc_project0.name url = self.endpoint_url() query_parameters = { @@ -125,14 +125,14 @@ def test_project_filter(self): def test_resources_filter(self): """Test that querying by resource filters results properly.""" - allocation = Allocation.objects.get(project=self.project0) + allocation = Allocation.objects.get(project=self.fc_project0) resource_type = ResourceType.objects.get(name='Cluster') resource = Resource.objects.create( name='Other Compute', resource_type=resource_type) allocation.resources.add(resource) first = allocation.pk - second = Allocation.objects.get(project=self.project1).pk + second = Allocation.objects.get(project=self.fc_project1).pk allocation_ids_iterator = iter([first, first, first, first, second, second, second, second]) diff --git a/coldfront/api/allocation/tests/allocations/test_allocations.py b/coldfront/api/allocation/tests/allocations/test_allocations.py index a01682c6f..bebfffa6a 100644 --- a/coldfront/api/allocation/tests/allocations/test_allocations.py +++ b/coldfront/api/allocation/tests/allocations/test_allocations.py @@ -71,7 +71,7 @@ def test_no_filters(self): def test_project_filter(self): """Test that querying by project filters results properly.""" - project = self.project0.name + project = self.fc_project0.name url = self.endpoint_url() query_parameters = { @@ -97,7 +97,7 @@ def test_project_filter(self): def test_resources_filter(self): """Test that querying by resource filters results properly.""" - allocation = Allocation.objects.get(project=self.project0) + allocation = Allocation.objects.get(project=self.fc_project0) resource_type = ResourceType.objects.get(name='Cluster') resource = Resource.objects.create( name='Other Compute', resource_type=resource_type) diff --git a/coldfront/api/allocation/tests/allocations/test_cluster_access_requests.py b/coldfront/api/allocation/tests/allocations/test_cluster_access_requests.py index 960b846c0..b4b889206 100644 --- a/coldfront/api/allocation/tests/allocations/test_cluster_access_requests.py +++ b/coldfront/api/allocation/tests/allocations/test_cluster_access_requests.py @@ -42,8 +42,9 @@ def setUp(self): status_choices = ClusterAccessRequestStatusChoice.objects.all() for i in range(4): kwargs = { - 'allocation_user': AllocationUser.objects.get(user__username=f'user{i}', - allocation__project__name='project0'), + 'allocation_user': AllocationUser.objects.get( + user__username=f'user{i}', + allocation__project__name='fc_project0'), 'request_time': utc_now_offset_aware(), } if i == 0: @@ -60,7 +61,7 @@ def setUp(self): self.allocation_user0 = \ AllocationUser.objects.get(user=self.user0, - allocation__project=self.project0) + allocation__project=self.fc_project0) self.allocation_su_attr = AllocationAttribute.objects.get( allocation_attribute_type__name='Service Units', @@ -193,7 +194,7 @@ def _get_cluster_account_status_attr(self, allocation_user): return cluster_access_attribute def _assert_complete_emails_sent(self): - email_body = [f'now has access to the project {self.project0.name}.', + email_body = [f'now has access to the project {self.fc_project0.name}.', f'supercluster username is - {self.new_username}', f'If this is the first time you are accessing', f'start with the below Logging In page:'] @@ -209,7 +210,7 @@ def _assert_complete_emails_sent(self): self.assertEqual(settings.EMAIL_SENDER, email.from_email) def _assert_denial_emails_sent(self): - email_body = [f'access request under project {self.project0.name}', + email_body = [f'access request under project {self.fc_project0.name}', f'and allocation ' f'{self.allocation_user0.allocation.pk} ' f'has been denied.'] @@ -300,7 +301,7 @@ def test_read_only_fields_ignored(self): 'allocation_user': {'id': 12, 'allocation': 3, 'user': 'user3', - 'project': 'project0', + 'project': 'fc_project0', 'status': 'Complete'}, 'username': 'new_username', 'cluster_uid': '1234' diff --git a/coldfront/api/allocation/tests/test_allocation_base.py b/coldfront/api/allocation/tests/test_allocation_base.py index 5731ec304..c93368d54 100644 --- a/coldfront/api/allocation/tests/test_allocation_base.py +++ b/coldfront/api/allocation/tests/test_allocation_base.py @@ -57,8 +57,8 @@ def setUp(self): for i in range(2): # Create a Project and ProjectUsers. project = Project.objects.create( - name=f'project{i}', status=project_status) - setattr(self, f'project{i}', project) + name=f'fc_project{i}', status=project_status) + setattr(self, f'fc_project{i}', project) for j in range(4): ProjectUser.objects.create( user=getattr(self, f'user{j}'), project=project, diff --git a/coldfront/api/statistics/tests/test_job_view_set.py b/coldfront/api/statistics/tests/test_job_view_set.py index 630d37523..0f85fcfe7 100644 --- a/coldfront/api/statistics/tests/test_job_view_set.py +++ b/coldfront/api/statistics/tests/test_job_view_set.py @@ -73,7 +73,7 @@ def setUp(self): allocation_amount = Decimal('1000.00') for i in range(self.num_projects): project = Project.objects.create( - name=f'PROJECT_{i}', status=project_status) + name=f'fc_project_{i}', status=project_status) allocation_objects = create_project_allocation( project, allocation_amount) allocation_objects.allocation.start_date = \ @@ -88,7 +88,7 @@ def setUp(self): for i in range(self.num_users): user = User.objects.get(username=f'user{i}') project = Project.objects.get( - name=f'PROJECT_{i // self.num_projects}') + name=f'fc_project_{i // self.num_projects}') status = ProjectUserStatusChoice.objects.get(name='Active') ProjectUser.objects.create( user=user, project=project, role=role, status=status) @@ -212,9 +212,9 @@ def test_account_filter(self): self.assert_results(url, status_code, count) # PROJECT_0 submitted 4 jobs. try: - account = Project.objects.get(name='PROJECT_0') + account = Project.objects.get(name='fc_project_0') except Project.DoesNotExist: - self.fail('A Project with name "PROJECT_0" should exist.') + self.fail('A Project with name "fc_project_0" should exist.') url = TestJobList.get_url(account=account.name) status_code, count = 200, 4 results_dict = self.assert_results(url, status_code, count) diff --git a/coldfront/core/allocation/utils.py b/coldfront/core/allocation/utils.py index fecb39b1a..c19c5a8c1 100644 --- a/coldfront/core/allocation/utils.py +++ b/coldfront/core/allocation/utils.py @@ -24,9 +24,7 @@ SecureDirAddUserRequest, SecureDirAddUserRequestStatusChoice, SecureDirRemoveUserRequest, - SecureDirRemoveUserRequestStatusChoice, - SecureDirRequest, - SecureDirRequestStatusChoice) + SecureDirRemoveUserRequestStatusChoice) from coldfront.core.allocation.signals import allocation_activate_user from coldfront.core.project.models import Project from coldfront.core.resource.models import Resource @@ -157,26 +155,20 @@ def get_project_compute_resource_name(project_obj): The name is based on currently-enabled flags (i.e., BRC, LRC). If one cannot be determined, return the empty string.""" - if flag_enabled('BRC_ONLY'): - if project_obj.name == 'abc': - resource_name = 'ABC Compute' - elif project_obj.name.startswith('vector_'): - resource_name = 'Vector Compute' - else: - resource_name = get_primary_compute_resource_name() - return resource_name - if flag_enabled('LRC_ONLY'): - computing_allowance_interface = ComputingAllowanceInterface() - project_name_prefixes = tuple([ - computing_allowance_interface.code_from_name(allowance.name) - for allowance in computing_allowance_interface.allowances()]) - if project_obj.name.startswith(project_name_prefixes): - resource_name = get_primary_compute_resource_name() - else: - # TODO: Verify this behavior. - resource_name = f'{project_obj.name.upper()} Compute' - return resource_name - return '' + project_name = project_obj.name + + computing_allowance_interface = ComputingAllowanceInterface() + project_name_prefixes = tuple([ + computing_allowance_interface.code_from_name(allowance.name) + for allowance in computing_allowance_interface.allowances()]) + if project_name.startswith(project_name_prefixes): + return get_primary_compute_resource_name() + + if flag_enabled('BRC_ONLY') and project_name.startswith('vector_'): + cluster_name = 'Vector' + else: + cluster_name = project_name.upper() + return f'{cluster_name} Compute' def get_project_compute_allocation(project_obj): diff --git a/coldfront/core/billing/management/__init__.py b/coldfront/core/billing/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/coldfront/core/billing/management/commands/__init__.py b/coldfront/core/billing/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/coldfront/core/billing/management/commands/billing_ids.py b/coldfront/core/billing/management/commands/billing_ids.py new file mode 100644 index 000000000..229e44670 --- /dev/null +++ b/coldfront/core/billing/management/commands/billing_ids.py @@ -0,0 +1,314 @@ +import logging + +from django.contrib.auth.models import User +from django.core.management import CommandError +from django.core.management.base import BaseCommand + +from coldfront.core.billing.models import BillingActivity +from coldfront.core.billing.utils import ProjectBillingActivityManager +from coldfront.core.billing.utils import ProjectUserBillingActivityManager +from coldfront.core.billing.utils import UserBillingActivityManager +from coldfront.core.billing.utils.queries import get_billing_activity_from_full_id +from coldfront.core.billing.utils.queries import get_or_create_billing_activity_from_full_id +from coldfront.core.billing.utils.queries import is_billing_id_well_formed +from coldfront.core.billing.utils.validation import is_billing_id_valid +from coldfront.core.project.models import Project +from coldfront.core.project.models import ProjectUser +from coldfront.core.utils.common import add_argparse_dry_run_argument + + +"""An admin command for creating and setting billing IDs.""" + + +class Command(BaseCommand): + + help = 'Create and set billing IDs.' + + logger = logging.getLogger(__name__) + + def add_arguments(self, parser): + """Define subcommands with different functions.""" + subparsers = parser.add_subparsers( + dest='subcommand', + help='The subcommand to run.', + title='subcommands') + subparsers.required = True + self._add_create_subparser(subparsers) + self._add_set_subparser(subparsers) + + def handle(self, *args, **options): + """Call the handler for the provided subcommand.""" + subcommand = options['subcommand'] + if subcommand == 'create': + self._handle_create(*args, **options) + elif subcommand == 'set': + self._handle_set(*args, **options) + + @staticmethod + def _add_create_subparser(parsers): + """Add a subparser for the 'create' subcommand.""" + parser = parsers.add_parser('create', help='Create a billing ID.') + add_billing_id_argument(parser) + add_ignore_invalid_argument(parser) + add_argparse_dry_run_argument(parser) + + @staticmethod + def _add_set_subparser(parsers): + """Add a subparser for the 'set' subcommand.""" + parser = parsers.add_parser( + 'set', help='Set a billing ID for a particular entity.') + subparsers = parser.add_subparsers( + dest='set_subcommand', + help='The subcommand to run.', + title='set_subcommands') + subparsers.required = True + + project_default_parser = subparsers.add_parser( + 'project_default', + help=( + 'Set the default billing ID for the Project with the given ' + 'name.')) + add_project_name_argument(project_default_parser) + add_billing_id_argument(project_default_parser) + add_ignore_invalid_argument(project_default_parser) + add_argparse_dry_run_argument(project_default_parser) + + recharge_parser = subparsers.add_parser( + 'recharge', + help=( + 'Set the billing ID to be used for the Recharge fee for the ' + 'given User with the given user on the Project with the given ' + 'name.')) + add_project_name_argument(recharge_parser) + add_username_argument(recharge_parser) + add_billing_id_argument(recharge_parser) + add_ignore_invalid_argument(recharge_parser) + add_argparse_dry_run_argument(recharge_parser) + + user_account_parser = subparsers.add_parser( + 'user_account', + help=( + 'Set the billing ID to tbe used for the user account fee for ' + 'the User with the given username.')) + add_username_argument(user_account_parser) + add_billing_id_argument(user_account_parser) + add_ignore_invalid_argument(user_account_parser) + add_argparse_dry_run_argument(user_account_parser) + + @staticmethod + def _get_billing_activity_or_error(full_id): + """Return the BillingActivity corresponding to the given + fully-formed billing ID, if it exists, else raise a + CommandError.""" + if not is_billing_id_well_formed(full_id): + raise CommandError(f'Billing ID {full_id} is malformed.') + billing_activity = get_billing_activity_from_full_id(full_id) + if not isinstance(billing_activity, BillingActivity): + raise CommandError(f'Billing ID {full_id} does not exist.') + return billing_activity + + @staticmethod + def _get_project_or_error(project_name): + """Return the Project with the given name, if it exists, else + raise a CommandError.""" + try: + return Project.objects.get(name=project_name) + except Project.DoesNotExist: + raise CommandError( + f'Project with name "{project_name}" does not exist.') + + @staticmethod + def _get_project_user_or_error(project, user): + """Return the ProjectUser associated with the given Project and + User, if it exists, else raise a CommandError.""" + try: + return ProjectUser.objects.get(project=project, user=user) + except ProjectUser.DoesNotExist: + raise CommandError( + f'ProjectUser for Project {project.name} and User ' + f'{user.username} does not exist.') + + @staticmethod + def _get_user_or_error(username): + """Return the User with the given username, if it exists, else + raise a CommandError.""" + try: + return User.objects.get(username=username) + except User.DoesNotExist: + raise CommandError( + f'User with username "{username}" does not exist.') + + def _handle_create(self, *args, **options): + """Handle the 'create' subcommand.""" + full_id = options['billing_id'] + if not is_billing_id_well_formed(full_id): + raise CommandError(f'Billing ID {full_id} is malformed.') + billing_activity = get_billing_activity_from_full_id(full_id) + if isinstance(billing_activity, BillingActivity): + raise CommandError(f'Billing ID {full_id} already exists.') + self._validate_billing_id( + full_id, invalid_allowed=options['ignore_invalid']) + + dry_run = options['dry_run'] + if dry_run: + message = ( + f'Would create a BillingActivity for billing ID {full_id}.') + self.stdout.write(self.style.WARNING(message)) + else: + try: + billing_activity = get_or_create_billing_activity_from_full_id( + full_id) + except Exception as e: + self.logger.exception(e) + raise CommandError(e) + else: + message = ( + f'Created BillingActivity {billing_activity.pk} for ' + f'billing ID {full_id}.') + self.stdout.write(self.style.SUCCESS(message)) + self.logger.info(message) + + def _handle_set(self, *args, **options): + """Handle the 'set' subcommand.""" + billing_activity = self._get_billing_activity_or_error( + options['billing_id']) + self._validate_billing_id( + billing_activity.full_id(), + invalid_allowed=options['ignore_invalid']) + + dry_run = options['dry_run'] + set_subcommand = options['set_subcommand'] + if set_subcommand == 'project_default': + project = self._get_project_or_error(options['project_name']) + self._handle_set_project_default( + project, billing_activity, dry_run=dry_run) + elif set_subcommand == 'recharge': + project = self._get_project_or_error(options['project_name']) + user = self._get_user_or_error(options['username']) + project_user = self._get_project_user_or_error(project, user) + self._handle_set_recharge( + project_user, billing_activity, dry_run=dry_run) + elif set_subcommand == 'user_account': + user = self._get_user_or_error(options['username']) + self._handle_set_user_account( + user, billing_activity, dry_run=dry_run) + + def _handle_set_project_default(self, project, billing_activity, + dry_run=False): + """Handle the 'project_default' subcommand of the 'set' + subcommand.""" + entity = Entity( + project, + f'Project {project.name} ({project.pk})', + ProjectBillingActivityManager) + self._set_billing_activity_for_entity( + entity, billing_activity, dry_run=dry_run) + + def _handle_set_recharge(self, project_user, billing_activity, + dry_run=False): + """Handle the 'recharge' subcommand of the 'set' subcommand.""" + entity = Entity( + project_user, + (f'ProjectUser {project_user.project.name}-' + f'{project_user.user.username} ({project_user.pk})'), + ProjectUserBillingActivityManager) + self._set_billing_activity_for_entity( + entity, billing_activity, dry_run=dry_run) + + def _handle_set_user_account(self, user, billing_activity, dry_run=False): + """Handle the 'user_account' subcommand of the 'set' + subcommand.""" + entity = Entity( + user, + f'User {user.username} ({user.pk})', + UserBillingActivityManager) + self._set_billing_activity_for_entity( + entity, billing_activity, dry_run=dry_run) + + def _set_billing_activity_for_entity(self, entity, billing_activity, + dry_run=False): + """Set the BillingActivity for the given Entity to the given + one. Optionally display updates instead of performing them.""" + instance = entity.instance + instance_str = entity.instance_str + manager_class = entity.manager_class + + manager = manager_class(instance) + + previous = manager.billing_activity + previous_str = ( + previous.full_id() if isinstance(previous, BillingActivity) + else None) + new_str = billing_activity.full_id() + + if dry_run: + phrase = 'Would update' + style = self.style.WARNING + else: + phrase = 'Updated' + style = self.style.SUCCESS + try: + manager.billing_activity = billing_activity + except Exception as e: + self.logger.exception(e) + raise CommandError(e) + message = ( + f'{phrase} billing ID for {instance_str} from {previous_str} to ' + f'{new_str}.') + self.stdout.write(style(message)) + if not dry_run: + self.logger.info(message) + + def _validate_billing_id(self, billing_id, invalid_allowed=False): + """Check whether the given billing ID (str) is currently valid. + If not, raise a CommandError or write a warning to stdout based + on whether invalidity is allowed.""" + if not is_billing_id_valid(billing_id): + message = f'Billing ID {billing_id} is invalid.' + if invalid_allowed: + message += ' Proceeding anyway.' + self.stdout.write(self.style.WARNING(message)) + else: + raise CommandError(message) + + +def add_billing_id_argument(parser): + """Add an argument 'billing_id' to the given argparse parser to + accept a billing ID.""" + parser.add_argument( + 'billing_id', help='A billing ID (e.g., 123456-789).', type=str) + + +def add_ignore_invalid_argument(parser): + """Add an optional argument '--ignore_invalid' to the given argparse + parser to indicate that an action involving a billing ID should be + taken, even if the ID is invalid.""" + parser.add_argument( + '--ignore_invalid', + action='store_true', + help='Allow the billing ID to be invalid.') + + +def add_project_name_argument(parser): + """Add an argument 'project_name' to the given argparse parser to + accept the name of a Project.""" + parser.add_argument( + 'project_name', help='The name of a project.', type=str) + + +def add_username_argument(parser): + """Add an argument 'username' to the given argparse parser to accept + the username of a User.""" + parser.add_argument('username', help='The username of a user.', type=str) + + +class Entity(object): + """A wrapper for storing details of a database object to set a + BillingActivity for.""" + + def __init__(self, instance, instance_str, manager_class): + """Store the instance to update, a string representation of it, + and the BillingActivity manager class to use.""" + self.instance = instance + self.instance_str = instance_str + self.manager_class = manager_class diff --git a/coldfront/core/billing/tests/__init__.py b/coldfront/core/billing/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/coldfront/core/billing/tests/test_billing_base.py b/coldfront/core/billing/tests/test_billing_base.py new file mode 100644 index 000000000..3a08058b5 --- /dev/null +++ b/coldfront/core/billing/tests/test_billing_base.py @@ -0,0 +1,48 @@ +from decimal import Decimal + +from django.contrib.auth.models import User + +from coldfront.core.allocation.models import Allocation +from coldfront.core.allocation.models import AllocationStatusChoice +from coldfront.core.project.models import ProjectUser +from coldfront.core.project.tests.utils import create_project_and_request +from coldfront.core.resource.models import Resource +from coldfront.core.project.utils_.new_project_utils import SavioProjectProcessingRunner +from coldfront.core.project.utils_.renewal_utils import get_current_allowance_year_period +from coldfront.core.resource.utils import get_primary_compute_resource +from coldfront.core.utils.email.email_strategy import DropEmailStrategy +from coldfront.core.utils.tests.test_base import enable_deployment +from coldfront.core.utils.tests.test_base import TestBase + + +class TestBillingBase(TestBase): + """A base class for testing Billing-related functionality.""" + + @enable_deployment('LRC') + def setUp(self): + """Set up test data.""" + super().setUp() + + self.username = 'user' + self.user = User.objects.create_user( + email='user@email.com', username=self.username) + + computing_allowance = Resource.objects.get(name='Recharge Allocation') + allocation_period = get_current_allowance_year_period() + self.project_name = 'ac_project' + self.project, new_project_request = create_project_and_request( + self.project_name, 'New', computing_allowance, allocation_period, + self.user, self.user, 'Under Review') + self.allocation = Allocation.objects.create( + project=self.project, + status=AllocationStatusChoice.objects.get(name='New')) + self.allocation.resources.add(get_primary_compute_resource()) + + runner = SavioProjectProcessingRunner( + new_project_request, Decimal('300000.00'), + email_strategy=DropEmailStrategy()) + runner.run() + + self.project.refresh_from_db() + self.project_user = ProjectUser.objects.get( + project=self.project, user=self.user) diff --git a/coldfront/core/billing/tests/test_commands/__init__.py b/coldfront/core/billing/tests/test_commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/coldfront/core/billing/tests/test_commands/test_billing_ids.py b/coldfront/core/billing/tests/test_commands/test_billing_ids.py new file mode 100644 index 000000000..b310385ec --- /dev/null +++ b/coldfront/core/billing/tests/test_commands/test_billing_ids.py @@ -0,0 +1,400 @@ +from io import StringIO + +from django.core.management import call_command +from django.core.management import CommandError + +from coldfront.core.billing.models import BillingActivity +from coldfront.core.billing.tests.test_billing_base import TestBillingBase +from coldfront.core.billing.utils import ProjectBillingActivityManager +from coldfront.core.billing.utils import ProjectUserBillingActivityManager +from coldfront.core.billing.utils import UserBillingActivityManager +from coldfront.core.billing.utils.queries import get_billing_activity_from_full_id +from coldfront.core.billing.utils.queries import is_billing_id_well_formed +from coldfront.core.billing.utils.validation import is_billing_id_valid +from coldfront.core.utils.tests.test_base import enable_deployment + + +class TestBillingIds(TestBillingBase): + """A class for testing the billing_ids management command.""" + + @enable_deployment('LRC') + def setUp(self): + """Set up test data.""" + super().setUp() + + self.command = BillingIdsCommand() + + def test_create_billing_id_existent(self): + """Test that, when the given billing ID already exists, the + 'create' subcommand raises an error.""" + billing_id = '123456-788' + self.assertIsNone(get_billing_activity_from_full_id(billing_id)) + + _, error = self.command.create(billing_id) + self.assertFalse(error) + + billing_activity = get_billing_activity_from_full_id(billing_id) + self.assertTrue(isinstance(billing_activity, BillingActivity)) + + with self.assertRaises(CommandError) as cm: + self.command.create(billing_id) + self.assertIn('already exists', str(cm.exception)) + + def test_create_dry_run(self): + """Test that, when the --dry_run flag is given to the 'create' + subcommand, changes are displayed, but not performed.""" + billing_id = '123456-788' + self.assertIsNone(get_billing_activity_from_full_id(billing_id)) + + output, error = self.command.create(billing_id, dry_run=True) + self.assertIn('Would create', output) + self.assertFalse(error) + + self.assertIsNone(get_billing_activity_from_full_id(billing_id)) + + def test_create_billing_id_invalid(self): + """Test that, when the given billing ID is invalid, the 'create' + subcommand raises an error, unless the --ignore_invalid flag is + given, in which case a warning is raised before proceeding.""" + billing_id = '123456-789' + self.assertIsNone(get_billing_activity_from_full_id(billing_id)) + self.assertFalse(is_billing_id_valid(billing_id)) + + with self.assertRaises(CommandError) as cm: + self.command.create(billing_id) + self.assertIn('is invalid', str(cm.exception)) + + output, error = self.command.create(billing_id, ignore_invalid=True) + self.assertIn('is invalid', output) + self.assertIn('Proceeding anyway', output) + self.assertIn('Created', output) + self.assertFalse(error) + + billing_activity = get_billing_activity_from_full_id(billing_id) + self.assertTrue(isinstance(billing_activity, BillingActivity)) + + def test_create_billing_id_malformed(self): + """Test that, when the given billing ID is malformed, the + 'create' subcommand raises an error.""" + billing_id = '12345-67' + self.assertIsNone(get_billing_activity_from_full_id(billing_id)) + self.assertFalse(is_billing_id_well_formed(billing_id)) + + with self.assertRaises(CommandError) as cm: + self.command.create(billing_id) + self.assertIn('is malformed', str(cm.exception)) + + self.assertIsNone(get_billing_activity_from_full_id(billing_id)) + + def test_create_success(self): + """Test that the 'create' subcommand successfully creates a + billing ID.""" + billing_id = '123456-788' + self.assertIsNone(get_billing_activity_from_full_id(billing_id)) + + _, error = self.command.create(billing_id) + self.assertFalse(error) + + billing_activity = get_billing_activity_from_full_id(billing_id) + self.assertTrue(isinstance(billing_activity, BillingActivity)) + + def test_set_billing_id_invalid(self): + """Test that, when the given billing ID is invalid, each of the + subcommands of the 'set' subcommand raises an error, unless the + --ignore_invalid flag is given, in which case a warning is + raised before proceeding.""" + billing_id = '123456-789' + self.command.create(billing_id, ignore_invalid=True) + + billing_activity = get_billing_activity_from_full_id(billing_id) + + calls = [ + { + 'command': self.command.set_project_default, + 'manager': ProjectBillingActivityManager(self.project), + 'args': [self.project_name, billing_id], + }, + { + 'command': self.command.set_recharge, + 'manager': ProjectUserBillingActivityManager( + self.project_user), + 'args': [self.project_name, self.user.username, billing_id], + }, + { + 'command': self.command.set_user_account, + 'manager': UserBillingActivityManager(self.user), + 'args': [self.user.username, billing_id], + } + ] + + for call in calls: + command = call['command'] + manager = call['manager'] + args = call['args'] + + self.assertIsNone(manager.billing_activity) + with self.assertRaises(CommandError) as cm: + command(*args) + self.assertIn('is invalid', str(cm.exception)) + self.assertIsNone(manager.billing_activity) + + output, error = command(*args, ignore_invalid=True) + self.assertIn('is invalid', output) + self.assertIn('Proceeding anyway', output) + self.assertIn('Updated', output) + self.assertFalse(error) + + self.assertEqual(manager.billing_activity, billing_activity) + + def test_set_billing_id_malformed(self): + """Test that, when the given billing ID is malformed, each of + the subcommands of the 'set' subcommand raises an error.""" + billing_id = '12345-67' + self.assertIsNone(get_billing_activity_from_full_id(billing_id)) + self.assertFalse(is_billing_id_well_formed(billing_id)) + + calls = [ + { + 'command': self.command.set_project_default, + 'manager': ProjectBillingActivityManager(self.project), + 'args': [self.project_name, billing_id], + } + ] + + for call in calls: + command = call['command'] + manager = call['manager'] + args = call['args'] + + self.assertIsNone(manager.billing_activity) + with self.assertRaises(CommandError) as cm: + command(*args) + self.assertIn('is malformed', str(cm.exception)) + self.assertIsNone(manager.billing_activity) + + def test_set_billing_id_nonexistent(self): + """Test that, when the given billing ID does not already exist, + each of the subcommands of the 'set' subcommand raises an + error.""" + billing_id = '123456-789' + self.assertIsNone(get_billing_activity_from_full_id(billing_id)) + + calls = [ + { + 'command': self.command.set_project_default, + 'manager': ProjectBillingActivityManager(self.project), + 'args': [self.project_name, billing_id], + }, + { + 'command': self.command.set_recharge, + 'manager': ProjectUserBillingActivityManager( + self.project_user), + 'args': [self.project_name, self.user.username, billing_id], + }, + { + 'command': self.command.set_user_account, + 'manager': UserBillingActivityManager(self.user), + 'args': [self.user.username, billing_id], + } + ] + + for call in calls: + command = call['command'] + manager = call['manager'] + args = call['args'] + + self.assertIsNone(manager.billing_activity) + with self.assertRaises(CommandError) as cm: + command(*args) + self.assertIn('does not exist', str(cm.exception)) + self.assertIsNone(manager.billing_activity) + + def test_set_project_default_dry_run(self): + """Test that, when the --dry_run flag is given to the + 'project_default' subcommand of the 'set' subcommand, changes + are displayed, but not performed.""" + billing_id = '123456-788' + self.command.create(billing_id) + + manager = ProjectBillingActivityManager(self.project) + self.assertIsNone(manager.billing_activity) + + output, error = self.command.set_project_default( + self.project_name, billing_id, dry_run=True) + self.assertIn('Would update', output) + self.assertFalse(error) + + self.assertIsNone(manager.billing_activity) + + def test_set_project_default_success(self): + """Test that the 'project_default' subcommand of the 'set' + subcommand successfully sets a billing ID for a Project.""" + billing_id = '123456-788' + self.command.create(billing_id) + billing_activity = get_billing_activity_from_full_id(billing_id) + + manager = ProjectBillingActivityManager(self.project) + self.assertIsNone(manager.billing_activity) + + output, error = self.command.set_project_default( + self.project_name, billing_id) + self.assertIn('Updated', output) + self.assertFalse(error) + + self.assertEqual(manager.billing_activity, billing_activity) + + other_billing_id = '123456-790' + self.command.create(other_billing_id) + other_billing_activity = get_billing_activity_from_full_id( + other_billing_id) + + output, error = self.command.set_project_default( + self.project_name, other_billing_id) + self.assertIn('Updated', output) + self.assertFalse(error) + + self.assertEqual(manager.billing_activity, other_billing_activity) + + def test_set_recharge_dry_run(self): + """Test that, when the --dry_run flag is given to the 'recharge' + subcommand of the 'set' subcommand, changes are displayed, but + not performed.""" + billing_id = '123456-788' + self.command.create(billing_id) + + manager = ProjectUserBillingActivityManager(self.project_user) + self.assertIsNone(manager.billing_activity) + + output, error = self.command.set_recharge( + self.project_name, self.user.username, billing_id, dry_run=True) + self.assertIn('Would update', output) + self.assertFalse(error) + + self.assertIsNone(manager.billing_activity) + + def test_set_recharge_success(self): + """Test that the 'recharge' subcommand of the 'set' subcommand + successfully sets a billing ID for a ProjectUser.""" + billing_id = '123456-788' + self.command.create(billing_id) + billing_activity = get_billing_activity_from_full_id(billing_id) + + manager = ProjectUserBillingActivityManager(self.project_user) + self.assertIsNone(manager.billing_activity) + + output, error = self.command.set_recharge( + self.project_name, self.user.username, billing_id) + self.assertIn('Updated', output) + self.assertFalse(error) + + self.assertEqual(manager.billing_activity, billing_activity) + + other_billing_id = '123456-790' + self.command.create(other_billing_id) + other_billing_activity = get_billing_activity_from_full_id( + other_billing_id) + + output, error = self.command.set_recharge( + self.project_name, self.user.username, other_billing_id) + self.assertIn('Updated', output) + self.assertFalse(error) + + self.assertEqual(manager.billing_activity, other_billing_activity) + + def test_set_user_account_dry_run(self): + """Test that, when the --dry_run flag is given to the + 'user_account' subcommand of the 'set' subcommand, changes are + displayed, but not performed.""" + billing_id = '123456-788' + self.command.create(billing_id) + + manager = UserBillingActivityManager(self.user) + self.assertIsNone(manager.billing_activity) + + output, error = self.command.set_user_account( + self.user.username, billing_id, dry_run=True) + self.assertIn('Would update', output) + self.assertFalse(error) + + self.assertIsNone(manager.billing_activity) + + def test_set_user_account_success(self): + """Test that the 'user_account' subcommand of the 'set' + subcommand successfully sets a billing ID for a User.""" + billing_id = '123456-788' + self.command.create(billing_id) + billing_activity = get_billing_activity_from_full_id(billing_id) + + manager = UserBillingActivityManager(self.user) + self.assertIsNone(manager.billing_activity) + + output, error = self.command.set_user_account( + self.user.username, billing_id) + self.assertIn('Updated', output) + self.assertFalse(error) + + self.assertEqual(manager.billing_activity, billing_activity) + + other_billing_id = '123456-790' + self.command.create(other_billing_id) + other_billing_activity = get_billing_activity_from_full_id( + other_billing_id) + + output, error = self.command.set_user_account( + self.user.username, other_billing_id) + self.assertIn('Updated', output) + self.assertFalse(error) + + self.assertEqual(manager.billing_activity, other_billing_activity) + + +class BillingIdsCommand(object): + """A wrapper class over the 'billing_ids' management command.""" + + command_name = 'billing_ids' + + def call_subcommand(self, name, *args): + """Call the subcommand with the given name and arguments. Return + output written to stdout and stderr.""" + out, err = StringIO(), StringIO() + args = [self.command_name, name, *args] + kwargs = {'stdout': out, 'stderr': err} + call_command(*args, **kwargs) + return out.getvalue(), err.getvalue() + + def create(self, billing_id, **flags): + """Call the 'create' subcommand with the given billing ID, and + flag values.""" + args = ['create', billing_id] + self._add_flags_to_args(args, **flags) + return self.call_subcommand(*args) + + def set_project_default(self, project_name, billing_id, **flags): + """Call the 'project_default' subcommand of the 'set' subcommand + with the given Project name, billing ID, and flag values.""" + args = ['set', 'project_default', project_name, billing_id] + self._add_flags_to_args(args, **flags) + return self.call_subcommand(*args) + + def set_recharge(self, project_name, username, billing_id, **flags): + """Call the 'recharge' subcommand of the 'set' subcommand with + the given Project name, username, billing ID, and flag + values.""" + args = ['set', 'recharge', project_name, username, billing_id] + self._add_flags_to_args(args, **flags) + return self.call_subcommand(*args) + + def set_user_account(self, username, billing_id, **flags): + """Call the 'user_account' subcommand of the 'set' subcommand + with the given username, billing ID, and flag values.""" + args = ['set', 'user_account', username, billing_id] + self._add_flags_to_args(args, **flags) + return self.call_subcommand(*args) + + @staticmethod + def _add_flags_to_args(args, **flags): + """Given a list of arguments to the command and a dict of flag + values, add the latter to the former.""" + for key in ('dry_run', 'ignore_invalid'): + if flags.get(key, False): + args.append(f'--{key}') diff --git a/coldfront/core/billing/tests/test_utils/__init__.py b/coldfront/core/billing/tests/test_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/coldfront/core/billing/tests/test_utils/test_billing_activity_managers.py b/coldfront/core/billing/tests/test_utils/test_billing_activity_managers.py new file mode 100644 index 000000000..acad5e186 --- /dev/null +++ b/coldfront/core/billing/tests/test_utils/test_billing_activity_managers.py @@ -0,0 +1,397 @@ +from coldfront.core.allocation.models import AllocationAttribute +from coldfront.core.allocation.models import AllocationAttributeType +from coldfront.core.allocation.models import AllocationUser +from coldfront.core.allocation.models import AllocationUserAttribute +from coldfront.core.billing.models import BillingActivity +from coldfront.core.billing.tests.test_billing_base import TestBillingBase +from coldfront.core.billing.utils import ProjectBillingActivityManager +from coldfront.core.billing.utils import ProjectUserBillingActivityManager +from coldfront.core.billing.utils import UserBillingActivityManager +from coldfront.core.billing.utils.queries import get_or_create_billing_activity_from_full_id +from coldfront.core.user.models import UserProfile + + +class TestBillingActivityManagerBase(TestBillingBase): + """A base class for testing billing activity manager utility + classes.""" + + def setUp(self): + """Set up test data.""" + super().setUp() + + self.billing_id_1 = '123456-789' + self.billing_activity_1 = get_or_create_billing_activity_from_full_id( + self.billing_id_1) + self.billing_id_2 = '987654-321' + self.billing_activity_2 = get_or_create_billing_activity_from_full_id( + self.billing_id_2) + + +class TestProjectBillingActivityManager(TestBillingActivityManagerBase): + """A class for testing the ProjectBillingActivityManager utility + class.""" + + def setUp(self): + """Set up test data.""" + super().setUp() + + self.allocation_attribute_type = AllocationAttributeType.objects.get( + name='Billing Activity') + self.manager = ProjectBillingActivityManager(self.project) + + def test_get_invalid_allocation_attribute_value(self): + """Test that the getter raises an exception when the given + Project's AllocationAttribute contains a value that does not + correspond to a BillingActivity.""" + allocation_attribute_kwargs = { + 'allocation_attribute_type': self.allocation_attribute_type, + 'allocation': self.allocation, + 'value': 'invalid', + } + allocation_attribute = AllocationAttribute.objects.create( + **allocation_attribute_kwargs) + with self.assertRaises(ValueError) as cm: + _ = self.manager.billing_activity + self.assertIn('invalid literal', str(cm.exception)) + + allocation_attribute.value = str( + self.billing_activity_1.pk + self.billing_activity_2.pk + 1) + allocation_attribute.save() + + with self.assertRaises(BillingActivity.DoesNotExist) as cm: + _ = self.manager.billing_activity + self.assertIn('does not exist', str(cm.exception)) + + def test_get_nonexistent_allocation_attribute(self): + """Test that the getter returns None when the given Project does + not have an associated AllocationAttribute for storing a + BillingActivity, but returns the contained BillingActivity once + it has been created.""" + allocation_attribute_kwargs = { + 'allocation_attribute_type': self.allocation_attribute_type, + 'allocation': self.allocation, + } + self.assertFalse( + AllocationAttribute.objects.filter(**allocation_attribute_kwargs)) + self.assertIsNone(self.manager.billing_activity) + + allocation_attribute_kwargs['value'] = str(self.billing_activity_1.pk) + AllocationAttribute.objects.create(**allocation_attribute_kwargs) + self.assertEqual( + self.manager.billing_activity, self.billing_activity_1) + + def test_get_refreshes_value(self): + """Test that the getter returns the most up-to-date + BillingActivity stored in the AllocationAttribute, accounting + for changes from elsewhere.""" + allocation_attribute_kwargs = { + 'allocation_attribute_type': self.allocation_attribute_type, + 'allocation': self.allocation, + 'value': str(self.billing_activity_1.pk), + } + allocation_attribute = AllocationAttribute.objects.create( + **allocation_attribute_kwargs) + self.assertEqual( + self.manager.billing_activity, self.billing_activity_1) + + allocation_attribute.value = str(self.billing_activity_2.pk) + allocation_attribute.save() + self.assertEqual( + self.manager.billing_activity, self.billing_activity_2) + + allocation_attribute.delete() + self.assertIsNone(self.manager.billing_activity) + + def test_set_creates_allocation_attribute_if_nonexistent(self): + """Test that the setter creates an associated + AllocationAttribute for the Project if one does not already + exist.""" + allocation_attribute_kwargs = { + 'allocation_attribute_type': self.allocation_attribute_type, + 'allocation': self.allocation, + } + self.assertFalse( + AllocationAttribute.objects.filter(**allocation_attribute_kwargs)) + self.assertIsNone(self.manager.billing_activity) + + self.manager.billing_activity = self.billing_activity_1 + + allocation_attribute_kwargs['value'] = str(self.billing_activity_1.pk) + try: + allocation_attribute = AllocationAttribute.objects.get( + **allocation_attribute_kwargs) + except AllocationAttribute.DoesNotExist: + self.fail('An AllocationAttribute should have been created.') + old_attribute_pk = allocation_attribute.pk + + allocation_attribute.delete() + self.assertIsNone(self.manager.billing_activity) + + self.manager.billing_activity = self.billing_activity_2 + allocation_attribute_kwargs.pop('value') + try: + allocation_attribute = AllocationAttribute.objects.get( + **allocation_attribute_kwargs) + except AllocationAttribute.DoesNotExist: + self.fail('An AllocationAttribute should have been created.') + new_attribute_pk = allocation_attribute.pk + + self.assertGreater(new_attribute_pk, old_attribute_pk) + + def test_set_updates_allocation_attribute_if_existent(self): + """Test that the setter updates an associated + AllocationAttribute for the Project if one already exists.""" + allocation_attribute_kwargs = { + 'allocation_attribute_type': self.allocation_attribute_type, + 'allocation': self.allocation, + } + + self.manager.billing_activity = self.billing_activity_1 + allocation_attribute = AllocationAttribute.objects.get( + **allocation_attribute_kwargs) + self.assertEqual( + allocation_attribute.value, str(self.billing_activity_1.pk)) + + self.manager.billing_activity = self.billing_activity_2 + allocation_attribute.refresh_from_db() + self.assertEqual( + allocation_attribute.value, str(self.billing_activity_2.pk)) + + +class TestProjectUserBillingActivityManager(TestBillingActivityManagerBase): + """A class for testing the ProjectUserBillingActivityManager utility + class.""" + + def setUp(self): + """Set up test data.""" + super().setUp() + + self.allocation_user = AllocationUser.objects.get( + allocation=self.allocation, user=self.user) + self.allocation_attribute_type = AllocationAttributeType.objects.get( + name='Billing Activity') + self.manager = ProjectUserBillingActivityManager(self.project_user) + + def test_get_invalid_allocation_user_attribute_value(self): + """Test that the getter raises an exception when the given + ProjectUser's AllocationUserAttribute contains a value that does + not correspond to a BillingActivity.""" + allocation_user_attribute_kwargs = { + 'allocation_attribute_type': self.allocation_attribute_type, + 'allocation': self.allocation, + 'allocation_user': self.allocation_user, + 'value': 'invalid', + } + allocation_user_attribute = AllocationUserAttribute.objects.create( + **allocation_user_attribute_kwargs) + with self.assertRaises(ValueError) as cm: + _ = self.manager.billing_activity + self.assertIn('invalid literal', str(cm.exception)) + + allocation_user_attribute.value = str( + self.billing_activity_1.pk + self.billing_activity_2.pk + 1) + allocation_user_attribute.save() + + with self.assertRaises(BillingActivity.DoesNotExist) as cm: + _ = self.manager.billing_activity + self.assertIn('does not exist', str(cm.exception)) + + def test_get_nonexistent_allocation_user_attribute(self): + """Test that the getter returns None when the given ProjectUser + does not have an associated AllocationUserAttribute for storing + a BillingActivity, but returns the contained BillingActivity + once it has been created.""" + allocation_user_attribute_kwargs = { + 'allocation_attribute_type': self.allocation_attribute_type, + 'allocation': self.allocation, + 'allocation_user': self.allocation_user, + } + self.assertFalse( + AllocationUserAttribute.objects.filter( + **allocation_user_attribute_kwargs)) + self.assertIsNone(self.manager.billing_activity) + + allocation_user_attribute_kwargs['value'] = str( + self.billing_activity_1.pk) + AllocationUserAttribute.objects.create( + **allocation_user_attribute_kwargs) + self.assertEqual( + self.manager.billing_activity, self.billing_activity_1) + + def test_get_refreshes_value(self): + """Test that the getter returns the most up-to-date + BillingActivity stored in the AllocationUserAttribute, + accounting for changes from elsewhere.""" + allocation_user_attribute_kwargs = { + 'allocation_attribute_type': self.allocation_attribute_type, + 'allocation': self.allocation, + 'allocation_user': self.allocation_user, + 'value': str(self.billing_activity_1.pk), + } + allocation_user_attribute = AllocationUserAttribute.objects.create( + **allocation_user_attribute_kwargs) + self.assertEqual( + self.manager.billing_activity, self.billing_activity_1) + + allocation_user_attribute.value = str(self.billing_activity_2.pk) + allocation_user_attribute.save() + self.assertEqual( + self.manager.billing_activity, self.billing_activity_2) + + allocation_user_attribute.delete() + self.assertIsNone(self.manager.billing_activity) + + def test_set_creates_allocation_user_attribute_if_nonexistent(self): + """Test that the setter creates an associated + AllocationUserAttribute for the ProjectUser if one does not + already exist.""" + allocation_user_attribute_kwargs = { + 'allocation_attribute_type': self.allocation_attribute_type, + 'allocation': self.allocation, + 'allocation_user': self.allocation_user, + } + self.assertFalse( + AllocationUserAttribute.objects.filter( + **allocation_user_attribute_kwargs)) + self.assertIsNone(self.manager.billing_activity) + + self.manager.billing_activity = self.billing_activity_1 + + allocation_user_attribute_kwargs['value'] = str( + self.billing_activity_1.pk) + try: + allocation_user_attribute = AllocationUserAttribute.objects.get( + **allocation_user_attribute_kwargs) + except AllocationUserAttribute.DoesNotExist: + self.fail('An AllocationUserAttribute should have been created.') + old_attribute_pk = allocation_user_attribute.pk + + allocation_user_attribute.delete() + self.assertIsNone(self.manager.billing_activity) + + self.manager.billing_activity = self.billing_activity_2 + allocation_user_attribute_kwargs.pop('value') + try: + allocation_user_attribute = AllocationUserAttribute.objects.get( + **allocation_user_attribute_kwargs) + except AllocationUserAttribute.DoesNotExist: + self.fail('An AllocationUserAttribute should have been created.') + new_attribute_pk = allocation_user_attribute.pk + + self.assertGreater(new_attribute_pk, old_attribute_pk) + + def test_set_updates_allocation_user_attribute_if_existent(self): + """Test that the setter updates an associated + AllocationUserAttribute for the ProjectUser if one already + exists.""" + allocation_user_attribute_kwargs = { + 'allocation_attribute_type': self.allocation_attribute_type, + 'allocation': self.allocation, + 'allocation_user': self.allocation_user, + } + self.manager.billing_activity = self.billing_activity_1 + allocation_user_attribute = AllocationUserAttribute.objects.get( + **allocation_user_attribute_kwargs) + self.assertEqual( + allocation_user_attribute.value, str(self.billing_activity_1.pk)) + + self.manager.billing_activity = self.billing_activity_2 + allocation_user_attribute.refresh_from_db() + self.assertEqual( + allocation_user_attribute.value, str(self.billing_activity_2.pk)) + + +class TestUserBillingActivityManager(TestBillingActivityManagerBase): + """A class for testing the UserBillingActivityManager utility + class.""" + + def setUp(self): + """Set up test data.""" + super().setUp() + + self.user.userprofile.delete() + self.manager = UserBillingActivityManager(self.user) + + def test_get_nonexistent_user_profile(self): + """Test that the getter returns None when the given User does + not have an associated UserProfile for storing a + BillingActivity, but returns the contained BillingActivity once + it has been created.""" + user_profile_kwargs = { + 'user': self.user, + } + self.assertFalse(UserProfile.objects.filter(**user_profile_kwargs)) + self.assertIsNone(self.manager.billing_activity) + + user_profile_kwargs['billing_activity'] = self.billing_activity_1 + UserProfile.objects.create(**user_profile_kwargs) + self.assertEqual( + self.manager.billing_activity, self.billing_activity_1) + + def test_get_refreshes_value(self): + """Test that the getter returns the most up-to-date + BillingActivity stored in the UserProfile, accounting for + changes from elsewhere.""" + user_profile_kwargs = { + 'user': self.user, + 'billing_activity': self.billing_activity_1, + } + user_profile = UserProfile.objects.create(**user_profile_kwargs) + self.assertEqual( + self.manager.billing_activity, self.billing_activity_1) + + user_profile.billing_activity = self.billing_activity_2 + user_profile.save() + self.assertEqual( + self.manager.billing_activity, self.billing_activity_2) + + user_profile.delete() + self.assertIsNone(self.manager.billing_activity) + + def test_set_creates_user_profile_if_nonexistent(self): + """Test that the setter creates an associated UserProfile for + the User if one does not already exist.""" + user_profile_kwargs = { + 'user': self.user, + } + self.assertFalse(UserProfile.objects.filter(**user_profile_kwargs)) + self.assertIsNone(self.manager.billing_activity) + + self.manager.billing_activity = self.billing_activity_1 + + user_profile_kwargs['billing_activity'] = self.billing_activity_1 + try: + user_profile = UserProfile.objects.get(**user_profile_kwargs) + except UserProfile.DoesNotExist: + self.fail('A UserProfile should have been created.') + old_profile_pk = user_profile.pk + + user_profile.delete() + self.assertIsNone(self.manager.billing_activity) + + self.manager.billing_activity = self.billing_activity_2 + user_profile_kwargs.pop('billing_activity') + try: + user_profile = UserProfile.objects.get(**user_profile_kwargs) + except UserProfile.DoesNotExist: + self.fail('A UserProfile should have been created.') + new_profile_pk = user_profile.pk + + self.assertGreater(new_profile_pk, old_profile_pk) + + def test_set_updates_user_profile_if_existent(self): + """Test that the setter updates an associated UserProfile for + the User if one already exists.""" + user_profile_kwargs = { + 'user': self.user, + } + + self.manager.billing_activity = self.billing_activity_1 + user_profile = UserProfile.objects.get(**user_profile_kwargs) + self.assertEqual( + user_profile.billing_activity, self.billing_activity_1) + + self.manager.billing_activity = self.billing_activity_2 + user_profile.refresh_from_db() + self.assertEqual( + user_profile.billing_activity, self.billing_activity_2) diff --git a/coldfront/core/billing/utils/__init__.py b/coldfront/core/billing/utils/__init__.py index e69de29bb..4b325a2f1 100644 --- a/coldfront/core/billing/utils/__init__.py +++ b/coldfront/core/billing/utils/__init__.py @@ -0,0 +1,13 @@ +from coldfront.core.billing.utils.billing_activity_managers import ProjectBillingActivityManager +from coldfront.core.billing.utils.billing_activity_managers import ProjectUserBillingActivityManager +from coldfront.core.billing.utils.billing_activity_managers import UserBillingActivityManager + + +"""Utility methods related to billing IDs.""" + + +__all__ = [ + 'ProjectBillingActivityManager', + 'ProjectUserBillingActivityManager', + 'UserBillingActivityManager', +] diff --git a/coldfront/core/billing/utils/billing_activity_managers.py b/coldfront/core/billing/utils/billing_activity_managers.py new file mode 100644 index 000000000..6fd87cd17 --- /dev/null +++ b/coldfront/core/billing/utils/billing_activity_managers.py @@ -0,0 +1,209 @@ +from abc import ABC +from abc import abstractmethod + +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist + +from coldfront.core.allocation.models import AllocationAttribute +from coldfront.core.allocation.models import AllocationAttributeType +from coldfront.core.allocation.models import AllocationUser +from coldfront.core.allocation.models import AllocationUserAttribute +from coldfront.core.allocation.utils import get_project_compute_allocation +from coldfront.core.billing.models import BillingActivity +from coldfront.core.project.models import Project +from coldfront.core.project.models import ProjectUser +from coldfront.core.user.models import UserProfile + + +class BillingActivityManager(ABC): + """An interface for getting and setting the BillingActivity for a + particular database entity.""" + + entity_type = None + container_type = None + + @abstractmethod + def __init__(self, entity): + """Validate the input's type and get the container that stores + its BillingActivity, if any.""" + self._entity = entity + assert isinstance(self._entity, self.entity_type) + self._container = self._get_container() + + @property + def billing_activity(self): + """Return the BillingActivity object stored in the container, or + None.""" + if self._container is None: + self._container = self._get_container() + if isinstance(self._container, self.container_type): + # Refreshing sets the container to None if the container object has + # been deleted. + self._refresh_container() + if isinstance(self._container, self.container_type): + return self._deserialize_billing_activity() + return None + + @billing_activity.setter + def billing_activity(self, billing_activity): + """Store the BillingActivity in the container, creating the + container if needed.""" + assert isinstance(billing_activity, BillingActivity) + value = self._serialize_billing_activity(billing_activity) + if isinstance(self._container, self.container_type): + # Refreshing sets the container to None if the container object has + # been deleted. + self._refresh_container() + if isinstance(self._container, self.container_type): + self._update_container_with_value(value) + else: + self._create_container_with_value(value) + + @abstractmethod + def _create_container_with_value(self, value): + """Create the container to store the given value, and set the + _container attribute to it.""" + pass + + @abstractmethod + def _deserialize_billing_activity(self): + """Return the BillingActivity object stored in the container.""" + pass + + @abstractmethod + def _get_container(self): + """Return the database object (container) related to the entity + that stores the BillingActivity on its behalf, or None.""" + pass + + def _get_container_or_none(self, **container_kwargs): + """Return the database object with the expected container type + that matches the given filtering keyword arguments, or None.""" + try: + return self.container_type.objects.get(**container_kwargs) + except self.container_type.DoesNotExist: + return None + + def _refresh_container(self): + """Refresh the container from the database. If it no longer + exists, set it to None. This method should be called before + getting or updating a BillingActivity.""" + try: + self._container.refresh_from_db() + except ObjectDoesNotExist: + self._container = None + + @abstractmethod + def _serialize_billing_activity(self, billing_activity): + """Return a serialized version of the given BillingActivity that + is suitable for storage in the container.""" + pass + + @abstractmethod + def _update_container_with_value(self, value): + """Update the already-existent container so that it stores the + given value.""" + pass + + +class ProjectBillingActivityManager(BillingActivityManager): + """A class for getting and setting the BillingActivity for a + Project.""" + + entity_type = Project + container_type = AllocationAttribute + + def __init__(self, project): + self._allocation_attribute_type = AllocationAttributeType.objects.get( + name='Billing Activity') + self._allocation = get_project_compute_allocation(project) + super().__init__(project) + + def _create_container_with_value(self, value): + self._container = AllocationAttribute.objects.create( + allocation_attribute_type=self._allocation_attribute_type, + allocation=self._allocation, + value=value) + + def _deserialize_billing_activity(self): + return BillingActivity.objects.get(pk=int(self._container.value)) + + def _get_container(self): + return self._get_container_or_none( + allocation_attribute_type=self._allocation_attribute_type, + allocation=self._allocation) + + def _serialize_billing_activity(self, billing_activity): + return str(billing_activity.pk) + + def _update_container_with_value(self, value): + self._container.value = value + self._container.save() + + +class ProjectUserBillingActivityManager(BillingActivityManager): + """A class for getting and setting the BillingActivity for a + ProjectUser.""" + + entity_type = ProjectUser + container_type = AllocationUserAttribute + + def __init__(self, project_user): + self._allocation_attribute_type = AllocationAttributeType.objects.get( + name='Billing Activity') + self._allocation = get_project_compute_allocation(project_user.project) + self._allocation_user = AllocationUser.objects.get( + allocation=self._allocation, user=project_user.user) + super().__init__(project_user) + + def _create_container_with_value(self, value): + self._container = self.container_type.objects.create( + allocation_attribute_type=self._allocation_attribute_type, + allocation=self._allocation, + allocation_user=self._allocation_user, + value=value) + + def _deserialize_billing_activity(self): + return BillingActivity.objects.get(pk=int(self._container.value)) + + def _get_container(self): + return self._get_container_or_none( + allocation_attribute_type=self._allocation_attribute_type, + allocation=self._allocation, + allocation_user=self._allocation_user) + + def _serialize_billing_activity(self, billing_activity): + return str(billing_activity.pk) + + def _update_container_with_value(self, value): + self._container.value = value + self._container.save() + + +class UserBillingActivityManager(BillingActivityManager): + """A class for getting and setting the BillingActivity for a + User.""" + + entity_type = User + container_type = UserProfile + + def __init__(self, user): + super().__init__(user) + + def _create_container_with_value(self, value): + self._container = self.container_type.objects.create( + user=self._entity, + billing_activity=value) + + def _deserialize_billing_activity(self): + return self._container.billing_activity + + def _get_container(self): + return self._get_container_or_none(user=self._entity) + + def _serialize_billing_activity(self, billing_activity): + return billing_activity + + def _update_container_with_value(self, value): + self._container.billing_activity = value + self._container.save() diff --git a/coldfront/core/billing/utils/queries.py b/coldfront/core/billing/utils/queries.py index 9ff664be2..552cb4f03 100644 --- a/coldfront/core/billing/utils/queries.py +++ b/coldfront/core/billing/utils/queries.py @@ -1,4 +1,5 @@ import logging +import re from flags.state import flag_enabled @@ -13,6 +14,23 @@ logger = logging.getLogger(__name__) +def get_billing_activity_from_full_id(full_id): + """Given a fully-formed billing ID, get the matching + BillingActivity, if it exists, or None.""" + project_identifier, activity_identifier = full_id.split('-') + try: + billing_project = BillingProject.objects.get( + identifier=project_identifier) + except BillingProject.DoesNotExist: + return None + try: + billing_activity = BillingActivity.objects.get( + billing_project=billing_project, identifier=activity_identifier) + except BillingActivity.DoesNotExist: + return None + return billing_activity + + def get_or_create_billing_activity_from_full_id(full_id): """Given a fully-formed billing ID, get or create a matching BillingActivity, creating a BillingProject as needed.""" @@ -24,6 +42,13 @@ def get_or_create_billing_activity_from_full_id(full_id): return billing_activity +def is_billing_id_well_formed(full_id): + """Given a fully-formed billing ID, return whether it conforms to + the expected format.""" + regex = r'^\d{6}-\d{3}$' + return re.match(regex, full_id) + + def is_project_billing_id_required_and_missing(project_obj): """Return whether the given Project is expected to have a default billing ID, and, if so, whether it has one.""" diff --git a/coldfront/core/project/management/commands/projects.py b/coldfront/core/project/management/commands/projects.py new file mode 100644 index 000000000..dd441f982 --- /dev/null +++ b/coldfront/core/project/management/commands/projects.py @@ -0,0 +1,256 @@ +from django.conf import settings +from django.contrib.auth.models import User +from django.core.management import CommandError +from django.core.management.base import BaseCommand +from django.db import transaction + +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 +from coldfront.core.project.utils import is_primary_cluster_project +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.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 + + +"""An admin command for managing projects.""" + + +class Command(BaseCommand): + + help = 'Manage projects.' + logger = logging.getLogger(__name__) + + def add_arguments(self, parser): + """Define subcommands with different functions.""" + subparsers = parser.add_subparsers( + dest='subcommand', + help='The subcommand to run.', + title='subcommands') + subparsers.required = True + self._add_create_subparser(subparsers) + + def handle(self, *args, **options): + """Call the handler for the provided subcommand.""" + subcommand = options['subcommand'] + if subcommand == 'create': + self._handle_create(*args, **options) + + @staticmethod + def _add_create_subparser(parsers): + """Add a subparser for the 'create' subcommand.""" + parser = parsers.add_parser( + 'create', + help=( + 'Create a project with an allocation to a particular compute ' + 'resource. Note: The current use case for this is to create a ' + 'project for a newly-created standalone cluster. It cannot be ' + 'used to create projects under the primary cluster (e.g., ' + 'Savio on BRC, Lawrencium on LRC), under a standalone ' + 'cluster that (a) can have at most one project and (b) ' + 'already has a project, or, for BRC, under the Vector ' + 'project.')) + parser.add_argument( + 'name', help='The name of the project to create.', type=str) + parser.add_argument( + 'cluster_name', + help=( + 'The name of a cluster, for which a compute resource (e.g., ' + '"{cluster_name} Compute") should exist.')) + parser.add_argument( + 'pi_usernames', + help=( + 'A space-separated list of usernames of users to make the ' + 'project\'s PIs.'), + nargs='+', + type=str) + add_argparse_dry_run_argument(parser) + + @staticmethod + def _create_project_with_compute_allocation_and_pis(project_name, + compute_resource, + pi_users): + """Create a Project with the given name, with an Allocation to + the given compute Resource, and with the given Users as + Principal Investigators. Return the Project. + + Some fields are set by default: + - The Project's status is 'Active'. + - The ProjectUsers' statuses are 'Active'. + - The Allocation's status is 'Active'. + - The Allocation's start_date is today. + - The Allocation's end_date is None. + - The Allocation has the maximum number of service units. + + TODO: When the command is generalized, allow these to be + specified. + """ + with transaction.atomic(): + project = Project.objects.create( + name=project_name, + title=project_name, + status=ProjectStatusChoice.objects.get(name='Active')) + + project_users = [] + for pi_user in pi_users: + project_user = ProjectUser.objects.create( + project=project, + user=pi_user, + role=ProjectUserRoleChoice.objects.get( + name='Principal Investigator'), + 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( + project_user, NewProjectUserSource.AUTO_ADDED, + email_strategy=DropEmailStrategy()) + runner.run() + + for pi_user in pi_users: + pi_user.userprofile.is_pi = True + pi_user.userprofile.save() + + return project + + def _handle_create(self, *args, **options): + """Handle the 'create' subcommand.""" + cleaned_options = self._validate_create_options(options) + project_name = cleaned_options['project_name'] + compute_resource = cleaned_options['compute_resource'] + pi_users = cleaned_options['pi_users'] + + pi_users_str = ( + '[' + + ', '.join(f'"{pi_user.username}"' for pi_user in pi_users) + + ']') + message_template = ( + f'{{0}} Project "{project_name}" with Allocation to ' + f'"{compute_resource.name}" Resource under PIs {pi_users_str}.') + if options['dry_run']: + message = message_template.format('Would create') + self.stdout.write(self.style.WARNING(message)) + return + + try: + self._create_project_with_compute_allocation_and_pis( + project_name, compute_resource, pi_users) + except Exception as e: + message = message_template.format('Failed to create') + self.stderr.write(self.style.ERROR(message)) + self.logger.exception(f'{message}\n{e}') + else: + message = message_template.format('Created') + self.stdout.write(self.style.SUCCESS(message)) + self.logger.info(message) + + @staticmethod + def _validate_create_options(options): + """Validate the options provided to the 'create' subcommand. + Raise a subcommand if any are invalid or if they violate + business logic, else return a dict of the form: + { + 'project_name': 'project_name', + 'compute_resource': Resource, + 'pi_users': list of Users, + } + """ + project_name = options['name'].lower() + if Project.objects.filter(name=project_name).exists(): + raise CommandError( + f'A Project with name "{project_name}" already exists.') + + cluster_name = options['cluster_name'] + lowercase_cluster_name = cluster_name.lower() + uppercase_cluster_name = cluster_name.upper() + + # TODO: When the command is generalized, enforce business logic re: + # the number of certain projects a PI may have. + pi_usernames = list(set(options['pi_usernames'])) + pi_users = [] + for pi_username in pi_usernames: + try: + pi_user = User.objects.get(username=pi_username) + except User.DoesNotExist: + raise CommandError( + f'User with username "{pi_username}" does not exist.') + else: + pi_users.append(pi_user) + + lowercase_primary_cluster_name = get_primary_compute_resource_name( + ).replace(' Compute', '').lower() + is_cluster_primary = ( + lowercase_cluster_name == lowercase_primary_cluster_name) + if (is_primary_cluster_project(Project(name=project_name)) or + is_cluster_primary): + raise CommandError( + 'This command may not be used to create a Project under the ' + 'primary cluster.') + + # On BRC, also prevent a project from being created for the Vector + # cluster. + if flag_enabled('BRC_ONLY'): + # TODO: As noted in the add_accounting_defaults management command, + # 'Vector' should be fully-uppercase in its Resource name. Update + # this when that is the case. + capitalized_cluster_name = cluster_name.capitalize() + if capitalized_cluster_name == 'Vector': + raise CommandError( + f'This command may not be used to create a Project under ' + f'the {uppercase_cluster_name} cluster.') + + try: + compute_resource = Resource.objects.get( + name=f'{uppercase_cluster_name} Compute') + except Resource.DoesNotExist: + raise CommandError( + f'Cluster {uppercase_cluster_name} does not exist.') + + # TODO: When the command is generalized, allow the project name to + # differ from the cluster name (within expected bounds). + if project_name != lowercase_cluster_name: + raise CommandError( + f'This command may not be used to create a Project whose name ' + f'differs from the cluster name.') + + return { + 'project_name': project_name, + 'compute_resource': compute_resource, + 'pi_users': pi_users, + } diff --git a/coldfront/core/project/tests/test_utils/test_project_removal_request_runners.py b/coldfront/core/project/tests/test_utils/test_project_removal_request_runners.py index e2cca34a7..0724932b9 100644 --- a/coldfront/core/project/tests/test_utils/test_project_removal_request_runners.py +++ b/coldfront/core/project/tests/test_utils/test_project_removal_request_runners.py @@ -71,7 +71,7 @@ def setUp(self): # Create Projects. self.project1 = Project.objects.create( - name='project1', status=active_project_status) + name='fc_project1', status=active_project_status) # add pis for pi_user in [self.pi1, self.pi2]: diff --git a/coldfront/core/project/tests/test_utils/test_renewal_utils/test_allocation_renewal_approval_runner.py b/coldfront/core/project/tests/test_utils/test_renewal_utils/test_allocation_renewal_approval_runner.py index 3872db9cc..73fdd8c2c 100644 --- a/coldfront/core/project/tests/test_utils/test_renewal_utils/test_allocation_renewal_approval_runner.py +++ b/coldfront/core/project/tests/test_utils/test_renewal_utils/test_allocation_renewal_approval_runner.py @@ -159,8 +159,8 @@ def setUp(self): name='Under Review'), computing_allowance=self.computing_allowance, pi=self.pi0, - pre_project=self.unpooled_project0, - post_project=self.unpooled_project0) + pre_project=self.fc_unpooled_project0, + post_project=self.fc_unpooled_project0) def test_pooling_preference_case(self): """Test that the pooling preference case for the class' renewal @@ -182,8 +182,8 @@ def setUp(self): name='Under Review'), computing_allowance=self.computing_allowance, pi=self.pi0, - pre_project=self.unpooled_project0, - post_project=self.pooled_project1) + pre_project=self.fc_unpooled_project0, + post_project=self.fc_pooled_project1) def test_pooling_preference_case(self): """Test that the pooling preference case for the class' renewal @@ -205,8 +205,8 @@ def setUp(self): name='Under Review'), computing_allowance=self.computing_allowance, pi=self.pi0, - pre_project=self.pooled_project0, - post_project=self.pooled_project0) + pre_project=self.fc_pooled_project0, + post_project=self.fc_pooled_project0) def test_pooling_preference_case(self): """Test that the pooling preference case for the class' renewal @@ -228,8 +228,8 @@ def setUp(self): name='Under Review'), computing_allowance=self.computing_allowance, pi=self.pi0, - pre_project=self.pooled_project0, - post_project=self.pooled_project1) + pre_project=self.fc_pooled_project0, + post_project=self.fc_pooled_project1) def test_pooling_preference_case(self): """Test that the pooling preference case for the class' renewal @@ -251,8 +251,8 @@ def setUp(self): name='Under Review'), computing_allowance=self.computing_allowance, pi=self.pi0, - pre_project=self.pooled_project0, - post_project=self.unpooled_project0) + pre_project=self.fc_pooled_project0, + post_project=self.fc_unpooled_project0) def test_pooling_preference_case(self): """Test that the pooling preference case for the class' renewal @@ -276,7 +276,7 @@ def setUp(self): name='Under Review'), computing_allowance=self.computing_allowance, pi=self.pi0, - pre_project=self.pooled_project0, + pre_project=self.fc_pooled_project0, post_project=new_project_request.project, new_project_request=new_project_request) diff --git a/coldfront/core/project/tests/test_utils/test_renewal_utils/test_allocation_renewal_denial_runner.py b/coldfront/core/project/tests/test_utils/test_renewal_utils/test_allocation_renewal_denial_runner.py index eb829da92..3469bf5ba 100644 --- a/coldfront/core/project/tests/test_utils/test_renewal_utils/test_allocation_renewal_denial_runner.py +++ b/coldfront/core/project/tests/test_utils/test_renewal_utils/test_allocation_renewal_denial_runner.py @@ -152,8 +152,8 @@ def setUp(self): name='Under Review'), computing_allowance=self.computing_allowance, pi=self.pi0, - pre_project=self.unpooled_project0, - post_project=self.unpooled_project0) + pre_project=self.fc_unpooled_project0, + post_project=self.fc_unpooled_project0) def test_pooling_preference_case(self): """Test that the pooling preference case for the class' renewal @@ -175,8 +175,8 @@ def setUp(self): name='Under Review'), computing_allowance=self.computing_allowance, pi=self.pi0, - pre_project=self.unpooled_project0, - post_project=self.pooled_project1) + pre_project=self.fc_unpooled_project0, + post_project=self.fc_pooled_project1) def test_pooling_preference_case(self): """Test that the pooling preference case for the class' renewal @@ -198,8 +198,8 @@ def setUp(self): name='Under Review'), computing_allowance=self.computing_allowance, pi=self.pi0, - pre_project=self.pooled_project0, - post_project=self.pooled_project0) + pre_project=self.fc_pooled_project0, + post_project=self.fc_pooled_project0) def test_pooling_preference_case(self): """Test that the pooling preference case for the class' renewal @@ -221,8 +221,8 @@ def setUp(self): name='Under Review'), computing_allowance=self.computing_allowance, pi=self.pi0, - pre_project=self.pooled_project0, - post_project=self.pooled_project1) + pre_project=self.fc_pooled_project0, + post_project=self.fc_pooled_project1) def test_pooling_preference_case(self): """Test that the pooling preference case for the class' renewal @@ -244,8 +244,8 @@ def setUp(self): name='Under Review'), computing_allowance=self.computing_allowance, pi=self.pi0, - pre_project=self.pooled_project0, - post_project=self.unpooled_project0) + pre_project=self.fc_pooled_project0, + post_project=self.fc_unpooled_project0) def test_pooling_preference_case(self): """Test that the pooling preference case for the class' renewal @@ -269,7 +269,7 @@ def setUp(self): name='Under Review'), computing_allowance=self.computing_allowance, pi=self.pi0, - pre_project=self.pooled_project0, + pre_project=self.fc_pooled_project0, post_project=new_project_request.project, new_project_request=new_project_request) diff --git a/coldfront/core/project/tests/test_utils/test_renewal_utils/test_allocation_renewal_processing_runner.py b/coldfront/core/project/tests/test_utils/test_renewal_utils/test_allocation_renewal_processing_runner.py index 49a4f6c13..e1ed18f0b 100644 --- a/coldfront/core/project/tests/test_utils/test_renewal_utils/test_allocation_renewal_processing_runner.py +++ b/coldfront/core/project/tests/test_utils/test_renewal_utils/test_allocation_renewal_processing_runner.py @@ -1049,8 +1049,8 @@ def setUp(self): name='Approved'), pi=self.pi0, computing_allowance=self.computing_allowance, - pre_project=self.unpooled_project0, - post_project=self.unpooled_project0) + pre_project=self.fc_unpooled_project0, + post_project=self.fc_unpooled_project0) def test_pooling_preference_case(self): """Test that the pooling preference case for the class' renewal @@ -1073,8 +1073,8 @@ def setUp(self): name='Approved'), pi=self.pi0, computing_allowance=self.computing_allowance, - pre_project=self.unpooled_project0, - post_project=self.pooled_project1) + pre_project=self.fc_unpooled_project0, + post_project=self.fc_pooled_project1) def test_pooling_preference_case(self): """Test that the pooling preference case for the class' renewal @@ -1097,8 +1097,8 @@ def setUp(self): name='Approved'), pi=self.pi0, computing_allowance=self.computing_allowance, - pre_project=self.pooled_project0, - post_project=self.pooled_project0) + pre_project=self.fc_pooled_project0, + post_project=self.fc_pooled_project0) def test_pooling_preference_case(self): """Test that the pooling preference case for the class' renewal @@ -1122,8 +1122,8 @@ def setUp(self): name='Approved'), pi=self.pi0, computing_allowance=self.computing_allowance, - pre_project=self.pooled_project0, - post_project=self.pooled_project1) + pre_project=self.fc_pooled_project0, + post_project=self.fc_pooled_project1) def test_pooling_preference_case(self): """Test that the pooling preference case for the class' renewal @@ -1146,8 +1146,8 @@ def setUp(self): name='Approved'), pi=self.pi0, computing_allowance=self.computing_allowance, - pre_project=self.pooled_project0, - post_project=self.unpooled_project0) + pre_project=self.fc_pooled_project0, + post_project=self.fc_unpooled_project0) def test_pooling_preference_case(self): """Test that the pooling preference case for the class' renewal @@ -1172,7 +1172,7 @@ def setUp(self): name='Approved'), pi=self.pi0, computing_allowance=self.computing_allowance, - pre_project=self.pooled_project0, + pre_project=self.fc_pooled_project0, post_project=new_project_request.project, new_project_request=new_project_request) diff --git a/coldfront/core/project/tests/test_utils/test_renewal_utils/utils.py b/coldfront/core/project/tests/test_utils/test_renewal_utils/utils.py index 81c15fc02..5d275dc43 100644 --- a/coldfront/core/project/tests/test_utils/test_renewal_utils/utils.py +++ b/coldfront/core/project/tests/test_utils/test_renewal_utils/utils.py @@ -101,21 +101,21 @@ def setUp(self): name='Principal Investigator') # Create Projects. - self.unpooled_project0 = Project.objects.create( - name='unpooled_project0', status=active_project_status) - self.unpooled_project1 = Project.objects.create( - name='unpooled_project1', status=inactive_project_status) - self.pooled_project0 = Project.objects.create( - name='pooled_project0', status=active_project_status) - self.pooled_project1 = Project.objects.create( - name='pooled_project1', status=active_project_status) + self.fc_unpooled_project0 = Project.objects.create( + name='fc_unpooled_project0', status=active_project_status) + self.fc_unpooled_project1 = Project.objects.create( + name='fc_unpooled_project1', status=inactive_project_status) + self.fc_pooled_project0 = Project.objects.create( + name='fc_pooled_project0', status=active_project_status) + self.fc_pooled_project1 = Project.objects.create( + name='fc_pooled_project1', status=active_project_status) # Add the designated PIs to each Project. self.projects_and_pis = { - self.unpooled_project0: [self.pi0], - self.unpooled_project1: [self.pi1], - self.pooled_project0: [self.pi0, self.pi1], - self.pooled_project1: [self.pi2, self.pi3], + self.fc_unpooled_project0: [self.pi0], + self.fc_unpooled_project1: [self.pi1], + self.fc_pooled_project0: [self.pi0, self.pi1], + self.fc_pooled_project1: [self.pi2, self.pi3], } for project, pi_users in self.projects_and_pis.items(): for pi_user in pi_users: @@ -208,7 +208,7 @@ def create_under_review_new_project_request(self): """Create a new Project, a corresponding 'CLUSTER_NAME Compute' Allocation, and an 'Under Review' new project request for it.""" # Create a new Project. - new_project_name = 'unpooled_project2' + new_project_name = 'fc_unpooled_project2' new_project_status = ProjectStatusChoice.objects.get(name='New') new_project = Project.objects.create( name=new_project_name, diff --git a/coldfront/core/user/tests/test_views/test_request_hub_view.py b/coldfront/core/user/tests/test_views/test_request_hub_view.py index 53eab14b8..ee9c9b324 100644 --- a/coldfront/core/user/tests/test_views/test_request_hub_view.py +++ b/coldfront/core/user/tests/test_views/test_request_hub_view.py @@ -69,16 +69,16 @@ def setUp(self): for i in range(2): # Create a Project and ProjectUsers. project = Project.objects.create( - name=f'project{i}', status=project_status) - setattr(self, f'project{i}', project) + name=f'fc_project{i}', status=project_status) + setattr(self, f'fc_project{i}', project) proj_user = ProjectUser.objects.create( user=self.user0, project=project, role=user_role, status=project_user_status) - setattr(self, f'project{i}_user0', proj_user) + setattr(self, f'fc_project{i}_user0', proj_user) pi_proj_user = ProjectUser.objects.create( user=self.pi, project=project, role=manager_role, status=project_user_status) - setattr(self, f'project{i}_pi', pi_proj_user) + setattr(self, f'fc_project{i}_pi', pi_proj_user) # Create a compute allocation for the Project. allocation = Decimal(f'{i + 1}000.00') @@ -213,9 +213,9 @@ def assert_request_shown(user, url): # creating two cluster access requests for user0 allocation_obj = \ - get_accounting_allocation_objects(self.project0) + get_accounting_allocation_objects(self.fc_project0) allocation_user_obj = \ - get_accounting_allocation_objects(self.project0, self.user0) + get_accounting_allocation_objects(self.fc_project0, self.user0) kwargs = { 'allocation_user': allocation_user_obj.allocation_user, @@ -287,8 +287,8 @@ def assert_request_shown(user, url): current_time = utc_now_offset_aware() kwargs = { - 'project_user': self.project0_user0, - 'requester': self.project0_pi.user, + 'project_user': self.fc_project0_user0, + 'requester': self.fc_project0_pi.user, 'request_time': current_time, } pending_req = ProjectUserRemovalRequest.objects.create( @@ -350,7 +350,7 @@ def assert_request_shown(user, url): 'requester': self.user0, 'allocation_type': 'FCA', 'pi': self.pi, - 'project': self.project0, + 'project': self.fc_project0, 'pool': False, 'survey_answers': savio_project_request_state_schema() } @@ -416,7 +416,7 @@ def assert_request_shown(user, url): kwargs = { 'requester': self.user0, 'pi': self.pi, - 'project': self.project0, + 'project': self.fc_project0, 'state': vector_project_request_state_schema() } @@ -483,14 +483,14 @@ def assert_request_shown(user, url, status): name='Pending - Add') user_role = ProjectUserRoleChoice.objects.get(name='User') pending_proj_user = ProjectUser.objects.create( - user=self.user1, project=self.project0, + user=self.user1, project=self.fc_project0, role=user_role, status=project_user_status) pending_req = ProjectUserJoinRequest.objects.create( project_user=pending_proj_user, reason='Request hub testing.') completed_req = ProjectUserJoinRequest.objects.create( - project_user=self.project0_user0, + project_user=self.fc_project0_user0, reason='Request hub testing.') # assert the correct requests are shown @@ -546,8 +546,8 @@ def assert_request_shown(user, url): 'pi': self.pi, 'requester': self.user0, 'allocation_period': get_current_allowance_year_period(), - 'pre_project': self.project0, - 'post_project': self.project0, + 'pre_project': self.fc_project0, + 'post_project': self.fc_project0, 'num_service_units': 1000, 'request_time': utc_now_offset_aware(), 'state': allocation_renewal_request_state_schema() @@ -612,7 +612,7 @@ def assert_request_shown(user, url): current_time = utc_now_offset_aware() kwargs = { 'requester': self.pi, - 'project': self.project0, + 'project': self.fc_project0, 'num_service_units': 1000, 'request_time': current_time, 'state': allocation_addition_request_state_schema() diff --git a/coldfront/core/utils/management/commands/add_accounting_defaults.py b/coldfront/core/utils/management/commands/add_accounting_defaults.py index c80465587..b22be4fc7 100644 --- a/coldfront/core/utils/management/commands/add_accounting_defaults.py +++ b/coldfront/core/utils/management/commands/add_accounting_defaults.py @@ -28,6 +28,7 @@ def handle(self, *args, **options): ('Savio Compute', 'Savio cluster compute access'), ('Vector Compute', 'Vector cluster compute access'), ('ABC Compute', 'ABC cluster compute access'), + ('REFMLAB Compute', 'REFMLAB cluster compute access'), ], 'LRC_ONLY': [ ('ALICE Compute', 'ALICE cluster compute access'), diff --git a/coldfront/core/utils/tests/test_export_data.py b/coldfront/core/utils/tests/test_export_data.py index 961d3b4a1..6fcd8df65 100644 --- a/coldfront/core/utils/tests/test_export_data.py +++ b/coldfront/core/utils/tests/test_export_data.py @@ -53,8 +53,8 @@ def setUp(self): # Create a Project and ProjectUsers. project = Project.objects.create( - name='project0', status=project_status) - setattr(self, 'project0', project) + name='fc_project0', status=project_status) + setattr(self, 'fc_project0', project) for j in range(2): ProjectUser.objects.create( user=getattr(self, f'user{j}'), project=project, @@ -72,7 +72,7 @@ def setUp(self): self.cluster_account_status = AllocationAttributeType.objects.get( name='Cluster Account Status') - allocation_object = get_accounting_allocation_objects(self.project0) + allocation_object = get_accounting_allocation_objects(self.fc_project0) for j, project_user in enumerate(project.projectuser_set.all()): if project_user.role.name != 'User': continue @@ -289,7 +289,7 @@ def test_json_with_date(self): allocation_user_attr_obj = AllocationUserAttribute.objects.get( allocation_attribute_type=self.cluster_account_status, - allocation__project__name='project0', + allocation__project__name='fc_project0', allocation_user__user__username='user0', value='Active') @@ -347,7 +347,7 @@ def test_csv_with_date(self): allocation_user_attr_obj = AllocationUserAttribute.objects.get( allocation_attribute_type=self.cluster_account_status, - allocation__project__name='project0', + allocation__project__name='fc_project0', allocation_user__user__username='user0', value='Active')