def test_formfield(self): from jsonfield.forms import JSONFormField from jsonfield.widgets import JSONWidget field = JSONField("test") field.set_attributes_from_name("json") formfield = field.formfield() self.assertEquals(type(formfield), JSONFormField) self.assertEquals(type(formfield.widget), JSONWidget)
def test_formfield_blank_clean_none(self): # Hmm, I'm not sure how to do this. What happens if we pass a # None to a field that has null=False? field = JSONField("test", null=False, blank=True) formfield = field.formfield() self.assertEqual(formfield.clean(value=None), None)
def test_db_json_type(self): field = JSONField(db_json_type='bob') self.assertEqual(field.db_type(connection=None), 'bob')
class InvalidModuleEncoderFieldTestModel(models.Model): json = JSONField( encoder_class='unknown_module.UnknownJSONEncoder')
def test_empty_strings_not_allowed(self): field = JSONField() self.assertEqual(field.get_default(), None)
def test_formfield_blank_clean_blank(self): field = JSONField("test", null=False, blank=True) formfield = field.formfield() self.assertEqual(formfield.clean(value=''), '')
class AbstractNotification(models.Model): """ Action model describing the actor acting out a verb (on an optional target). Nomenclature based on http://activitystrea.ms/specs/atom/1.0/ Generalized Format:: <actor> <verb> <time> <actor> <verb> <target> <time> <actor> <verb> <action_object> <target> <time> Examples:: <justquick> <reached level 60> <1 minute ago> <brosner> <commented on> <pinax/pinax> <2 hours ago> <washingtontimes> <started follow> <justquick> <8 minutes ago> <mitsuhiko> <closed> <issue 70> on <mitsuhiko/flask> <about 2 hours ago> Unicode Representation:: justquick reached level 60 1 minute ago mitsuhiko closed issue 70 on mitsuhiko/flask 3 hours ago HTML Representation:: <a href="http://oebfare.com/">brosner</a> commented on <a href="http://github.com/pinax/pinax">pinax/pinax</a> 2 hours ago # noqa """ LEVELS = Choices('success', 'info', 'warning', 'error') level = models.CharField(choices=LEVELS, default=LEVELS.info, max_length=20) recipient = models.ForeignKey( settings.AUTH_USER_MODEL, blank=False, related_name='notifications', on_delete=models.CASCADE ) unread = models.BooleanField(default=True, blank=False, db_index=True) actor_content_type = models.ForeignKey(ContentType, related_name='notify_actor', on_delete=models.CASCADE) actor_object_id = models.CharField(max_length=255) actor = GenericForeignKey('actor_content_type', 'actor_object_id') verb = models.CharField(max_length=255) description = models.TextField(blank=True, null=True) target_content_type = models.ForeignKey( ContentType, related_name='notify_target', blank=True, null=True, on_delete=models.CASCADE ) target_object_id = models.CharField(max_length=255, blank=True, null=True) target = GenericForeignKey('target_content_type', 'target_object_id') action_object_content_type = models.ForeignKey(ContentType, blank=True, null=True, related_name='notify_action_object', on_delete=models.CASCADE) action_object_object_id = models.CharField(max_length=255, blank=True, null=True) action_object = GenericForeignKey('action_object_content_type', 'action_object_object_id') created = models.DateTimeField(default=timezone.now, db_index=True) public = models.BooleanField(default=True, db_index=True) deleted = models.BooleanField(default=False, db_index=True) emailed = models.BooleanField(default=False, db_index=True) data = JSONField(blank=True, null=True) objects = NotificationQuerySet.as_manager() class Meta: abstract = True ordering = ('-created',) app_label = 'notifications' # speed up notifications count query index_together = ('recipient', 'unread') def __str__(self): ctx = { 'actor': self.actor, 'verb': self.verb, 'action_object': self.action_object, 'target': self.target, 'timesince': self.timesince() } if self.target: if self.action_object: return u'%(actor)s %(verb)s %(action_object)s on %(target)s %(timesince)s ago' % ctx return u'%(actor)s %(verb)s %(target)s %(timesince)s ago' % ctx if self.action_object: return u'%(actor)s %(verb)s %(action_object)s %(timesince)s ago' % ctx return u'%(actor)s %(verb)s %(timesince)s ago' % ctx def timesince(self, now=None): """ Shortcut for the ``django.utils.timesince.timesince`` function of created. """ from django.utils.timesince import timesince as timesince_ return timesince_(self.created, now) @property def slug(self): return id2slug(self.id) def mark_as_read(self): if self.unread: self.unread = False self.save() def mark_as_unread(self): if not self.unread: self.unread = True self.save()
class CascadeModelBase(CMSPlugin): """ The container to hold additional HTML element tags. """ class Meta: abstract = True cmsplugin_ptr = models.OneToOneField(CMSPlugin, related_name='+', parent_link=True) glossary = JSONField(blank=True, default={}) def __str__(self): return self.plugin_class.get_identifier(self) @property def plugin_class(self): if not hasattr(self, '_plugin_class'): self._plugin_class = self.get_plugin_class() return self._plugin_class @property def tag_type(self): return self.plugin_class.get_tag_type(self) @property def css_classes(self): css_classes = self.plugin_class.get_css_classes(self) return mark_safe(' '.join(c for c in css_classes if c)) @property def inline_styles(self): inline_styles = self.plugin_class.get_inline_styles(self) return format_html_join(' ', '{0}: {1};', (s for s in inline_styles.items() if s[1])) @property def html_tag_attributes(self): attributes = self.plugin_class.get_html_tag_attributes(self) return format_html_join(' ', '{0}="{1}"', ((attr, val) for attr, val in attributes.items() if val)) def get_parent(self): raise NotImplementedError( "This method is deprecated. Use `get_parent_instance` instead.") def get_parent_glossary(self): """ Return the glossary from the parent of this object. If there is no parent, retrieve the glossary from the placeholder settings, if configured. """ for model in CascadeModelBase._get_cascade_elements(): try: parent = model.objects.get(id=self.parent_id) except model.DoesNotExist: continue else: return parent.get_complete_glossary() # use self.placeholder.glossary as the starting dictionary template = self.placeholder.page.template if self.placeholder.page else None return get_placeholder_conf('glossary', self.placeholder.slot, template=template, default={}) def get_complete_glossary(self): """ Return the parent glossary for this model object merged with the current object. This is done by starting from the root element down to the current element and enriching the glossary with each models's own glossary. """ if not hasattr(self, '_complete_glossary_cache'): self._complete_glossary_cache = self.get_parent_glossary().copy() self._complete_glossary_cache.update(self.glossary or {}) return self._complete_glossary_cache def sanitize_children(self): """ Recursively walk down the plugin tree and invoke method ``save(sanitize_only=True)`` for each child. """ for model in CascadeModelBase._get_cascade_elements(): # execute query to not iterate over SELECT ... FROM while updating other models children = list(model.objects.filter(parent_id=self.id)) for child in children: child.save(sanitize_only=True) child.sanitize_children() def save(self, sanitize_only=False, *args, **kwargs): """ A hook which let the plugin instance sanitize the current object model while saving it. With ``sanitize_only=True``, the current model object only is saved when the method ``sanitize_model()`` from the corresponding plugin actually changed the glossary. """ sanitized = self.plugin_class.sanitize_model(self) if sanitize_only: if sanitized: super(CascadeModelBase, self).save(no_signals=True) else: super(CascadeModelBase, self).save(*args, **kwargs) def get_data_representation(self): """ Hook to return a serializable representation of this element. """ return {'glossary': self.glossary} @classmethod def _get_cascade_elements(cls): """ Returns a set of models which are derived from ``CascadeModelBase``. This set shall be used for traversing the plugin tree of interconnected Cascade models. Currently, Cascade itself offers only one model, namely ``CascadeElement``, but a third party library may extend ``CascadeModelBase`` and add arbitrary model fields. """ if not hasattr(cls, '_cached_cascade_elements'): cce = set([ p.model._meta.concrete_model for p in plugin_pool.get_all_plugins() if issubclass(p.model, cls) ]) cls._cached_cascade_elements = cce return cls._cached_cascade_elements
class CreditRequirement(TimeStampedModel): """ This model represents a credit requirement. Each requirement is uniquely identified by its 'namespace' and 'name' fields. The 'name' field stores the unique name or location (in case of XBlock) for a requirement, which serves as the unique identifier for that requirement. The 'display_name' field stores the display name of the requirement. The 'criteria' field dictionary provides additional information, clients may need to determine whether a user has satisfied the requirement. """ course = models.ForeignKey(CreditCourse, related_name="credit_requirements") namespace = models.CharField(max_length=255) name = models.CharField(max_length=255) display_name = models.CharField(max_length=255, default="") order = models.PositiveIntegerField(default=0) criteria = JSONField() active = models.BooleanField(default=True) class Meta(object): """ Model metadata. """ unique_together = ('namespace', 'name', 'course') @classmethod def add_or_update_course_requirement(cls, credit_course, requirement, order): """ Add requirement to a given course. Args: credit_course(CreditCourse): The identifier for credit course requirement(dict): Requirement dict to be added Returns: (CreditRequirement, created) tuple """ credit_requirement, created = cls.objects.get_or_create( course=credit_course, namespace=requirement["namespace"], name=requirement["name"], defaults={ "display_name": requirement["display_name"], "criteria": requirement["criteria"], "order": order, "active": True }) if not created: credit_requirement.criteria = requirement["criteria"] credit_requirement.active = True credit_requirement.order = order credit_requirement.display_name = requirement["display_name"] credit_requirement.save() return credit_requirement, created @classmethod def get_course_requirements(cls, course_key, namespace=None, name=None): """ Get credit requirements of a given course. Args: course_key (CourseKey): The identifier for a course Keyword Arguments namespace (str): Optionally filter credit requirements by namespace. name (str): Optionally filter credit requirements by name. Returns: QuerySet of CreditRequirement model """ # order credit requirements according to their appearance in courseware requirements = CreditRequirement.objects.filter( course__course_key=course_key, active=True).order_by("-order") if namespace is not None: requirements = requirements.filter(namespace=namespace) if name is not None: requirements = requirements.filter(name=name) return requirements @classmethod def disable_credit_requirements(cls, requirement_ids): """ Mark the given requirements inactive. Args: requirement_ids(list): List of ids Returns: None """ cls.objects.filter(id__in=requirement_ids).update(active=False) @classmethod def get_course_requirement(cls, course_key, namespace, name): """ Get credit requirement of a given course. Args: course_key(CourseKey): The identifier for a course namespace(str): Namespace of credit course requirements name(str): Name of credit course requirement Returns: CreditRequirement object if exists """ try: return cls.objects.get(course__course_key=course_key, active=True, namespace=namespace, name=name) except cls.DoesNotExist: return None
def test_formfield_clean_none(self): field = JSONField("test") formfield = field.formfield() self.assertRaisesMessage(forms.ValidationError, force_text(formfield.error_messages['required']), formfield.clean, value=None)
def test_db_prep_save(self): field = JSONField("test") field.set_attributes_from_name("json") self.assertEquals(None, field.get_db_prep_save(None, connection=None)) self.assertEquals('{"spam": "eggs"}', field.get_db_prep_save({"spam": "eggs"}, connection=None))
def test_invalid_json_default(self): with self.assertRaises(ValueError): field = JSONField('test', default='{"foo"}')
class RetirementProjection(models.Model): plan = models.OneToOneField(RetirementPlan, null=True, on_delete=models.CASCADE, related_name='projection') proj_data = JSONField( null=True, blank=True, help_text="Calculated Projection data for api response") on_track = models.BooleanField( default=False, null=False, help_text="Whether the retirement plan is on track") #user income_actual_monthly = JSONField( null=True, blank=True, help_text="List of monthly actual income") income_desired_monthly = JSONField( null=True, blank=True, help_text="List of monthly desired income") taxable_assets_monthly = JSONField( null=True, blank=True, help_text="List of monthly taxable assets") nontaxable_assets_monthly = JSONField( null=True, blank=True, help_text="List of monthly nontaxable assets") proj_balance_at_retire_in_todays = models.FloatField( default=0, null=True, help_text="Projected balance at retirement in today's money") proj_inc_actual_at_retire_in_todays = models.FloatField( default=0, null=True, help_text= "Projected monthly income actual at retirement in today's money") proj_inc_desired_at_retire_in_todays = models.FloatField( default=0, null=True, help_text= "Projected monthly income desired at retirement in today's money") savings_end_date_as_age = models.FloatField( default=0, null=True, help_text= "Projected age post retirement when taxable assets first deplete to zero" ) current_percent_soc_sec = models.FloatField( default=0, null=True, help_text= "Current percentage of monthly income represented by payments made towards social security" ) current_percent_medicare = models.FloatField( default=0, null=True, help_text= "Current percentage of monthly income represented by payments made towards medicare" ) current_percent_fed_tax = models.FloatField( default=0, null=True, help_text= "Current percentage of monthly income represented by payments made towards federal taxes" ) current_percent_state_tax = models.FloatField( default=0, null=True, help_text= "Current percentage of monthly income represented by payments made towards state taxes" ) non_taxable_inc = JSONField( null=True, blank=True, help_text="List of annual non taxable monthly income received") tot_taxable_dist = JSONField( null=True, blank=True, help_text="List of annual total taxable distributions received") annuity_payments = JSONField( null=True, blank=True, help_text="List of ammual annuity payments received") pension_payments = JSONField( null=True, blank=True, help_text="List of annual pension payments received") ret_working_inc = JSONField( null=True, blank=True, help_text="List of annual retirement working payments received") soc_sec_benefit = JSONField( null=True, blank=True, help_text="List of annual social security benefit payments received") taxable_accounts = JSONField(null=True, blank=True, help_text="List of annual taxable accounts") non_taxable_accounts = JSONField( null=True, blank=True, help_text="List of annual nontaxable accounts") list_of_account_balances = JSONField(null=True, blank=True, help_text="List of annual accounts") reverse_mort = models.BooleanField( default=False, null=False, help_text="Whether user has a reverse mortgage") house_value = models.FloatField(default=0, null=True, help_text="Current value of house") house_value_at_retire_in_todays = models.FloatField( default=0, null=True, help_text="Future value of house in todays") reverse_mort_pymnt_at_retire_in_todays = models.FloatField( default=0, null=True, help_text="Future value of monthly reverse mortgage payment in todays") #partner part_income_actual_monthly = JSONField( null=True, blank=True, help_text="List of monthly actual income") part_income_desired_monthly = JSONField( null=True, blank=True, help_text="List of monthly desired income") part_taxable_assets_monthly = JSONField( null=True, blank=True, help_text="List of monthly taxable assets") part_nontaxable_assets_monthly = JSONField( null=True, blank=True, help_text="List of monthly nontaxable assets") part_proj_balance_at_retire_in_todays = models.FloatField( default=0, null=True, help_text="Projected balance at retirement in today's money") part_proj_inc_actual_at_retire_in_todays = models.FloatField( default=0, null=True, help_text= "Projected monthly income actual at retirement in today's money") part_proj_inc_desired_at_retire_in_todays = models.FloatField( default=0, null=True, help_text= "Projected monthly income desired at retirement in today's money") part_savings_end_date_as_age = models.FloatField( default=0, null=True, help_text= "Projected age post retirement when taxable assets first deplete to zero" ) part_current_percent_soc_sec = models.FloatField( default=0, null=True, help_text= "Current percentage of monthly income represented by payments made towards social security" ) part_current_percent_medicare = models.FloatField( default=0, null=True, help_text= "Current percentage of monthly income represented by payments made towards medicare" ) part_current_percent_fed_tax = models.FloatField( default=0, null=True, help_text= "Current percentage of monthly income represented by payments made towards federal taxes" ) part_current_percent_state_tax = models.FloatField( default=0, null=True, help_text= "Current percentage of monthly income represented by payments made towards state taxes" ) part_non_taxable_inc = JSONField( null=True, blank=True, help_text="List of annual non taxable monthly income received") part_tot_taxable_dist = JSONField( null=True, blank=True, help_text="List of annual total taxable distributions received") part_annuity_payments = JSONField( null=True, blank=True, help_text="List of annual annuity payments received") part_pension_payments = JSONField( null=True, blank=True, help_text="List of annual pension payments received") part_ret_working_inc = JSONField( null=True, blank=True, help_text="List of annual retirement working payments received") part_soc_sec_benefit = JSONField( null=True, blank=True, help_text="List of annual social security benefit payments received") part_taxable_accounts = JSONField( null=True, blank=True, help_text="List of annual taxable accounts") part_non_taxable_accounts = JSONField( null=True, blank=True, help_text="List of annual nontaxable accounts") part_list_of_account_balances = JSONField( null=True, blank=True, help_text="List of annual accounts") part_house_value = models.FloatField(default=0, null=True, help_text="Current value of house") part_house_value_at_retire_in_todays = models.FloatField( default=0, null=True, help_text="Future value of house in todays") part_reverse_mort_pymnt_at_retire_in_todays = models.FloatField( default=0, null=True, help_text="Future value of monthly reverse mortgage payment in todays") def save(self, *args, **kwargs): super(RetirementProjection, self).save(*args, **kwargs) def __str__(self): return "{} Projection {}".format(self.plan, self.id)
class RetirementPlan(TimestampedModel): class AccountCategory(ChoiceEnum): EMPLOYER_DESCRETIONARY_CONTRIB = 1, 'Employer Discretionary Contributions' EMPLOYER_MATCHING_CONTRIB = 2, 'Employer Matching Contributions' SALARY_PRE_TAX_ELECTIVE_DEFERRAL = 3, 'Salary/ Pre-Tax Elective Deferral' AFTER_TAX_ROTH_CONTRIB = 4, 'After-Tax Roth Contributions' AFTER_TAX_CONTRIB = 5, 'After Tax Contributions' SELF_EMPLOYED_PRE_TAX_CONTRIB = 6, 'Self Employed Pre-Tax Contributions' SELF_EMPLOYED_AFTER_TAX_CONTRIB = 7, 'Self Employed After Tax Contributions' DORMANT_ACCOUNT_NO_CONTRIB = 8, 'Dormant / Inactive' class LifestyleCategory(ChoiceEnum): OK = 1, 'Doing OK' COMFORTABLE = 2, 'Comfortable' WELL = 3, 'Doing Well' LUXURY = 4, 'Luxury' class ExpenseCategory(ChoiceEnum): ALCOHOLIC_BEVERAGE = 1, 'Alcoholic Beverage' APPAREL_SERVICES = 2, 'Apparel & Services' EDUCATION = 3, 'Education' ENTERTAINMENT = 4, 'Entertainment' FOOD = 5, 'Food' HEALTHCARE = 6, 'Healthcare' HOUSING = 7, 'Housing' INSURANCE_PENSIONS_SOCIAL_SECURITY = 8, 'Insuarance, Pensions & Social Security' PERSONAL_CARE = 9, 'Personal Care' READING = 10, 'Reading' SAVINGS = 11, 'Savings' TAXES = 12, 'Taxes' TOBACCO = 13, 'Tobacco' TRANSPORTATION = 14, 'Transportation' MISCELLANEOUS = 15, 'Miscellaneous' class SavingCategory(ChoiceEnum): HEALTH_GAP = 1, 'Health Gap' EMPLOYER_CONTRIBUTION = 2, 'Employer Retirement Contributions' TAXABLE_PRC = 3, 'Taxable Personal Retirement Contributions' TAX_PAID_PRC = 4, 'Tax-paid Personal Retirement Contributions' PERSONAL = 5, 'Personal' INHERITANCE = 6, 'Inheritance' class HomeStyle(ChoiceEnum): SINGLE_DETACHED = 1, 'Single, Detached' SINGLE_ATTACHED = 2, 'Single, Attached' MULTI_9_OR_LESS = 3, 'Multi-Unit, 9 or less' MULTI_10_TO_20 = 4, 'Multi-Unit, 10 - 20' MULTI_20_PLUS = 5, 'Multi-Unit, 20+' MOBILE_HOME = 6, 'Mobile Home' RV = 7, 'RV, Van, Boat, etc' name = models.CharField(max_length=128, blank=True, null=True) description = models.TextField(null=True, blank=True) client = models.ForeignKey('client.Client') partner_plan = models.OneToOneField('RetirementPlan', related_name='partner_plan_reverse', null=True, on_delete=models.SET_NULL) lifestyle = models.PositiveIntegerField( choices=LifestyleCategory.choices(), default=1, help_text="The desired retirement lifestyle") desired_income = models.PositiveIntegerField( help_text= "The desired annual household pre-tax retirement income in system currency" ) income = models.PositiveIntegerField( help_text= "The current annual personal pre-tax income at the start of your plan") volunteer_days = models.PositiveIntegerField( validators=[MinValueValidator(0), MaxValueValidator(7)], help_text="The number of volunteer work days selected") paid_days = models.PositiveIntegerField( validators=[MinValueValidator(0), MaxValueValidator(7)], help_text="The number of paid work days selected") same_home = models.BooleanField( help_text="Will you be retiring in the same home?") same_location = models.NullBooleanField( help_text="Will you be retiring in the same general location?", blank=True, null=True) retirement_postal_code = models.CharField( max_length=10, validators=[MinLengthValidator(5), MaxLengthValidator(10)], help_text="What postal code will you retire in?") reverse_mortgage = models.BooleanField( help_text="Would you consider a reverse mortgage? (optional)") retirement_home_style = models.PositiveIntegerField( choices=HomeStyle.choices(), null=True, blank=True, help_text="The style of your retirement home") retirement_home_price = models.PositiveIntegerField( null=True, blank=True, help_text= "The price of your future retirement home (in today's dollars)") beta_partner = models.BooleanField( default=False, help_text="Will BetaSmartz manage your partner's " "retirement assets as well?") retirement_accounts = JSONField( null=True, blank=True, help_text= "List of retirement accounts [{id, name, acc_type, owner, balance, balance_efdt, contrib_amt, contrib_period, employer_match, employer_match_type},...]" ) expenses = JSONField( null=True, blank=True, help_text="List of expenses [{id, desc, cat, who, amt},...]") savings = JSONField( null=True, blank=True, help_text="List of savings [{id, desc, cat, who, amt},...]") initial_deposits = JSONField( null=True, blank=True, help_text="List of deposits [{id, asset, goal, amt},...]") income_growth = models.FloatField( default=0, help_text="Above consumer price index (inflation)") expected_return_confidence = models.FloatField( validators=[MinValueValidator(0), MaxValueValidator(1)], help_text="Planned confidence of the portfolio returns given the " "volatility and risk predictions.") retirement_age = models.PositiveIntegerField() btc = models.PositiveIntegerField(help_text="Annual personal before-tax " "contributions", blank=True) atc = models.PositiveIntegerField(help_text="Annual personal after-tax " "contributions", blank=True) max_employer_match_percent = models.FloatField( null=True, blank=True, help_text="The percent the employer matches of before-tax contributions" ) desired_risk = models.FloatField( validators=[MinValueValidator(0), MaxValueValidator(1)], help_text="The selected risk appetite for this retirement plan") # This is a field, not calculated, so we have a historical record of the value. recommended_risk = models.FloatField( editable=False, blank=True, validators=[MinValueValidator(0), MaxValueValidator(1)], help_text="The calculated recommended risk for this retirement plan") # This is a field, not calculated, so we have a historical record of the value. max_risk = models.FloatField( editable=False, blank=True, validators=[MinValueValidator(0), MaxValueValidator(1)], help_text="The maximum allowable risk appetite for this retirement " "plan, based on our risk model") # calculated_life_expectancy should be calculated, # read-only don't let client create/update calculated_life_expectancy = models.PositiveIntegerField(editable=False, blank=True) selected_life_expectancy = models.PositiveIntegerField() agreed_on = models.DateTimeField(null=True, blank=True) goal_setting = models.OneToOneField(GoalSetting, null=True, related_name='retirement_plan', on_delete=PROTECT) partner_data = JSONField(null=True, blank=True) # balance of retirement account number balance = models.FloatField(null=True, blank=True) date_of_estimate = models.DateField(null=True, blank=True) # Install the custom manager that knows how to filter. objects = RetirementPlanQuerySet.as_manager() class Meta: unique_together = ('name', 'client') def __init__(self, *args, **kwargs): # Keep a copy of agreed_on so we can see if it's changed super(RetirementPlan, self).__init__(*args, **kwargs) self.__was_agreed = self.agreed_on def __str__(self): return "RetirementPlan {}".format(self.id) @property def was_agreed(self): return self.__was_agreed @transaction.atomic def set_settings(self, new_setting): """ Updates the retirement plan with the new settings, and saves the plan :param new_setting: The new setting to set. :return: """ old_setting = self.goal_setting self.goal_setting = new_setting if not (old_setting and old_setting.retirement_plan.agreed_on): self.save() if old_setting is not None: old_group = old_setting.metric_group custom_group = old_group.type == GoalMetricGroup.TYPE_CUSTOM last_user = old_group.settings.count() == 1 try: old_setting.delete() except Exception as e: logger.error(e) if custom_group and last_user: old_group.delete() @cached_property def spendable_income(self): if isinstance(self.savings, str): savings = json.loads(self.savings) else: savings = self.savings if isinstance(self.expenses, str): expenses = json.loads(self.expenses) else: expenses = self.expenses if self.savings: savings_cost = sum([s.get('amt', 0) for s in savings]) else: savings_cost = 0 if self.expenses: expenses_cost = sum([e.get('amt', 0) for e in expenses]) else: expenses_cost = 0 return self.income - savings_cost - expenses_cost def save(self, *args, **kwargs): """ Override save() so we can do some custom validation of partner plans. """ self.calculated_life_expectancy = self.client.life_expectancy bas_scores = self.client.get_risk_profile_bas_scores() self.recommended_risk = GoalSettingRiskProfile._recommend_risk( bas_scores) self.max_risk = GoalSettingRiskProfile._max_risk(bas_scores) if self.was_agreed: raise ValidationError( "Cannot save a RetirementPlan that has been agreed upon") reverse_plan = getattr(self, 'partner_plan_reverse', None) if self.partner_plan is not None and reverse_plan is not None and \ self.partner_plan != reverse_plan: raise ValidationError( "Partner plan relationship must be symmetric.") super(RetirementPlan, self).save(*args, **kwargs) if self.get_soa() is None and self.id is not None: self.generate_soa() def get_soa(self): from statements.models import RetirementStatementOfAdvice qs = RetirementStatementOfAdvice.objects.filter( retirement_plan_id=self.pk) if qs.count(): self.statement_of_advice = qs[0] return qs[0] else: return self.generate_soa() def generate_soa(self): from statements.models import RetirementStatementOfAdvice soa = RetirementStatementOfAdvice(retirement_plan_id=self.id) soa.save() return soa def send_plan_agreed_email(self): try: send_plan_agreed_email_task.delay(self.id) except: self._send_plan_agreed_email(self.id) @staticmethod def _send_plan_agreed_email(plan_id): plan = RetirementPlan.objects.get(pk=plan_id) soa = plan.get_soa() pdf_content = soa.save_pdf() partner_name = plan.partner_data[ 'name'] if plan.client.is_married and plan.partner_data else None context = { 'site': Site.objects.get_current(), 'client': plan.client, 'advisor': plan.client.advisor, 'firm': plan.client.firm, 'partner_name': partner_name } # Send to client subject = "Your BetaSmartz Retirement Plan Completed" html_content = render_to_string( 'email/retiresmartz/plan_agreed_client.html', context) email = EmailMessage(subject, html_content, None, [plan.client.user.email]) email.content_subtype = "html" email.attach('SOA.pdf', pdf_content, 'application/pdf') email.send() # Send to advisor subject = "Your clients have completed their Retirement Plan" if partner_name else \ "Your client completed a Retirement Plan" html_content = render_to_string( 'email/retiresmartz/plan_agreed_advisor.html', context) email = EmailMessage(subject, html_content, None, [plan.client.advisor.user.email]) email.content_subtype = "html" email.attach('SOA.pdf', pdf_content, 'application/pdf') email.send() @property def portfolio(self): return self.goal_setting.portfolio if self.goal_setting else None @cached_property def on_track(self): if hasattr(self, '_on_track'): return self._on_track self._on_track = False return self._on_track @property def opening_tax_deferred_balance(self): # TODO: Sum the complete amount that is expected to be in the retirement plan accounts on account opening. return 0 @property def opening_tax_paid_balance(self): # TODO: Sum the complete amount that is expected to be in the retirement plan accounts on account opening. return 0 @property def replacement_ratio(self): partner_income = 0 if self.partner_data is not None: partner_income = self.partner_plan.income return self.desired_income / (self.income + partner_income) @staticmethod def get_lifestyle_text(lifestyle): return get_text_of_choices_enum( lifestyle, RetirementPlan.LifestyleCategory.choices()) @cached_property def lifestyle_text(self): return RetirementPlan.get_lifestyle_text(self.lifestyle) @staticmethod def get_expense_category_text(expense_cat): return get_text_of_choices_enum( expense_cat, RetirementPlan.ExpenseCategory.choices())
class Migration(migrations.Migration): dependencies = [ ('reversion', '0002_auto_20141216_1509'), ('kpi', '0014_discoverable_subscribable_collections'), ] operations = [ migrations.CreateModel( name='AssetVersion', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('uid', kpi.fields.KpiUidField(uid_prefix=b'v')), ('name', models.CharField(max_length=255, null=True)), ('date_modified', models.DateTimeField(default=timezone.now)), ('version_content', JSONBField()), ('deployed_content', JSONBField(null=True)), ('_deployment_data', JSONBField(default=False)), ('deployed', models.BooleanField(default=False)), ('_reversion_version', models.OneToOneField(null=True, on_delete=models.SET_NULL, to='reversion.Version')), ('asset', models.ForeignKey(related_name='asset_versions', to='kpi.Asset', on_delete=models.CASCADE)), ], options={ 'ordering': ['-date_modified'], }, ), migrations.AlterField( model_name='asset', name='summary', field=JSONField(default=dict, null=True), ), migrations.AddField( model_name='asset', name='report_styles', field=JSONBField(default=dict), ), migrations.RenameField( model_name='assetsnapshot', old_name='asset_version_id', new_name='_reversion_version_id', ), migrations.AddField( model_name='assetsnapshot', name='asset_version', field=models.OneToOneField(null=True, on_delete=models.CASCADE, to='kpi.AssetVersion'), ), migrations.RunPython( copy_reversion_to_assetversion, noop, ), ]
class CreditRequirementStatus(TimeStampedModel): """ This model represents the status of each requirement. For a particular credit requirement, a user can either: 1) Have satisfied the requirement (example: approved in-course reverification) 2) Have failed the requirement (example: denied in-course reverification) 3) Neither satisfied nor failed (example: the user hasn't yet attempted in-course reverification). Cases (1) and (2) are represented by having a CreditRequirementStatus with the status set to "satisfied" or "failed", respectively. In case (3), no CreditRequirementStatus record will exist for the requirement and user. """ REQUIREMENT_STATUS_CHOICES = ( ("satisfied", "satisfied"), ("failed", "failed"), ) username = models.CharField(max_length=255, db_index=True) requirement = models.ForeignKey(CreditRequirement, related_name="statuses") status = models.CharField(max_length=32, choices=REQUIREMENT_STATUS_CHOICES) # Include additional information about why the user satisfied or failed # the requirement. This is specific to the type of requirement. # For example, the minimum grade requirement might record the user's # final grade when the user completes the course. This allows us to display # the grade to users later and to send the information to credit providers. reason = JSONField(default={}) # Maintain a history of requirement status updates for auditing purposes history = HistoricalRecords() class Meta(object): # pylint: disable=missing-docstring unique_together = ('username', 'requirement') @classmethod def get_statuses(cls, requirements, username): """ Get credit requirement statuses of given requirement and username Args: requirement(CreditRequirement): The identifier for a requirement username(str): username of the user Returns: Queryset 'CreditRequirementStatus' objects """ return cls.objects.filter(requirement__in=requirements, username=username) @classmethod @transaction.commit_on_success def add_or_update_requirement_status(cls, username, requirement, status="satisfied", reason=None): """ Add credit requirement status for given username. Args: username(str): Username of the user requirement(CreditRequirement): 'CreditRequirement' object status(str): Status of the requirement reason(dict): Reason of the status """ requirement_status, created = cls.objects.get_or_create( username=username, requirement=requirement, defaults={ "reason": reason, "status": status }) if not created: requirement_status.status = status requirement_status.reason = reason if reason else {} requirement_status.save()
def test_formfield_null_and_blank_clean_blank(self): field = JSONField("test", null=True, blank=True) formfield = field.formfield() self.assertEquals(formfield.clean(value=''), '')
class CreditRequest(TimeStampedModel): """ A request for credit from a particular credit provider. When a user initiates a request for credit, a CreditRequest record will be created. Each CreditRequest is assigned a unique identifier so we can find it when the request is approved by the provider. The CreditRequest record stores the parameters to be sent at the time the request is made. If the user re-issues the request (perhaps because the user did not finish filling in forms on the credit provider's site), the request record will be updated, but the UUID will remain the same. """ uuid = models.CharField(max_length=32, unique=True, db_index=True) username = models.CharField(max_length=255, db_index=True) course = models.ForeignKey(CreditCourse, related_name="credit_requests") provider = models.ForeignKey(CreditProvider, related_name="credit_requests") parameters = JSONField() REQUEST_STATUS_PENDING = "pending" REQUEST_STATUS_APPROVED = "approved" REQUEST_STATUS_REJECTED = "rejected" REQUEST_STATUS_CHOICES = ( (REQUEST_STATUS_PENDING, "Pending"), (REQUEST_STATUS_APPROVED, "Approved"), (REQUEST_STATUS_REJECTED, "Rejected"), ) status = models.CharField(max_length=255, choices=REQUEST_STATUS_CHOICES, default=REQUEST_STATUS_PENDING) history = HistoricalRecords() class Meta(object): # pylint: disable=missing-docstring # Enforce the constraint that each user can have exactly one outstanding # request to a given provider. Multiple requests use the same UUID. unique_together = ('username', 'course', 'provider') get_latest_by = 'created' @classmethod def credit_requests_for_user(cls, username): """ Retrieve all credit requests for a user. Arguments: username (unicode): The username of the user. Returns: list Example Usage: >>> CreditRequest.credit_requests_for_user("bob") [ { "uuid": "557168d0f7664fe59097106c67c3f847", "timestamp": 1434631630, "course_key": "course-v1:HogwartsX+Potions101+1T2015", "provider": { "id": "HogwartsX", "display_name": "Hogwarts School of Witchcraft and Wizardry", }, "status": "pending" # or "approved" or "rejected" } ] """ return [{ "uuid": request.uuid, "timestamp": request.parameters.get("timestamp"), "course_key": request.course.course_key, "provider": { "id": request.provider.provider_id, "display_name": request.provider.display_name }, "status": request.status } for request in cls.objects.select_related( 'course', 'provider').filter(username=username)] @classmethod def get_user_request_status(cls, username, course_key): """ Returns the latest credit request of user against the given course. Args: username(str): The username of requesting user course_key(CourseKey): The course identifier Returns: CreditRequest if any otherwise None """ try: return cls.objects.filter( username=username, course__course_key=course_key).select_related( 'course', 'provider').latest() except cls.DoesNotExist: return None def __unicode__(self): """Unicode representation of a credit request.""" return u"{course}, {provider}, {status}".format( course=self.course.course_key, provider=self.provider.provider_id, # pylint: disable=no-member status=self.status, )
class LogEntry(models.Model): """ Represents an entry in the audit log. The content type is saved along with the textual and numeric (if available) primary key, as well as the textual representation of the object when it was saved. It holds the action performed and the fields that were changed in the transaction. If AuditlogMiddleware is used, the actor will be set automatically. Keep in mind that editing / re-saving LogEntry instances may set the actor to a wrong value - editing LogEntry instances is not recommended (and it should not be necessary). """ class Action: """ The actions that Auditlog distinguishes: creating, updating and deleting objects. Viewing objects is not logged. The values of the actions are numeric, a higher integer value means a more intrusive action. This may be useful in some cases when comparing actions because the ``__lt``, ``__lte``, ``__gt``, ``__gte`` lookup filters can be used in queries. The valid actions are :py:attr:`Action.CREATE`, :py:attr:`Action.UPDATE` and :py:attr:`Action.DELETE`. """ CREATE = 0 UPDATE = 1 DELETE = 2 choices = ( (CREATE, _("create")), (UPDATE, _("update")), (DELETE, _("delete")), ) content_type = models.ForeignKey('contenttypes.ContentType', on_delete=models.CASCADE, related_name='+', verbose_name=_("content type")) object_pk = models.CharField(db_index=True, max_length=255, verbose_name=_("object pk")) object_id = models.BigIntegerField(blank=True, db_index=True, null=True, verbose_name=_("object id")) object_repr = models.TextField(verbose_name=_("object representation")) action = models.PositiveSmallIntegerField(choices=Action.choices, verbose_name=_("action")) changes = models.TextField(blank=True, verbose_name=_("change message")) actor = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_("actor")) remote_addr = models.GenericIPAddressField( blank=True, null=True, verbose_name=_("remote address")) timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("timestamp")) additional_data = JSONField(blank=True, null=True, verbose_name=_("additional data")) objects = LogEntryManager() class Meta: get_latest_by = 'timestamp' ordering = ['-timestamp'] verbose_name = _("log entry") verbose_name_plural = _("log entries") def __str__(self): if self.action == self.Action.CREATE: fstring = _("Created {repr:s}") elif self.action == self.Action.UPDATE: fstring = _("Updated {repr:s}") elif self.action == self.Action.DELETE: fstring = _("Deleted {repr:s}") else: fstring = _("Logged {repr:s}") return fstring.format(repr=self.object_repr) @property def changes_dict(self): """ :return: The changes recorded in this log entry as a dictionary object. """ try: return json.loads(self.changes) except ValueError: return {} @property def changes_str(self, colon=': ', arrow=smart_text(' \u2192 '), separator='; '): """ Return the changes recorded in this log entry as a string. The formatting of the string can be customized by setting alternate values for colon, arrow and separator. If the formatting is still not satisfying, please use :py:func:`LogEntry.changes_dict` and format the string yourself. :param colon: The string to place between the field name and the values. :param arrow: The string to place between each old and new value. :param separator: The string to place between each field. :return: A readable string of the changes in this log entry. """ substrings = [] for field, values in iteritems(self.changes_dict): substring = smart_text( '{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}').format( field_name=field, colon=colon, old=values[0], arrow=arrow, new=values[1], ) substrings.append(substring) return separator.join(substrings)
class Event(StripeObject): kind = models.CharField(max_length=250) livemode = models.BooleanField() customer = models.ForeignKey("Customer", null=True) webhook_message = JSONField() validated_message = JSONField(null=True) valid = models.NullBooleanField(null=True) processed = models.BooleanField(default=False) @property def message(self): return self.validated_message def __unicode__(self): return "%s - %s" % (self.kind, self.stripe_id) def link_customer(self): cus_id = None customer_crud_events = [ "customer.created", "customer.updated", "customer.deleted" ] if self.kind in customer_crud_events: cus_id = self.message["data"]["object"]["id"] else: cus_id = self.message["data"]["object"].get("customer", None) if cus_id is not None: try: self.customer = Customer.objects.get(stripe_id=cus_id) self.save() except Customer.DoesNotExist: pass def validate(self): evt = stripe.Event.retrieve(self.stripe_id) self.validated_message = json.loads( json.dumps(evt.to_dict(), sort_keys=True, cls=stripe.StripeObjectEncoder)) if self.webhook_message["data"] == self.validated_message["data"]: self.valid = True else: self.valid = False self.save() def process(self): """ "account.updated", "account.application.deauthorized", "charge.succeeded", "charge.failed", "charge.refunded", "charge.dispute.created", "charge.dispute.updated", "chagne.dispute.closed", "customer.created", "customer.updated", "customer.deleted", "customer.subscription.created", "customer.subscription.updated", "customer.subscription.deleted", "customer.subscription.trial_will_end", "customer.discount.created", "customer.discount.updated", "customer.discount.deleted", "invoice.created", "invoice.updated", "invoice.payment_succeeded", "invoice.payment_failed", "invoiceitem.created", "invoiceitem.updated", "invoiceitem.deleted", "plan.created", "plan.updated", "plan.deleted", "coupon.created", "coupon.updated", "coupon.deleted", "transfer.created", "transfer.updated", "transfer.failed", "ping" """ if self.valid and not self.processed: try: if not self.kind.startswith("plan.") and \ not self.kind.startswith("transfer."): self.link_customer() if self.kind.startswith("invoice."): Invoice.handle_event(self) elif self.kind.startswith("charge."): if not self.customer: self.link_customer() self.customer.record_charge( self.message["data"]["object"]["id"]) elif self.kind.startswith("transfer."): Transfer.process_transfer(self, self.message["data"]["object"]) elif self.kind.startswith("customer.subscription."): if not self.customer: self.link_customer() if self.customer: self.customer.sync_current_subscription() elif self.kind == "customer.deleted": if not self.customer: self.link_customer() self.customer.purge() self.send_signal() self.processed = True self.save() except stripe.StripeError, e: EventProcessingException.log(data=e.http_body, exception=e, event=self) webhook_processing_error.send(sender=Event, data=e.http_body, exception=e)
class FieldDefinition(models.Model): """" Defines fields of an elastic model """ class Meta: unique_together = (('schema', 'name'), ) ordering = 'id', NUMBER = 'number' TEXT = 'text' ENUM = 'enum' DATE = 'date' TYPE_CHOICES = ( (NUMBER, 'Number'), (TEXT, 'Text'), (ENUM, 'Enum'), (DATE, 'Date'), ) schema = models.ForeignKey('Schema', related_name='field_definitions') type = models.CharField(choices=TYPE_CHOICES, max_length=10) name = models.CharField(max_length=NAME_MAX_LENGTH, validators=[ alphanumeric_or_under_validator, ]) label = models.CharField(max_length=NAME_MAX_LENGTH, blank=True) blank = models.BooleanField(default=False) choices = JSONField(blank=True, validators=[ list_of_strings_validator, ], default=list) def __str__(self): return self.name.title() def clean(self): if self.type == self.ENUM and not self.choices: raise ValidationError('Choices are required for Enum type') if self.type != self.ENUM and self.choices: raise ValidationError('Choices are allowed only for Enum type') TYPE_TO_DJANGO_CLASS_MAPPING = { NUMBER: 'IntegerField', TEXT: 'TextField', ENUM: 'CharField', DATE: 'DateField', } def get_hstore_definition(self): """ Return field definition compatibile with django_hstore.fields.DictionaryField """ definiton = { 'class': self.TYPE_TO_DJANGO_CLASS_MAPPING[self.type], 'name': self.name, 'kwargs': { 'blank': self.blank, } } if self.choices: definiton['kwargs']['choices'] = [[value, value] for value in self.choices] if self.blank: if self.type in (self.NUMBER, self.DATE): definiton['kwargs']['null'] = True return definiton def _adjust_names(self): self.name = self.name.lower() if not self.label: self.label = self.name.title() def save(self, force_insert=False, force_update=False, using=None, update_fields=None): self._adjust_names() self.schema.instances.all().delete() super().save(force_insert, force_update, using, update_fields)
class Script(models.Model): shop = models.ForeignKey("wshop.Shop", verbose_name=_("shop")) event_identifier = models.CharField(max_length=64, blank=False, db_index=True, verbose_name=_('event identifier')) identifier = InternalIdentifierField(unique=True) created_on = models.DateTimeField(auto_now_add=True, editable=False, verbose_name=_('created on')) name = models.CharField(max_length=64, verbose_name=_('name')) enabled = models.BooleanField(default=False, db_index=True, verbose_name=_('enabled')) _step_data = JSONField(default=[], db_column="step_data") template = models.CharField( max_length=64, blank=True, null=True, default=None, verbose_name=_('template identifier'), help_text=_('the template identifier used to create this script')) def get_steps(self): """ :rtype Iterable[Step] """ if getattr(self, "_steps", None) is None: from wshop.notify.script import Step self._steps = [Step.unserialize(data) for data in self._step_data] return self._steps def set_steps(self, steps): self._step_data = [step.serialize() for step in steps] self._steps = steps def get_serialized_steps(self): return [step.serialize() for step in self.get_steps()] def set_serialized_steps(self, serialized_data): self._steps = None self._step_data = serialized_data # Poor man's validation for step in self.get_steps(): pass @property def event_class(self): return Event.class_for_identifier(self.event_identifier) def __str__(self): return self.name def execute(self, context): """ Execute the script in the given context. :param context: Script context :type context: wshop.notify.script.Context """ for step in self.get_steps(): if step.execute(context) == StepNext.STOP: break
class UserRequisit(StateSavingModel): alias = models.CharField( _("alias"), max_length=255, default="", blank=True, help_text=_( 'Give these payment details a name, for example "Forex wallet". ' 'It will help you find them in forms more easily')) purse = models.CharField(_("purse"), max_length=255, db_index=True, blank=True) user = models.ForeignKey(User, related_name="requisits", verbose_name=_("user"), blank=True) payment_system = PaymentSystemField(_("payment system")) is_valid = models.NullBooleanField(_("is the data valid?"), blank=True, null=True, default=False) is_deleted = models.BooleanField( _('Is deleted'), help_text=_('Users do not see deleted requisits'), default=False) comment = models.TextField( _("manager comment"), blank=True, null=True, help_text=_("If you reject a requisit, leave a comment here")) params = JSONField(_("details"), blank=True, null=True) creation_ts = models.DateTimeField(_("created at"), auto_now_add=True) previous = models.ForeignKey( 'self', blank=True, null=True, verbose_name=_("previous version of requisit")) objects = RequisitManager() class Meta: verbose_name = _("user requisit") verbose_name_plural = _("user requisits") ordering = ["payment_system", "-creation_ts"] unique_together = ("purse", "payment_system", "user") def __unicode__(self): return "%s" % (self.purse if self.alias == "" else self.alias) def get_params(self, flat=False): """Определяет порядок полей при выводе в списке реквизитов и выдает правильные названия полей""" from payments.models import REASONS_TO_WITHDRAWAL order = { "bankrur": [("bank_account", _("Bank account")), ("name", _("Name")), ("tin", _("TIN")), ("bank", _("Bank")), ("credit_card_number", _("Credit card number")), ("correspondent", _("Correspondent")), ("bic", _("BIC")), ("payment_details", _("Payment details"))], "bankeur": [("bank_account", _("Bank account")), ("name", _("Sender")), ("country", _("Country")), ("address", _("Address")), ("bank", _("Bank")), ("bank_swift", _("Bank's SWIFT code")), ("correspondent", _("Correspondent"))], "bankusd": [("bank_account", _("Bank account")), ("name", _("Sender")), ("country", _("Country")), ("address", _("Address")), ("bank", _("Bank")), ("bank_swift", _("Bank's SWIFT code")), ("correspondent", _("Correspondent"))], "bankuah": [("bank_account", _("Bank account")), ("name", _("Name")), ("tin", _("TIN")), ("bank", _("Bank")), ("credit_card_number", _("Credit card number")), ("correspondent", _("Correspondent")), ("bic", _("BIC")), ("payment_details", _("Payment details"))], "default": [] } d = SortedDict() system = self.payment_system if not system: return None if isinstance(system, basestring): system = load_payment_system(self.payment_system) if system.slug in order: key = system.slug else: key = "default" val = namedtuple("NiceKey", ("key", "value")) if self.params: for real_key, display_key in order[key]: if self.params.get(real_key) in ["", None]: # пустое значение обозначается длинным дефисом mdash # mark_safe нужен для корректного отображения — d[display_key] = val(key=real_key, value=mark_safe("—")) else: if real_key == "correspondent" and system.slug in [ "bankeur", "bankusd" ]: currency = system.currency if currency in settings.BANK_ACCOUNTS: d[display_key] = val( key=real_key, value=settings.BANK_ACCOUNTS[currency][int( self.params[real_key])][0]) elif real_key == "country": d[display_key] = val(key=real_key, value=get_country( self.params[real_key])) elif real_key == "reason": d[display_key] = val( key=real_key, value=REASONS_TO_WITHDRAWAL[self.params[real_key]]) else: d[display_key] = val(key=real_key, value=self.params[real_key]) if flat: d = SortedDict((key, res.value) for (key, res) in d.iteritems()) return d
def test_indent(self): JSONField('test', indent=2)
class Notification(models.Model): """ A model for persistent notifications to be shown in the admin, etc. """ recipient_type = EnumIntegerField(RecipientType, default=RecipientType.ADMINS) recipient = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name="+") created_on = models.DateTimeField(auto_now_add=True, editable=False) message = models.CharField(max_length=140, editable=False, default="") identifier = InternalIdentifierField(unique=False) priority = EnumIntegerField(Priority, default=Priority.NORMAL, db_index=True) _data = JSONField(blank=True, null=True, editable=False, db_column="data") marked_read = models.BooleanField(db_index=True, editable=False, default=False) marked_read_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, editable=False, related_name="+") marked_read_on = models.DateTimeField(null=True, blank=True) objects = NotificationManager() def __init__(self, *args, **kwargs): url = kwargs.pop("url", None) super(Notification, self).__init__(*args, **kwargs) if url: self.url = url def save(self, *args, **kwargs): if self.recipient_type == RecipientType.SPECIFIC_USER and not self.recipient_id: raise ValueError( "With RecipientType.SPECIFIC_USER, recipient is required") super(Notification, self).save(*args, **kwargs) def mark_read(self, user): if self.marked_read: return False self.marked_read = True self.marked_read_by = user self.marked_read_on = now() self.save(update_fields=('marked_read', 'marked_read_by', 'marked_read_on')) return True @property def is_read(self): return self.marked_read @property def data(self): if not self._data: self._data = {} return self._data @property def url(self): url = self.data.get("_url") if isinstance(url, dict): return reverse(**url) return url @url.setter def url(self, value): if self.pk: raise ValueError("URL can't be set on a saved notification") self.data["_url"] = value def set_reverse_url(self, **reverse_kwargs): if self.pk: raise ValueError("URL can't be set on a saved notification") try: reverse(**reverse_kwargs) except NoReverseMatch: # pragma: no cover raise ValueError("Invalid reverse URL parameters") self.data["_url"] = reverse_kwargs
class InvalidEncoderFieldTestModel(models.Model): json = JSONField( encoder_class='jsonfield.encoder.UnknownJSONEncoder')
class User(AbstractUser): """ Custom user model for use with python-social-auth via edx-auth-backends. """ # This preserves the 30 character limit on last_name, avoiding a large migration # on the ecommerce_user table that would otherwise have come with Django 2. # See https://docs.djangoproject.com/en/3.0/releases/2.0/#abstractuser-last-name-max-length-increased-to-150 last_name = models.CharField(_('last name'), max_length=30, blank=True) full_name = models.CharField(_('Full Name'), max_length=255, blank=True, null=True) tracking_context = JSONField(blank=True, null=True) email = models.EmailField(max_length=254, verbose_name='email address', blank=True, db_index=True) lms_user_id = models.IntegerField( null=True, blank=True, help_text=_(u'LMS user id'), ) class Meta: get_latest_by = 'date_joined' db_table = 'ecommerce_user' @property def access_token(self): """ Returns the access token from the extra data in the user's social auth. Note that a single user_id can be associated with multiple provider/uid combinations. For example: provider uid user_id edx-oidc person 123 edx-oauth2 person 123 edx-oauth2 [email protected] 123 """ try: return self.social_auth.order_by('-id').first().extra_data[ u'access_token'] # pylint: disable=no-member except Exception: # pylint: disable=broad-except return None def lms_user_id_with_metric(self, usage=None, allow_missing=False): """ Returns the LMS user_id, or None if not found. Also sets a metric with the result. Arguments: usage (string): Optional. A description of how the returned id will be used. This will be included in log messages if the LMS user id cannot be found. allow_missing (boolean): True if the LMS user id is allowed to be missing. This affects the log messages and custom metrics. Defaults to False. Side effect: Writes custom metric. """ # Read the lms_user_id from the ecommerce_user. lms_user_id = self.lms_user_id if lms_user_id: monitoring_utils.set_custom_metric('ecommerce_found_lms_user_id', lms_user_id) return lms_user_id # Could not find the lms_user_id if allow_missing: monitoring_utils.set_custom_metric( 'ecommerce_missing_lms_user_id_allowed', self.id) log.info( u'Could not find lms_user_id with metric for user %s for %s. Missing lms_user_id is allowed.', self.id, usage, exc_info=True) else: monitoring_utils.set_custom_metric('ecommerce_missing_lms_user_id', self.id) log.warning( u'Could not find lms_user_id with metric for user %s for %s.', self.id, usage, exc_info=True) return None def add_lms_user_id(self, missing_metric_key, called_from, allow_missing=False): """ If this user does not already have an LMS user id, look for the id in social auth. If the id can be found, add it to the user and save the user. The LMS user_id may already be present for the user. It may have been added from the jwt (see the EDX_DRF_EXTENSIONS.JWT_PAYLOAD_USER_ATTRIBUTE_MAPPING settings) or by a previous call to this method. Arguments: missing_metric_key (String): Key name for metric that will be created if the LMS user id cannot be found. called_from (String): Descriptive string describing the caller. This will be included in log messages. allow_missing (boolean): True if the LMS user id is allowed to be missing. This affects the log messages, custom metrics, and (in combination with the allow_missing_lms_user_id switch), whether an MissingLmsUserIdException is raised. Defaults to False. Side effect: If the LMS id cannot be found, writes custom metrics. """ if not self.lms_user_id: # Check for the LMS user id in social auth lms_user_id_social_auth, social_auth_id = self._get_lms_user_id_from_social_auth( ) if lms_user_id_social_auth: self.lms_user_id = lms_user_id_social_auth self.save() log.info( u'Saving lms_user_id from social auth with id %s for user %s. Called from %s', social_auth_id, self.id, called_from) else: # Could not find the LMS user id if allow_missing or waffle.switch_is_active( ALLOW_MISSING_LMS_USER_ID): monitoring_utils.set_custom_metric( 'ecommerce_missing_lms_user_id_allowed', self.id) monitoring_utils.set_custom_metric( missing_metric_key + '_allowed', self.id) error_msg = ( u'Could not find lms_user_id for user {user_id}. Missing lms_user_id is allowed. ' u'Called from {called_from}'.format( user_id=self.id, called_from=called_from)) log.info(error_msg, exc_info=True) else: monitoring_utils.set_custom_metric( 'ecommerce_missing_lms_user_id', self.id) monitoring_utils.set_custom_metric(missing_metric_key, self.id) error_msg = u'Could not find lms_user_id for user {user_id}. Called from {called_from}'.format( user_id=self.id, called_from=called_from) log.error(error_msg, exc_info=True) raise MissingLmsUserIdException(error_msg) def _get_lms_user_id_from_social_auth(self): """ Find the LMS user_id passed through social auth. Because a single user_id can be associated with multiple provider/uid combinations, start by checking the most recently saved social auth entry. Returns: (lms_user_id, social_auth_id): a tuple containing the LMS user id and the id of the social auth entry where the LMS user id was found. Returns None, None if the LMS user id was not found. """ try: auth_entries = self.social_auth.order_by('-id') if auth_entries: for auth_entry in auth_entries: lms_user_id_social_auth = auth_entry.extra_data.get( u'user_id') if lms_user_id_social_auth: return lms_user_id_social_auth, auth_entry.id except Exception: # pylint: disable=broad-except log.warning( u'Exception retrieving lms_user_id from social_auth for user %s.', self.id, exc_info=True) return None, None def get_full_name(self): return self.full_name or super(User, self).get_full_name() def account_details(self, request): """ Returns the account details from LMS. Args: request (WSGIRequest): The request from which the LMS account API endpoint is created. Returns: A dictionary of account details. Raises: ConnectionError, SlumberBaseException and Timeout for failures in establishing a connection with the LMS account API endpoint. """ try: api = EdxRestApiClient( request.site.siteconfiguration.build_lms_url('/api/user/v1'), append_slash=False, jwt=request.site.siteconfiguration.access_token) response = api.accounts(self.username).get() return response except (ReqConnectionError, SlumberBaseException, Timeout): log.exception('Failed to retrieve account details for [%s]', self.username) raise def is_eligible_for_credit(self, course_key, site_configuration): """ Check if a user is eligible for a credit course. Calls the LMS eligibility API endpoint and sends the username and course key query parameters and returns eligibility details for the user and course combination. Args: course_key (string): The course key for which the eligibility is checked for. Returns: A list that contains eligibility information, or empty if user is not eligible. Raises: ConnectionError, SlumberBaseException and Timeout for failures in establishing a connection with the LMS eligibility API endpoint. """ query_strings = {'username': self.username, 'course_key': course_key} try: api = site_configuration.credit_api_client response = api.eligibility().get(**query_strings) except (ReqConnectionError, SlumberBaseException, Timeout): # pragma: no cover log.exception( 'Failed to retrieve eligibility details for [%s] in course [%s]', self.username, course_key) raise return response def is_verified(self, site): """ Check if a user has verified his/her identity. Calls the LMS verification status API endpoint and returns the verification status information. The status information is stored in cache, if the user is verified, until the verification expires. Args: site (Site): The site object from which the LMS account API endpoint is created. Returns: True if the user is verified, false otherwise. """ try: cache_key = 'verification_status_{username}'.format( username=self.username) cache_key = hashlib.md5(cache_key.encode('utf-8')).hexdigest() verification_cached_response = TieredCache.get_cached_response( cache_key) if verification_cached_response.is_found: return verification_cached_response.value api = site.siteconfiguration.user_api_client response = api.accounts(self.username).verification_status().get() verification = response.get('is_verified', False) if verification: cache_timeout = int( (parse(response.get('expiration_datetime')) - now()).total_seconds()) TieredCache.set_all_tiers(cache_key, verification, cache_timeout) return verification except HttpNotFoundError: log.debug('No verification data found for [%s]', self.username) return False except (ReqConnectionError, SlumberBaseException, Timeout): msg = 'Failed to retrieve verification status details for [{username}]'.format( username=self.username) log.warning(msg) return False def deactivate_account(self, site_configuration): """Deactivate the user's account. Args: site_configuration (SiteConfiguration): The site configuration from which the LMS account API endpoint is created. Returns: Response from the deactivation API endpoint. """ try: api = site_configuration.user_api_client return api.accounts(self.username).deactivate().post() except: # pylint: disable=bare-except log.exception('Failed to deactivate account for user [%s]', self.username) raise
def test_formfield_null_and_blank_clean_none(self): field = JSONField("test", null=True, blank=True) formfield = field.formfield() self.assertEqual(formfield.clean(value=None), None)
class LogEntry(models.Model): """ Represents an entry in the audit log. The content type is saved along with the textual and numeric (if available) primary key, as well as the textual representation of the object when it was saved. It holds the action performed and the fields that were changed in the transaction. If AuditlogMiddleware is used, the actor will be set automatically. Keep in mind that editing / re-saving LogEntry instances may set the actor to a wrong value - editing LogEntry instances is not recommended (and it should not be necessary). """ class Action: """ The actions that Auditlog distinguishes: creating, updating and deleting objects. Viewing objects is not logged. The values of the actions are numeric, a higher integer value means a more intrusive action. This may be useful in some cases when comparing actions because the ``__lt``, ``__lte``, ``__gt``, ``__gte`` lookup filters can be used in queries. The valid actions are :py:attr:`Action.CREATE`, :py:attr:`Action.UPDATE` and :py:attr:`Action.DELETE`. """ CREATE = 0 UPDATE = 1 DELETE = 2 choices = ( (CREATE, _("create")), (UPDATE, _("update")), (DELETE, _("delete")), ) content_type = models.ForeignKey( to="contenttypes.ContentType", on_delete=models.CASCADE, related_name="+", verbose_name=_("content type"), ) object_pk = models.CharField( db_index=True, max_length=255, verbose_name=_("object pk") ) object_id = models.BigIntegerField( blank=True, db_index=True, null=True, verbose_name=_("object id") ) object_repr = models.TextField(verbose_name=_("object representation")) action = models.PositiveSmallIntegerField( choices=Action.choices, verbose_name=_("action") ) changes = models.TextField(blank=True, verbose_name=_("change message")) actor = models.ForeignKey( to=settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, blank=True, null=True, related_name="+", verbose_name=_("actor"), ) remote_addr = models.GenericIPAddressField( blank=True, null=True, verbose_name=_("remote address") ) timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("timestamp")) additional_data = JSONField( blank=True, null=True, verbose_name=_("additional data") ) objects = LogEntryManager() class Meta: get_latest_by = "timestamp" ordering = ["-timestamp"] verbose_name = _("log entry") verbose_name_plural = _("log entries") def __str__(self): if self.action == self.Action.CREATE: fstring = _("Created {repr:s}") elif self.action == self.Action.UPDATE: fstring = _("Updated {repr:s}") elif self.action == self.Action.DELETE: fstring = _("Deleted {repr:s}") else: fstring = _("Logged {repr:s}") return fstring.format(repr=self.object_repr) @property def changes_dict(self): """ :return: The changes recorded in this log entry as a dictionary object. """ try: return json.loads(self.changes) except ValueError: return {} @property def changes_str(self, colon=": ", arrow=" \u2192 ", separator="; "): """ Return the changes recorded in this log entry as a string. The formatting of the string can be customized by setting alternate values for colon, arrow and separator. If the formatting is still not satisfying, please use :py:func:`LogEntry.changes_dict` and format the string yourself. :param colon: The string to place between the field name and the values. :param arrow: The string to place between each old and new value. :param separator: The string to place between each field. :return: A readable string of the changes in this log entry. """ substrings = [] for field, values in self.changes_dict.items(): substring = "{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}".format( field_name=field, colon=colon, old=values[0], arrow=arrow, new=values[1], ) substrings.append(substring) return separator.join(substrings) @property def changes_display_dict(self): """ :return: The changes recorded in this log entry intended for display to users as a dictionary object. """ # Get the model and model_fields from auditlog.registry import auditlog model = self.content_type.model_class() model_fields = auditlog.get_model_fields(model._meta.model) changes_display_dict = {} # grab the changes_dict and iterate through for field_name, values in self.changes_dict.items(): # try to get the field attribute on the model try: field = model._meta.get_field(field_name) except FieldDoesNotExist: changes_display_dict[field_name] = values continue values_display = [] # handle choices fields and Postgres ArrayField to get human readable version choices_dict = None if getattr(field, "choices") and len(field.choices) > 0: choices_dict = dict(field.choices) if ( hasattr(field, "base_field") and isinstance(field.base_field, Field) and getattr(field.base_field, "choices") and len(field.base_field.choices) > 0 ): choices_dict = dict(field.base_field.choices) if choices_dict: for value in values: try: value = ast.literal_eval(value) if type(value) is [].__class__: values_display.append( ", ".join( [choices_dict.get(val, "None") for val in value] ) ) else: values_display.append(choices_dict.get(value, "None")) except ValueError: values_display.append(choices_dict.get(value, "None")) except: values_display.append(choices_dict.get(value, "None")) else: try: field_type = field.get_internal_type() except AttributeError: # if the field is a relationship it has no internal type and exclude it continue for value in values: # handle case where field is a datetime, date, or time type if field_type in ["DateTimeField", "DateField", "TimeField"]: try: value = parser.parse(value) if field_type == "DateField": value = value.date() elif field_type == "TimeField": value = value.time() elif field_type == "DateTimeField": value = value.replace(tzinfo=timezone.utc) value = value.astimezone(gettz(settings.TIME_ZONE)) value = formats.localize(value) except ValueError: pass # check if length is longer than 140 and truncate with ellipsis if len(value) > 140: value = "{}...".format(value[:140]) values_display.append(value) verbose_name = model_fields["mapping_fields"].get( field.name, getattr(field, "verbose_name", field.name) ) changes_display_dict[verbose_name] = values_display return changes_display_dict
class SiteConfiguration(models.Model): """Tenant configuration. Each site/tenant should have an instance of this model. This model is responsible for providing databased-backed configuration specific to each site. """ site = models.OneToOneField('sites.Site', null=False, blank=False, on_delete=models.CASCADE) partner = models.ForeignKey('partner.Partner', null=False, blank=False, on_delete=models.CASCADE) lms_url_root = models.URLField( verbose_name=_('LMS base url for custom site/microsite'), help_text=_("Root URL of this site's LMS (e.g. https://courses.stage.edx.org)"), null=False, blank=False ) theme_scss_path = models.CharField( verbose_name=_('Path to custom site theme'), help_text='DEPRECATED: THIS FIELD WILL BE REMOVED!', max_length=255, null=True, blank=True ) payment_processors = models.CharField( verbose_name=_('Payment processors'), help_text=_("Comma-separated list of processor names: 'cybersource,paypal'"), max_length=255, null=False, blank=False ) client_side_payment_processor = models.CharField( verbose_name=_('Client-side payment processor'), help_text=_('Processor that will be used for client-side payments'), max_length=255, null=True, blank=True ) oauth_settings = JSONField( verbose_name=_('OAuth settings'), help_text=_('JSON string containing OAuth backend settings.'), null=False, blank=False, default={} ) segment_key = models.CharField( verbose_name=_('Segment key'), help_text=_('Segment write/API key.'), max_length=255, null=True, blank=True ) from_email = models.CharField( verbose_name=_('From email'), help_text=_('Address from which emails are sent.'), max_length=255, null=True, blank=True ) enable_enrollment_codes = models.BooleanField( verbose_name=_('Enable enrollment codes'), help_text=_('Enable the creation of enrollment codes.'), blank=True, default=False ) payment_support_email = models.CharField( verbose_name=_('Payment support email'), help_text=_('Contact email for payment support issues.'), max_length=255, blank=True, default="*****@*****.**" ) payment_support_url = models.CharField( verbose_name=_('Payment support url'), help_text=_('URL for payment support issues.'), max_length=255, blank=True ) utm_cookie_name = models.CharField( verbose_name=_('UTM Cookie Name'), help_text=_('Name of cookie storing UTM data.'), max_length=255, blank=True, default="", ) affiliate_cookie_name = models.CharField( verbose_name=_('Affiliate Cookie Name'), help_text=_('Name of cookie storing affiliate data.'), max_length=255, blank=True, default="", ) send_refund_notifications = models.BooleanField( verbose_name=_('Send refund email notification'), blank=True, default=False ) enable_sdn_check = models.BooleanField( verbose_name=_('Enable SDN check'), help_text=_('Enable SDN check at checkout.'), default=False ) sdn_api_url = models.CharField( verbose_name=_('US Treasury SDN API URL'), max_length=255, blank=True ) sdn_api_key = models.CharField( verbose_name=_('US Treasury SDN API key'), max_length=255, blank=True ) sdn_api_list = models.CharField( verbose_name=_('SDN lists'), help_text=_('A comma-separated list of Treasury OFAC lists to check against.'), max_length=255, blank=True ) require_account_activation = models.BooleanField( verbose_name=_('Require Account Activation'), help_text=_('Require users to activate their account before allowing them to redeem a coupon.'), default=True ) optimizely_snippet_src = models.CharField( verbose_name=_('Optimizely snippet source URL'), help_text=_('This script will be loaded on every page.'), max_length=255, blank=True ) enable_sailthru = models.BooleanField( verbose_name=_('Enable Sailthru Reporting'), help_text=_('Determines if purchases should be reported to Sailthru.'), default=False ) base_cookie_domain = models.CharField( verbose_name=_('Base Cookie Domain'), help_text=_('Base cookie domain used to share cookies across services.'), max_length=255, blank=True, default='', ) enable_embargo_check = models.BooleanField( verbose_name=_('Enable embargo check'), help_text=_('Enable embargo check at checkout.'), default=False ) discovery_api_url = models.URLField( verbose_name=_('Discovery API URL'), null=False, blank=False, ) # TODO: journals dependency journals_api_url = models.URLField( verbose_name=_('Journals Service API URL'), null=True, blank=True ) enable_apple_pay = models.BooleanField( # Translators: Do not translate "Apple Pay" verbose_name=_('Enable Apple Pay'), default=False ) enable_partial_program = models.BooleanField( verbose_name=_('Enable Partial Program Offer'), help_text=_('Enable the application of program offers to remaining unenrolled or unverified courses'), blank=True, default=False ) hubspot_secret_key = models.CharField( verbose_name=_('Hubspot Portal Secret Key'), help_text=_('Secret key for Hubspot portal authentication'), max_length=255, blank=True ) @property def payment_processors_set(self): """ Returns a set of enabled payment processor keys Returns: set[string]: Returns a set of enabled payment processor keys """ return {raw_processor_value.strip() for raw_processor_value in self.payment_processors.split(',')} def _clean_payment_processors(self): """ Validates payment_processors field value Raises: ValidationError: If `payment_processors` field contains invalid/unknown payment_processor names """ value = self.payment_processors.strip() if not value: raise ValidationError('Invalid payment processors field: must not consist only of whitespace characters') processor_names = value.split(',') for name in processor_names: try: get_processor_class_by_name(name.strip()) except ProcessorNotFoundError as exc: log.exception( "Exception validating site configuration for site `%s` - payment processor %s could not be found", self.site.id, name ) raise ValidationError(exc.message) def _clean_client_side_payment_processor(self): """ Validates the client_side_payment_processor field value. Raises: ValidationError: If the field contains the name of a payment processor NOT found in the payment_processors field list. """ value = (self.client_side_payment_processor or '').strip() if value and value not in self.payment_processors_set: raise ValidationError('Processor [{processor}] must be in the payment_processors field in order to ' 'be configured as a client-side processor.'.format(processor=value)) def _all_payment_processors(self): """ Returns all processor classes declared in settings. """ all_processors = [get_processor_class(path) for path in settings.PAYMENT_PROCESSORS] return all_processors def get_payment_processors(self): """ Returns payment processor classes enabled for the corresponding Site Returns: list[BasePaymentProcessor]: Returns payment processor classes enabled for the corresponding Site """ all_processors = self._all_payment_processors() all_processor_names = {processor.NAME for processor in all_processors} missing_processor_configurations = self.payment_processors_set - all_processor_names if missing_processor_configurations: processor_config_repr = ", ".join(missing_processor_configurations) log.warning( 'Unknown payment processors [%s] are configured for site %s', processor_config_repr, self.site.id ) return [ processor for processor in all_processors if processor.NAME in self.payment_processors_set and processor.is_enabled() ] def get_client_side_payment_processor_class(self): """ Returns the payment processor class to be used for client-side payments. If no processor is set, returns None. Returns: BasePaymentProcessor """ if self.client_side_payment_processor: for processor in self._all_payment_processors(): if processor.NAME == self.client_side_payment_processor: return processor return None def get_from_email(self): """ Returns the configured from_email value for the specified site. If no from_email is available we return the base OSCAR_FROM_EMAIL setting Returns: string: Returns sender address for use in customer emails/alerts """ return self.from_email or settings.OSCAR_FROM_EMAIL @cached_property def segment_client(self): return SegmentClient(self.segment_key, debug=settings.DEBUG, send=settings.SEND_SEGMENT_EVENTS) def save(self, *args, **kwargs): # Clear Site cache upon SiteConfiguration changed Site.objects.clear_cache() super(SiteConfiguration, self).save(*args, **kwargs) def build_ecommerce_url(self, path=''): """ Returns path joined with the appropriate ecommerce URL root for the current site. Returns: str """ scheme = 'http' if settings.DEBUG else 'https' ecommerce_url_root = "{scheme}://{domain}".format(scheme=scheme, domain=self.site.domain) return urljoin(ecommerce_url_root, path) def build_lms_url(self, path=''): """ Returns path joined with the appropriate LMS URL root for the current site. Returns: str """ return urljoin(self.lms_url_root, path) def build_enterprise_service_url(self, path=''): """ Returns path joined with the appropriate Enterprise service URL root for the current site. Returns: str """ return urljoin(settings.ENTERPRISE_SERVICE_URL, path) def build_program_dashboard_url(self, uuid): """ Returns a URL to a specific student program dashboard (hosted by LMS). """ return self.build_lms_url('/dashboard/programs/{}'.format(uuid)) @property def student_dashboard_url(self): """ Returns a URL to the student dashboard (hosted by LMS). """ return self.build_lms_url('/dashboard') @property def enrollment_api_url(self): """ Returns the URL for the root of the Enrollment API. """ return self.build_lms_url('/api/enrollment/v1/') @property def oauth2_provider_url(self): """ Returns the URL for the OAuth 2.0 provider. """ return self.build_lms_url('/oauth2') @property def enterprise_api_url(self): """ Returns the URL for the Enterprise service. """ return settings.ENTERPRISE_API_URL @property def enterprise_grant_data_sharing_url(self): """ Returns the URL for the Enterprise data sharing permission view. """ return self.build_enterprise_service_url('grant_data_sharing_permissions') @property def access_token(self): """ Returns an access token for this site's service user. The access token is retrieved using the current site's OAuth credentials and the client credentials grant. The token is cached for the lifetime of the token, as specified by the OAuth provider's response. The token type is JWT. Returns: str: JWT access token """ key = 'siteconfiguration_access_token_{}'.format(self.id) access_token_cached_response = TieredCache.get_cached_response(key) if access_token_cached_response.is_found: return access_token_cached_response.value url = '{root}/access_token'.format(root=self.oauth2_provider_url) access_token, expiration_datetime = EdxRestApiClient.get_oauth_access_token( url, self.oauth_settings['BACKEND_SERVICE_EDX_OAUTH2_KEY'], # pylint: disable=unsubscriptable-object self.oauth_settings['BACKEND_SERVICE_EDX_OAUTH2_SECRET'], # pylint: disable=unsubscriptable-object token_type='jwt' ) expires = (expiration_datetime - datetime.datetime.utcnow()).seconds TieredCache.set_all_tiers(key, access_token, expires) return access_token @cached_property def discovery_api_client(self): """ Returns an API client to access the Discovery service. Returns: EdxRestApiClient: The client to access the Discovery service. """ return EdxRestApiClient(self.discovery_api_url, jwt=self.access_token) # TODO: journals dependency @cached_property def journal_discovery_api_client(self): """ Returns an Journal API client to access the Discovery service. Returns: EdxRestApiClient: The client to access the Journal API in the Discovery service. """ split_url = urlsplit(self.discovery_api_url) journal_discovery_url = urlunsplit([ split_url.scheme, split_url.netloc, JOURNAL_DISCOVERY_API_PATH, split_url.query, split_url.fragment ]) return EdxRestApiClient(journal_discovery_url, jwt=self.access_token) @cached_property def embargo_api_client(self): """ Returns the URL for the embargo API """ return EdxRestApiClient(self.build_lms_url('/api/embargo/v1'), jwt=self.access_token) @cached_property def enterprise_api_client(self): """ Constructs a Slumber-based REST API client for the provided site. Example: site.siteconfiguration.enterprise_api_client.enterprise-learner(learner.username).get() Returns: EdxRestApiClient: The client to access the Enterprise service. """ return EdxRestApiClient(self.enterprise_api_url, jwt=self.access_token) @cached_property def consent_api_client(self): return EdxRestApiClient(self.build_lms_url('/consent/api/v1/'), jwt=self.access_token, append_slash=False) @cached_property def user_api_client(self): """ Returns the API client to access the user API endpoint on LMS. Returns: EdxRestApiClient: The client to access the LMS user API service. """ return EdxRestApiClient(self.build_lms_url('/api/user/v1/'), jwt=self.access_token) @cached_property def commerce_api_client(self): return EdxRestApiClient(self.build_lms_url('/api/commerce/v1/'), jwt=self.access_token) @cached_property def credit_api_client(self): return EdxRestApiClient(self.build_lms_url('/api/credit/v1/'), jwt=self.access_token) @cached_property def enrollment_api_client(self): return EdxRestApiClient(self.build_lms_url('/api/enrollment/v1/'), jwt=self.access_token, append_slash=False) @cached_property def entitlement_api_client(self): return EdxRestApiClient(self.build_lms_url('/api/entitlements/v1/'), jwt=self.access_token)
class User(AbstractUser): """ Custom user model for use with python-social-auth via edx-auth-backends. """ full_name = models.CharField(_('Full Name'), max_length=255, blank=True, null=True) tracking_context = JSONField(blank=True, null=True) class Meta(object): get_latest_by = 'date_joined' db_table = 'ecommerce_user' @property def access_token(self): try: return self.social_auth.first().extra_data[u'access_token'] # pylint: disable=no-member except Exception: # pylint: disable=broad-except return None @property def lms_user_id(self): """ Returns the LMS user_id, or None if not found. """ # JWT cookie is used with API calls from new microfrontends. This is not persisted. # TODO: Rename ``_get_lms_user_id_from_jwt_cookie`` to ``_get_lms_user_id_from_jwt`` # and update to use new method to be added to JwtAuthentication in edx-drf-extensions # to get the decoded JWT used for authentication, no matter where it came from. # See https://github.com/edx/edx-drf-extensions/pull/69#discussion_r286618922 lms_user_id = self._get_lms_user_id_from_jwt_cookie() if lms_user_id: return lms_user_id # This is persisted to the database during any new oAuth+SSO flow. lms_user_id = self._get_lms_user_id_from_social_auth() if lms_user_id: return lms_user_id # Server-to-server calls from LMS to ecommerce use a specially crafted JWT. lms_user_id = self._get_lms_user_id_from_tracking_context() if lms_user_id: return lms_user_id # If we get here, it means either: # 1. The user has an old social_auth session created before the LMS user_id was written to the database, or # 2. This could be a server-to-server call that isn't properly handled, or # 3. Some other unknown flow. monitoring_utils.set_custom_metric('ecommerce_user_missing_lms_user_id', self.id) return None def _get_lms_user_id_from_jwt_cookie(self): """ Return LMS user_id from JWT cookie, if found. Returns None if not found. Side effect: If found, writes custom metric: 'lms_user_id_jwt_cookie' """ request = crum.get_current_request() if not request: return None decoded_jwt = get_decoded_jwt_from_jwt_cookie(request) if not decoded_jwt: return None if 'user_id' in decoded_jwt: lms_user_id_in_jwt_cookie = decoded_jwt['user_id'] monitoring_utils.set_custom_metric('lms_user_id_jwt_cookie', lms_user_id_in_jwt_cookie) return lms_user_id_in_jwt_cookie def _get_lms_user_id_from_social_auth(self): """ Return LMS user_id passed through social auth, if found. Returns None if not found. Side effect: If found, writes custom metric: 'lms_user_id_social_auth' """ try: lms_user_id_social_auth = self.social_auth.first().extra_data[u'user_id'] # pylint: disable=no-member if lms_user_id_social_auth: monitoring_utils.set_custom_metric('lms_user_id_social_auth', lms_user_id_social_auth) return lms_user_id_social_auth else: # pragma: no cover pass # allows coverage skip for just this case. except Exception: # pylint: disable=broad-except pass def _get_lms_user_id_from_tracking_context(self): """ Return LMS user_id passed through tracking_context, if found. Returns None if not found. Side effect: If found, writes custom metric: 'lms_user_id_tracking_context' """ # Return lms_user_id passed through tracking_context, if found. tracking_context = self.tracking_context or {} lms_user_id_tracking_context = tracking_context.get('lms_user_id') if lms_user_id_tracking_context: monitoring_utils.set_custom_metric('lms_user_id_tracking_context', lms_user_id_tracking_context) return lms_user_id_tracking_context def get_full_name(self): return self.full_name or super(User, self).get_full_name() def account_details(self, request): """ Returns the account details from LMS. Args: request (WSGIRequest): The request from which the LMS account API endpoint is created. Returns: A dictionary of account details. Raises: ConnectionError, SlumberBaseException and Timeout for failures in establishing a connection with the LMS account API endpoint. """ try: api = EdxRestApiClient( request.site.siteconfiguration.build_lms_url('/api/user/v1'), append_slash=False, jwt=request.site.siteconfiguration.access_token ) response = api.accounts(self.username).get() return response except (ConnectionError, SlumberBaseException, Timeout): log.exception( 'Failed to retrieve account details for [%s]', self.username ) raise def is_eligible_for_credit(self, course_key, site_configuration): """ Check if a user is eligible for a credit course. Calls the LMS eligibility API endpoint and sends the username and course key query parameters and returns eligibility details for the user and course combination. Args: course_key (string): The course key for which the eligibility is checked for. Returns: A list that contains eligibility information, or empty if user is not eligible. Raises: ConnectionError, SlumberBaseException and Timeout for failures in establishing a connection with the LMS eligibility API endpoint. """ query_strings = { 'username': self.username, 'course_key': course_key } try: api = site_configuration.credit_api_client response = api.eligibility().get(**query_strings) except (ConnectionError, SlumberBaseException, Timeout): # pragma: no cover log.exception( 'Failed to retrieve eligibility details for [%s] in course [%s]', self.username, course_key ) raise return response def is_verified(self, site): """ Check if a user has verified his/her identity. Calls the LMS verification status API endpoint and returns the verification status information. The status information is stored in cache, if the user is verified, until the verification expires. Args: site (Site): The site object from which the LMS account API endpoint is created. Returns: True if the user is verified, false otherwise. """ try: cache_key = 'verification_status_{username}'.format(username=self.username) cache_key = hashlib.md5(cache_key).hexdigest() verification_cached_response = TieredCache.get_cached_response(cache_key) if verification_cached_response.is_found: return verification_cached_response.value api = site.siteconfiguration.user_api_client response = api.accounts(self.username).verification_status().get() verification = response.get('is_verified', False) if verification: cache_timeout = int((parse(response.get('expiration_datetime')) - now()).total_seconds()) TieredCache.set_all_tiers(cache_key, verification, cache_timeout) return verification except HttpNotFoundError: log.debug('No verification data found for [%s]', self.username) return False except (ConnectionError, SlumberBaseException, Timeout): msg = 'Failed to retrieve verification status details for [{username}]'.format(username=self.username) log.warning(msg) return False def deactivate_account(self, site_configuration): """Deactivate the user's account. Args: site_configuration (SiteConfiguration): The site configuration from which the LMS account API endpoint is created. Returns: Response from the deactivation API endpoint. """ try: api = site_configuration.user_api_client return api.accounts(self.username).deactivate().post() except: # pylint: disable=bare-except log.exception( 'Failed to deactivate account for user [%s]', self.username ) raise
def test_formfield_blank_clean_none(self): field = JSONField("test", null=False, blank=True) formfield = field.formfield() self.assertEquals(formfield.clean(value=None), '')
def mark_as_unread(self): if not self.unread: self.unread = True self.save() EXTRA_DATA = False if getattr(settings, 'NOTIFY_USE_JSONFIELD', False): try: from jsonfield.fields import JSONField except ImportError: raise ImproperlyConfigured( "You must have a suitable JSONField installed") JSONField(blank=True, null=True).contribute_to_class(Notification, 'data') EXTRA_DATA = True def notify_handler(verb, **kwargs): """ Handler function to create Notification instance upon action signal call. """ kwargs.pop('signal', None) recipient = kwargs.pop('recipient') actor = kwargs.pop('sender') newnotify = Notification( recipient=recipient, actor_content_type=ContentType.objects.get_for_model(actor), actor_object_id=actor.pk,