class CartItem(core_models.UuidMixin, TimeStampedModel, RequestTypeMixin): user = models.ForeignKey(core_models.User, related_name='+', on_delete=models.CASCADE) offering = models.ForeignKey(Offering, related_name='+', on_delete=models.CASCADE) plan = models.ForeignKey('Plan', null=True, blank=True) attributes = BetterJSONField(blank=True, default=dict) limits = BetterJSONField(blank=True, default=dict) class Meta(object): ordering = ('created',) @property def estimate(self): return self.plan.get_estimate(self.limits)
class EventTypesMixin(models.Model): """ Mixin to add a event_types and event_groups fields. """ class Meta: abstract = True event_types = BetterJSONField('List of event types') event_groups = BetterJSONField('List of event groups', default=list) @classmethod @lru_cache(maxsize=1) def get_all_models(cls): return [model for model in apps.get_models() if issubclass(model, cls)]
class UserDetailsMixin(models.Model): """ This mixin is shared by User and Invitation model. All fields are optional. User is populated with these details when invitation is approved. Note that civil_number and email fields are not included in this mixin because they have different constraints in User and Invitation model. """ class Meta: abstract = True native_name = models.CharField(_('native name'), max_length=100, blank=True) phone_number = models.CharField(_('phone number'), max_length=255, blank=True) organization = models.CharField(_('organization'), max_length=255, blank=True) job_title = models.CharField(_('job title'), max_length=40, blank=True) affiliations = BetterJSONField( default=list, blank=True, help_text= "Person's affiliation within organization such as student, faculty, staff.", ) def _process_saml2_affiliations(self, affiliations): """ Due to djangosaml2 assumption that attributes list should have at most one element we have to implement custom method to process affiliations fetched from SAML2 IdP. See also: https://github.com/peppelinux/djangosaml2/issues/28 """ self.affiliations = affiliations
class OfferingTemplate(core_models.UuidMixin, structure_models.TimeStampedModel): name = models.CharField(_('name'), max_length=150) config = BetterJSONField() def __str__(self): return self.name
class Event(UuidMixin): created = AutoCreatedField() event_type = models.CharField(max_length=100, db_index=True) message = models.TextField() context = BetterJSONField(blank=True) class Meta: ordering = ('-created', )
class OrderItem(core_models.UuidMixin, core_models.ErrorMessageMixin, structure_models.TimeStampedModel, ScopeMixin): class States(object): PENDING = 1 EXECUTING = 2 DONE = 3 ERRED = 4 TERMINATED = 5 CHOICES = ( (PENDING, 'pending'), (EXECUTING, 'executing'), (DONE, 'done'), (ERRED, 'erred'), (TERMINATED, 'terminated'), ) TERMINAL_STATES = set([DONE, ERRED, TERMINATED]) order = models.ForeignKey(Order, related_name='items') offering = models.ForeignKey(Offering) attributes = BetterJSONField(blank=True, default=dict) cost = models.DecimalField(max_digits=22, decimal_places=10, null=True, blank=True) plan = models.ForeignKey('Plan', null=True, blank=True) objects = managers.MixinManager('scope') state = FSMIntegerField(default=States.PENDING, choices=States.CHOICES) tracker = FieldTracker() class Meta(object): verbose_name = _('Order item') ordering = ('created', ) @transition(field=state, source=[States.PENDING, States.ERRED], target=States.EXECUTING) def set_state_executing(self): pass @transition(field=state, source=States.EXECUTING, target=States.DONE) def set_state_done(self): pass @transition(field=state, source='*', target=States.ERRED) def set_state_erred(self): pass @transition(field=state, source='*', target=States.TERMINATED) def set_state_terminated(self): pass def set_state(self, state): getattr(self, 'set_state_' + state)() self.save(update_fields=['state'])
class Attribute(TimeStampedModel): key = models.CharField(primary_key=True, max_length=255, validators=[InternalNameValidator]) title = models.CharField(blank=False, max_length=255) section = models.ForeignKey(on_delete=models.CASCADE, to=Section, related_name='attributes') type = models.CharField(max_length=255, choices=ATTRIBUTE_TYPES) required = models.BooleanField(default=False, help_text=_('A value must be provided for the attribute.')) default = BetterJSONField(null=True, blank=True) def __str__(self): return str(self.title)
class OfferingTemplate(core_models.UuidMixin, TimeStampedModel): name = models.CharField(_('name'), max_length=150) config = BetterJSONField() @classmethod def get_url_name(cls): return 'support-offering-template' def __str__(self): return self.name
class OrderItem(core_models.UuidMixin, structure_models.TimeStampedModel): order = models.ForeignKey(Order, related_name='items') offering = models.ForeignKey(Offering) attributes = BetterJSONField(blank=True, default=dict) cost = models.DecimalField(max_digits=22, decimal_places=10, null=True, blank=True) class Meta(object): verbose_name = _('Order item') ordering = ('created', )
class CostEstimateMixin(models.Model): class Meta: abstract = True # Cost estimate is computed with respect to fixed plan components and usage-based limits cost = models.DecimalField(max_digits=22, decimal_places=10, null=True, blank=True) plan = models.ForeignKey(on_delete=models.CASCADE, to=Plan, null=True, blank=True) limits = BetterJSONField(blank=True, default=dict) def init_cost(self): if self.plan: self.cost = self.plan.get_estimate(self.limits)
class CartItem(core_models.UuidMixin, TimeStampedModel, RequestTypeMixin, CostEstimateMixin): user = models.ForeignKey(core_models.User, related_name='+', on_delete=models.CASCADE) offering = models.ForeignKey(Offering, related_name='+', on_delete=models.CASCADE) attributes = BetterJSONField(blank=True, default=dict) class Meta(object): ordering = ('created', )
class Offering(core_models.UuidMixin, core_models.NameMixin, core_models.DescribableMixin, quotas_models.QuotaModelMixin, structure_models.StructureModel, structure_models.TimeStampedModel): thumbnail = models.ImageField( upload_to='marketplace_service_offering_thumbnails', blank=True, null=True) full_description = models.TextField(blank=True) rating = models.IntegerField( null=True, validators=[MaxValueValidator(5), MinValueValidator(1)], help_text=_('Rating is value from 1 to 5.')) category = models.ForeignKey(Category, related_name='offerings') customer = models.ForeignKey(structure_models.Customer, related_name='+', null=True) attributes = BetterJSONField(blank=True, default=dict) geolocations = JSONField( default=list, blank=True, help_text=_( 'List of latitudes and longitudes. For example: ' '[{"latitude": 123, "longitude": 345}, {"latitude": 456, "longitude": 678}]' )) is_active = models.BooleanField(default=True) native_name = models.CharField(max_length=160, default='', blank=True) native_description = models.CharField(max_length=500, default='', blank=True) class Permissions(object): customer_path = 'customer' class Meta(object): verbose_name = _('Offering') class Quotas(quotas_models.QuotaModelMixin.Quotas): order_item_count = quotas_fields.CounterQuotaField( target_models=lambda: [OrderItem], path_to_scope='offering', ) def __str__(self): return six.text_type(self.name) @classmethod def get_url_name(cls): return 'marketplace-offering'
class OfferingTemplate(core_models.UuidMixin, TimeStampedModel): name = models.CharField(_('name'), max_length=150) config = BetterJSONField() sort_order = models.PositiveSmallIntegerField(default=1) class Meta: ordering = ['sort_order', 'name'] @classmethod def get_url_name(cls): return 'support-offering-template' def __str__(self): return self.name
class CartItem(core_models.UuidMixin, TimeStampedModel, RequestTypeMixin): user = models.ForeignKey(core_models.User, related_name='+', on_delete=models.CASCADE) project = models.ForeignKey(structure_models.Project, related_name='+', on_delete=models.CASCADE) offering = models.ForeignKey(Offering, related_name='+', on_delete=models.CASCADE) attributes = BetterJSONField(blank=True, default=dict) class Permissions: customer_path = 'project__customer' project_path = 'project' class Meta: ordering = ('created',) def __str__(self): return 'user: %s, offering: %s' % (self.user.username, self.offering.name)
class ResourceDetailsMixin( CostEstimateMixin, core_models.NameMixin, core_models.DescribableMixin, ): class Meta: abstract = True offering = models.ForeignKey(Offering, related_name='+', on_delete=models.PROTECT) attributes = BetterJSONField(blank=True, default=dict) end_date = models.DateField( null=True, blank=True, help_text= _('The date is inclusive. Once reached, a resource will be scheduled for termination.' ), )
class Offering( core_models.BackendMixin, core_models.UuidMixin, core_models.NameMixin, core_models.DescribableMixin, quotas_models.QuotaModelMixin, structure_models.PermissionMixin, structure_models.StructureModel, TimeStampedModel, core_mixins.ScopeMixin, LoggableMixin, pid_mixins.DataciteMixin, CoordinatesMixin, structure_models.ImageModelMixin, ): class States: DRAFT = 1 ACTIVE = 2 PAUSED = 3 ARCHIVED = 4 CHOICES = ( (DRAFT, 'Draft'), (ACTIVE, 'Active'), (PAUSED, 'Paused'), (ARCHIVED, 'Archived'), ) thumbnail = models.FileField( upload_to='marketplace_service_offering_thumbnails', blank=True, null=True, validators=[ImageValidator], ) full_description = models.TextField(blank=True) vendor_details = models.TextField(blank=True) rating = models.IntegerField( null=True, validators=[MaxValueValidator(5), MinValueValidator(1)], help_text=_('Rating is value from 1 to 5.'), ) category = models.ForeignKey(on_delete=models.CASCADE, to=Category, related_name='offerings') customer: structure_models.Customer = models.ForeignKey( on_delete=models.CASCADE, to=structure_models.Customer, related_name='+', null=True, ) project = models.ForeignKey( on_delete=models.CASCADE, to=structure_models.Project, related_name='+', null=True, blank=True, ) # Volume offering is linked with VPC offering via parent field parent = models.ForeignKey(on_delete=models.CASCADE, to='Offering', null=True, blank=True) attributes = BetterJSONField(blank=True, default=dict, help_text=_('Fields describing Category.')) options = BetterJSONField( blank=True, default=dict, help_text=_('Fields describing Offering request form.'), ) plugin_options = BetterJSONField( blank=True, default=dict, help_text= _('Public data used by specific plugin, such as storage mode for OpenStack.' ), ) secret_options = BetterJSONField( blank=True, default=dict, help_text= _('Private data used by specific plugin, such as credentials and hooks.' ), ) native_name = models.CharField(max_length=160, default='', blank=True) native_description = models.CharField(max_length=500, default='', blank=True) terms_of_service = models.TextField(blank=True) terms_of_service_link = models.URLField(blank=True) privacy_policy_link = models.URLField(blank=True) type = models.CharField(max_length=100) state = FSMIntegerField(default=States.DRAFT, choices=States.CHOICES) paused_reason = models.TextField(blank=True) divisions = models.ManyToManyField(structure_models.Division, related_name='offerings', blank=True) # If offering is not shared, it is available only to following user categories: # 1) staff user; # 2) global support user; # 3) users with active permission in original customer; # 4) users with active permission in related project. shared = models.BooleanField(default=True, help_text=_('Accessible to all customers.')) billable = models.BooleanField( default=True, help_text=_('Purchase and usage is invoiced.')) objects = managers.OfferingManager() tracker = FieldTracker() class Permissions: customer_path = 'customer' class Meta: verbose_name = _('Offering') class Quotas(quotas_models.QuotaModelMixin.Quotas): order_item_count = quotas_fields.CounterQuotaField( target_models=lambda: [OrderItem], path_to_scope='offering', ) @transition(field=state, source=[States.DRAFT, States.PAUSED], target=States.ACTIVE) def activate(self): pass @transition(field=state, source=States.ACTIVE, target=States.PAUSED) def pause(self): pass @transition(field=state, source=States.PAUSED, target=States.ACTIVE) def unpause(self): pass @transition(field=state, source='*', target=States.ARCHIVED) def archive(self): pass @transition(field=state, source='*', target=States.DRAFT) def draft(self): pass def __str__(self): return str(self.name) @classmethod def get_url_name(cls): return 'marketplace-offering' @cached_property def component_factors(self): # get factor from plugin components plugin_components = plugins.manager.get_components(self.type) return {c.type: c.factor for c in plugin_components} @cached_property def is_usage_based(self): return self.components.filter( billing_type=OfferingComponent.BillingTypes.USAGE, ).exists() def get_limit_components(self): components = self.components.filter( billing_type=OfferingComponent.BillingTypes.LIMIT) return {component.type: component for component in components} @cached_property def is_limit_based(self): if not plugins.manager.can_update_limits(self.type): return False if not self.components.filter( billing_type=OfferingComponent.BillingTypes.LIMIT).exists(): return False return True @property def is_private(self): return not self.billable and not self.shared def get_datacite_title(self): return self.name def get_datacite_creators_name(self): return self.customer.name def get_datacite_description(self): return self.description def get_datacite_publication_year(self): return self.created.year def get_datacite_url(self): return core_utils.format_homeport_link( 'marketplace-public-offering/{offering_uuid}/', offering_uuid=self.uuid.hex) def can_manage_role(self, user, role=None, timestamp=False): if not self.shared: return False return user.is_staff or self.customer.has_user( user, structure_models.CustomerRole.OWNER, timestamp) def get_users(self, role=None): query = Q(offeringpermission__offering=self, offeringpermission__is_active=True) return User.objects.filter(query).order_by('username')
class Offering(core_models.UuidMixin, core_models.NameMixin, core_models.DescribableMixin, quotas_models.QuotaModelMixin, structure_models.StructureModel, structure_models.TimeStampedModel, ScopeMixin): class States(object): DRAFT = 1 ACTIVE = 2 PAUSED = 3 ARCHIVED = 4 CHOICES = ( (DRAFT, 'Draft'), (ACTIVE, 'Active'), (PAUSED, 'Paused'), (ARCHIVED, 'Archived'), ) thumbnail = models.FileField( upload_to='marketplace_service_offering_thumbnails', blank=True, null=True, validators=[VectorizedImageValidator]) full_description = models.TextField(blank=True) vendor_details = models.TextField(blank=True) rating = models.IntegerField( null=True, validators=[MaxValueValidator(5), MinValueValidator(1)], help_text=_('Rating is value from 1 to 5.')) category = models.ForeignKey(Category, related_name='offerings') customer = models.ForeignKey(structure_models.Customer, related_name='+', null=True) attributes = BetterJSONField(blank=True, default=dict, help_text=_('Fields describing Category.')) options = BetterJSONField( blank=True, default=dict, help_text=_('Fields describing Offering request form.')) geolocations = JSONField( default=list, blank=True, help_text=_( 'List of latitudes and longitudes. For example: ' '[{"latitude": 123, "longitude": 345}, {"latitude": 456, "longitude": 678}]' )) native_name = models.CharField(max_length=160, default='', blank=True) native_description = models.CharField(max_length=500, default='', blank=True) type = models.CharField(max_length=100) state = FSMIntegerField(default=States.DRAFT, choices=States.CHOICES) # If offering is not shared, it is available only to following user categories: # 1) staff user; # 2) global support user; # 3) users with active permission in original customer; # 4) users with active permission in allowed customers and nested projects. shared = models.BooleanField(default=False, help_text=_('Anybody can use it')) allowed_customers = models.ManyToManyField(structure_models.Customer, blank=True) objects = managers.MixinManager('scope') tracker = FieldTracker() class Permissions(object): customer_path = 'customer' class Meta(object): verbose_name = _('Offering') class Quotas(quotas_models.QuotaModelMixin.Quotas): order_item_count = quotas_fields.CounterQuotaField( target_models=lambda: [OrderItem], path_to_scope='offering', ) @transition(field=state, source=States.DRAFT, target=States.ACTIVE) def activate(self): pass @transition(field=state, source=States.ACTIVE, target=States.PAUSED) def pause(self): pass @transition(field=state, source='*', target=States.ARCHIVED) def archive(self): pass def __str__(self): return six.text_type(self.name) @classmethod def get_url_name(cls): return 'marketplace-offering'
class OrderItem(core_models.UuidMixin, core_models.ErrorMessageMixin, RequestTypeMixin, TimeStampedModel): class States(object): PENDING = 1 EXECUTING = 2 DONE = 3 ERRED = 4 CHOICES = ( (PENDING, 'pending'), (EXECUTING, 'executing'), (DONE, 'done'), (ERRED, 'erred'), ) TERMINAL_STATES = {DONE, ERRED} order = models.ForeignKey(Order, related_name='items') offering = models.ForeignKey(Offering) attributes = BetterJSONField(blank=True, default=dict) limits = BetterJSONField(blank=True, null=True, default=dict) cost = models.DecimalField(max_digits=22, decimal_places=10, null=True, blank=True) plan = models.ForeignKey('Plan', null=True, blank=True) resource = models.ForeignKey(Resource, null=True, blank=True) state = FSMIntegerField(default=States.PENDING, choices=States.CHOICES) tracker = FieldTracker() class Permissions(object): customer_path = 'order__project__customer' project_path = 'order__project' class Meta(object): verbose_name = _('Order item') ordering = ('created',) @classmethod def get_url_name(cls): return 'marketplace-order-item' @transition(field=state, source=[States.PENDING, States.ERRED], target=States.EXECUTING) def set_state_executing(self): pass @transition(field=state, source=States.EXECUTING, target=States.DONE) def set_state_done(self): pass @transition(field=state, source='*', target=States.ERRED) def set_state_erred(self): pass def clean(self): offering = self.offering customer = self.order.project.customer if offering.shared: return if offering.customer == customer: return if offering.allowed_customers.filter(pk=customer.pk).exists(): return raise ValidationError( _('Offering "%s" is not allowed in organization "%s".') % (offering.name, customer.name) ) def init_cost(self): if self.plan: self.cost = self.plan.get_estimate(self.limits)
class Resource(core_models.UuidMixin, TimeStampedModel, ScopeMixin): """ Core resource is abstract model, marketplace resource is not abstract, therefore we don't need to compromise database query efficiency when we are getting a list of all resources. While migration from ad-hoc resources to marketplace as single entry point is pending, the core resource model may continue to be used in plugins and referenced via generic foreign key, and marketplace resource is going to be used as consolidated model for synchronization with external plugins. Eventually it is expected that core resource model is going to be superseded by marketplace resource model as a primary mean. """ class States(object): CREATING = 1 OK = 2 ERRED = 3 UPDATING = 4 TERMINATING = 5 TERMINATED = 6 CHOICES = ( (CREATING, 'Creating'), (OK, 'OK'), (ERRED, 'Erred'), (UPDATING, 'Updating'), (TERMINATING, 'Terminating'), (TERMINATED, 'Terminated'), ) class Permissions(object): customer_path = 'project__customer' project_path = 'project' state = FSMIntegerField(default=States.CREATING, choices=States.CHOICES) project = models.ForeignKey(structure_models.Project, on_delete=models.CASCADE) offering = models.ForeignKey(Offering, related_name='+', on_delete=models.PROTECT) plan = models.ForeignKey(Plan, null=True, blank=True) attributes = BetterJSONField(blank=True, default=dict) limits = BetterJSONField(blank=True, default=dict) tracker = FieldTracker() objects = managers.MixinManager('scope') @transition(field=state, source=[States.CREATING, States.UPDATING], target=States.OK) def set_state_ok(self): pass @transition(field=state, source='*', target=States.ERRED) def set_state_erred(self): pass @transition(field=state, source='*', target=States.UPDATING) def set_state_updating(self): pass @transition(field=state, source='*', target=States.TERMINATING) def set_state_terminating(self): pass @transition(field=state, source='*', target=States.TERMINATED) def set_state_terminated(self): pass @property def backend_uuid(self): if self.scope: return self.scope.uuid @property def backend_type(self): if self.scope: return self.scope.get_scope_type() def init_quotas(self): if self.limits: components_map = self.offering.get_usage_components() for key, value in self.limits.items(): component = components_map.get(key) if component: ComponentQuota.objects.create( resource=self, component=component, limit=value )
class Offering( core_models.UuidMixin, core_models.NameMixin, core_models.DescribableMixin, quotas_models.QuotaModelMixin, structure_models.StructureModel, TimeStampedModel, core_mixins.ScopeMixin, LoggableMixin, pid_mixins.DataciteMixin, ): class States: DRAFT = 1 ACTIVE = 2 PAUSED = 3 ARCHIVED = 4 CHOICES = ( (DRAFT, 'Draft'), (ACTIVE, 'Active'), (PAUSED, 'Paused'), (ARCHIVED, 'Archived'), ) thumbnail = models.FileField( upload_to='marketplace_service_offering_thumbnails', blank=True, null=True, validators=[ImageValidator], ) full_description = models.TextField(blank=True) vendor_details = models.TextField(blank=True) rating = models.IntegerField( null=True, validators=[MaxValueValidator(5), MinValueValidator(1)], help_text=_('Rating is value from 1 to 5.'), ) category = models.ForeignKey(on_delete=models.CASCADE, to=Category, related_name='offerings') customer = models.ForeignKey( on_delete=models.CASCADE, to=structure_models.Customer, related_name='+', null=True, ) # Volume offering is linked with VPC offering via parent field parent = models.ForeignKey(on_delete=models.CASCADE, to='Offering', null=True, blank=True) attributes = BetterJSONField(blank=True, default=dict, help_text=_('Fields describing Category.')) options = BetterJSONField( blank=True, default=dict, help_text=_('Fields describing Offering request form.'), ) plugin_options = BetterJSONField( blank=True, default=dict, help_text= _('Public data used by specific plugin, such as storage mode for OpenStack.' ), ) secret_options = BetterJSONField( blank=True, default=dict, help_text= _('Private data used by specific plugin, such as credentials and hooks.' ), ) geolocations = JSONField( default=list, blank=True, help_text=_( 'List of latitudes and longitudes. For example: ' '[{"latitude": 123, "longitude": 345}, {"latitude": 456, "longitude": 678}]' ), ) native_name = models.CharField(max_length=160, default='', blank=True) native_description = models.CharField(max_length=500, default='', blank=True) terms_of_service = models.TextField(blank=True) type = models.CharField(max_length=100) state = FSMIntegerField(default=States.DRAFT, choices=States.CHOICES) paused_reason = models.TextField(blank=True) # If offering is not shared, it is available only to following user categories: # 1) staff user; # 2) global support user; # 3) users with active permission in original customer; # 4) users with active permission in allowed customers and nested projects. shared = models.BooleanField(default=True, help_text=_('Accessible to all customers.')) allowed_customers = models.ManyToManyField(structure_models.Customer, blank=True) billable = models.BooleanField( default=True, help_text=_('Purchase and usage is invoiced.')) backend_id = models.CharField(max_length=255, blank=True) objects = managers.OfferingManager() tracker = FieldTracker() class Permissions: customer_path = 'customer' class Meta: verbose_name = _('Offering') class Quotas(quotas_models.QuotaModelMixin.Quotas): order_item_count = quotas_fields.CounterQuotaField( target_models=lambda: [OrderItem], path_to_scope='offering', ) @transition(field=state, source=[States.DRAFT, States.PAUSED], target=States.ACTIVE) def activate(self): pass @transition(field=state, source=States.ACTIVE, target=States.PAUSED) def pause(self): pass @transition(field=state, source='*', target=States.ARCHIVED) def archive(self): pass def __str__(self): return str(self.name) @classmethod def get_url_name(cls): return 'marketplace-offering' def get_usage_components(self): components = self.components.filter( billing_type=OfferingComponent.BillingTypes.USAGE) return {component.type: component for component in components} @cached_property def is_usage_based(self): return self.components.filter( billing_type=OfferingComponent.BillingTypes.USAGE, use_limit_for_billing=False, ).exists() @property def is_private(self): return not self.billable and not self.shared def get_datacite_title(self): return self.name def get_datacite_creators_name(self): return self.customer.name def get_datacite_description(self): return self.description def get_datacite_publication_year(self): return self.created.year def get_datacite_url(self): return settings.WALDUR_MARKETPLACE['OFFERING_LINK_TEMPLATE'].format( offering_uuid=self.uuid.hex)
class Resource( ResourceDetailsMixin, core_models.UuidMixin, core_models.BackendMixin, TimeStampedModel, core_mixins.ScopeMixin, structure_models.StructureLoggableMixin, ): """ Core resource is abstract model, marketplace resource is not abstract, therefore we don't need to compromise database query efficiency when we are getting a list of all resources. While migration from ad-hoc resources to marketplace as single entry point is pending, the core resource model may continue to be used in plugins and referenced via generic foreign key, and marketplace resource is going to be used as consolidated model for synchronization with external plugins. Eventually it is expected that core resource model is going to be superseded by marketplace resource model as a primary mean. """ class States: CREATING = 1 OK = 2 ERRED = 3 UPDATING = 4 TERMINATING = 5 TERMINATED = 6 CHOICES = ( (CREATING, 'Creating'), (OK, 'OK'), (ERRED, 'Erred'), (UPDATING, 'Updating'), (TERMINATING, 'Terminating'), (TERMINATED, 'Terminated'), ) class Permissions: customer_path = 'project__customer' project_path = 'project' state = FSMIntegerField(default=States.CREATING, choices=States.CHOICES) project = models.ForeignKey(structure_models.Project, on_delete=models.CASCADE) backend_metadata = BetterJSONField(blank=True, default=dict) report = BetterJSONField(blank=True, null=True) current_usages = BetterJSONField(blank=True, default=dict) tracker = FieldTracker() objects = managers.MixinManager('scope') @property def customer(self): return self.project.customer @transition( field=state, source=[States.ERRED, States.CREATING, States.UPDATING], target=States.OK, ) def set_state_ok(self): pass @transition(field=state, source='*', target=States.ERRED) def set_state_erred(self): pass @transition(field=state, source='*', target=States.UPDATING) def set_state_updating(self): pass @transition(field=state, source='*', target=States.TERMINATING) def set_state_terminating(self): pass @transition(field=state, source='*', target=States.TERMINATED) def set_state_terminated(self): pass @property def backend_uuid(self): if self.scope: return self.scope.uuid @property def backend_type(self): if self.scope: scope_type = self.scope.get_scope_type() return scope_type if scope_type else 'Marketplace.Resource' def init_quotas(self): if self.limits: components_map = self.offering.get_limit_components() for key, value in self.limits.items(): component = components_map.get(key) if component: ComponentQuota.objects.create(resource=self, component=component, limit=value) def get_log_fields(self): return ( 'uuid', 'name', 'project', 'offering', 'created', 'modified', 'attributes', 'cost', 'plan', 'limits', 'get_state_display', 'backend_metadata', 'backend_uuid', 'backend_type', ) @property def invoice_registrator_key(self): return self.offering.type @classmethod def get_scope_type(cls): return 'Marketplace.Resource' @classmethod def get_url_name(cls): return 'marketplace-resource' @property def is_expired(self): return self.end_date and self.end_date <= timezone.datetime.today( ).date() def __str__(self): if self.name: return f'{self.name} ({self.offering.name})' if self.scope: return f'{self.name} ({self.content_type} / {self.object_id})' return f'{self.uuid} ({self.offering.name})'
class User( LoggableMixin, UuidMixin, DescribableMixin, AbstractBaseUser, UserDetailsMixin, PermissionsMixin, ): username = models.CharField( _('username'), max_length=128, unique=True, help_text=_('Required. 128 characters or fewer. Letters, numbers and ' '@/./+/-/_ characters'), validators=[ validators.RegexValidator(re.compile(r'^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid') ], ) # Civil number is nullable on purpose, otherwise # it wouldn't be possible to put a unique constraint on it civil_number = models.CharField( _('civil number'), max_length=50, unique=True, blank=True, null=True, default=None, ) email = models.EmailField(_('email address'), max_length=75, blank=True) is_staff = models.BooleanField( _('staff status'), default=False, help_text=_('Designates whether the user can log into this admin ' 'site.'), ) is_active = models.BooleanField( _('active'), default=True, help_text=_('Designates whether this user should be treated as ' 'active. Unselect this instead of deleting accounts.'), ) is_support = models.BooleanField( _('support status'), default=False, help_text=_('Designates whether the user is a global support user.'), ) date_joined = models.DateTimeField(_('date joined'), default=django_timezone.now) registration_method = models.CharField( _('registration method'), max_length=50, default='default', blank=True, help_text=_('Indicates what registration method were used.'), ) agreement_date = models.DateTimeField( _('agreement date'), blank=True, null=True, help_text=_('Indicates when the user has agreed with the policy.'), ) preferred_language = models.CharField(max_length=10, blank=True) competence = models.CharField(max_length=255, blank=True) token_lifetime = models.PositiveIntegerField( null=True, help_text=_('Token lifetime in seconds.'), validators=[validators.MinValueValidator(60)], ) details = BetterJSONField( blank=True, default=dict, help_text=_('Extra details from authentication backend.'), ) backend_id = models.CharField(max_length=255, blank=True) tracker = FieldTracker() objects = UserManager() USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email'] class Meta: verbose_name = _('user') verbose_name_plural = _('users') def get_log_fields(self): return ( 'uuid', 'full_name', 'native_name', self.USERNAME_FIELD, 'is_staff', 'is_support', 'token_lifetime', ) def get_full_name(self): # This method is used in django-reversion as name of revision creator. return self.full_name def get_short_name(self): # This method is used in django-reversion as name of revision creator. return self.full_name def email_user(self, subject, message, from_email=None): """ Sends an email to this User. """ send_mail(subject, message, from_email, [self.email]) @classmethod def get_permitted_objects(cls, user): from waldur_core.structure.filters import filter_visible_users queryset = User.objects.all() if user.is_staff or user.is_support: return queryset else: return filter_visible_users(queryset, user) def clean(self): super(User, self).clean() # User email has to be unique or empty if (self.email and User.objects.filter(email=self.email).exclude( id=self.id).exists()): raise ValidationError({ 'email': _('User with email "%s" already exists.') % self.email }) @transaction.atomic def create_request_for_update_email(self, email): if User.objects.filter(email=email).exclude(id=self.id).exists(): raise ValidationError( {'email': _('User with email "%s" already exists.') % email}) ChangeEmailRequest.objects.filter(user=self).delete() change_request = ChangeEmailRequest.objects.create( user=self, email=email, ) return change_request def __str__(self): if self.full_name: return '%s (%s)' % (self.get_username(), self.full_name) return self.get_username()
class OrderItem( core_models.UuidMixin, core_models.ErrorMessageMixin, RequestTypeMixin, structure_models.StructureLoggableMixin, TimeStampedModel, ): class States: PENDING = 1 EXECUTING = 2 DONE = 3 ERRED = 4 TERMINATED = 5 TERMINATING = 6 CHOICES = ( (PENDING, 'pending'), (EXECUTING, 'executing'), (DONE, 'done'), (ERRED, 'erred'), (TERMINATED, 'terminated'), (TERMINATING, 'terminating'), ) TERMINAL_STATES = {DONE, ERRED} order = models.ForeignKey(on_delete=models.CASCADE, to=Order, related_name='items') offering = models.ForeignKey(on_delete=models.CASCADE, to=Offering) attributes = BetterJSONField(blank=True, default=dict) old_plan = models.ForeignKey(on_delete=models.CASCADE, to=Plan, related_name='+', null=True, blank=True) resource = models.ForeignKey(on_delete=models.CASCADE, to=Resource, null=True, blank=True) state = FSMIntegerField(default=States.PENDING, choices=States.CHOICES) activated = models.DateTimeField(_('activation date'), null=True, blank=True) output = models.TextField(blank=True) tracker = FieldTracker() class Permissions: customer_path = 'order__project__customer' project_path = 'order__project' class Meta: verbose_name = _('Order item') ordering = ('created', ) @classmethod def get_url_name(cls): return 'marketplace-order-item' @transition(field=state, source=[States.PENDING, States.ERRED], target=States.EXECUTING) def set_state_executing(self): pass @transition(field=state, source=States.EXECUTING, target=States.DONE) def set_state_done(self): self.activated = timezone.now() self.save() @transition(field=state, source='*', target=States.ERRED) def set_state_erred(self): pass @transition(field=state, source='*', target=States.TERMINATED) def set_state_terminated(self): pass @transition( field=state, source=[States.PENDING, States.EXECUTING], target=States.TERMINATING, ) def set_state_terminating(self): pass def clean(self): offering = self.offering customer = self.order.project.customer if offering.shared: return if offering.customer == customer: return if offering.allowed_customers.filter(pk=customer.pk).exists(): return raise ValidationError( _('Offering "%s" is not allowed in organization "%s".') % (offering.name, customer.name)) def get_log_fields(self): return ( 'uuid', 'created', 'modified', 'cost', 'limits', 'attributes', 'offering', 'resource', 'plan', 'get_state_display', 'get_type_display', ) @property def safe_attributes(self): """ Get attributes excluding secret attributes, such as username and password. """ secret_attributes = plugins.manager.get_secret_attributes( self.offering.type) attributes = self.attributes or {} return { key: attributes[key] for key in attributes.keys() if key not in secret_attributes } def __str__(self): return 'type: %s, created_by: %s' % ( self.get_type_display(), self.order.created_by, )