Пример #1
0
 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)
Пример #2
0
 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)
Пример #3
0
 def test_db_json_type(self):
     field = JSONField(db_json_type='bob')
     self.assertEqual(field.db_type(connection=None), 'bob')
Пример #4
0
 class InvalidModuleEncoderFieldTestModel(models.Model):
     json = JSONField(
         encoder_class='unknown_module.UnknownJSONEncoder')
Пример #5
0
 def test_empty_strings_not_allowed(self):
     field = JSONField()
     self.assertEqual(field.get_default(), None)
Пример #6
0
 def test_formfield_blank_clean_blank(self):
     field = JSONField("test", null=False, blank=True)
     formfield = field.formfield()
     self.assertEqual(formfield.clean(value=''), '')
Пример #7
0
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
Пример #9
0
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
Пример #10
0
 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)
Пример #11
0
 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))
Пример #12
0
 def test_invalid_json_default(self):
     with self.assertRaises(ValueError):
         field = JSONField('test', default='{"foo"}')
Пример #13
0
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)
Пример #14
0
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())
Пример #15
0
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,
        ),
    ]
Пример #16
0
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()
Пример #17
0
 def test_formfield_null_and_blank_clean_blank(self):
     field = JSONField("test", null=True, blank=True)
     formfield = field.formfield()
     self.assertEquals(formfield.clean(value=''), '')
Пример #18
0
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,
        )
Пример #19
0
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)
Пример #20
0
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)
Пример #21
0
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)
Пример #22
0
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
Пример #23
0
 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)
Пример #24
0
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 нужен для корректного отображения &mdash;
                    d[display_key] = val(key=real_key,
                                         value=mark_safe("&mdash;"))
                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
Пример #25
0
 def test_indent(self):
     JSONField('test', indent=2)
Пример #26
0
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
Пример #27
0
 class InvalidEncoderFieldTestModel(models.Model):
     json = JSONField(
         encoder_class='jsonfield.encoder.UnknownJSONEncoder')
Пример #28
0
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
Пример #29
0
 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)
Пример #30
0
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
Пример #31
0
 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))
Пример #32
0
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)
Пример #33
0
 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)
Пример #34
0
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
Пример #35
0
 def test_formfield_blank_clean_none(self):
     field = JSONField("test", null=False, blank=True)
     formfield = field.formfield()
     self.assertEquals(formfield.clean(value=None), '')
Пример #36
0
    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,