Example #1
0
class NERMixin(ModelMixin):
    """
    Миксин для работы с NER. Добавляет в модель методы получения списка именованных сущностей, а также прочие методы,
    необходимые для работы с ними
    """

    EXTRACTED_TYPES = [PER, LOC, ORG, 'DATE', 'MONEY']

    NO_INDEX_TYPES = [PER, LOC, 'DATE', 'MONEY']

    REPLACERS = [
        (' | | ', ' '),
        ('"|«|«|»|»|“|”|‘|’|‚|„',
         '\"'),
        ('–|—', '-'),
        ('…', '...'),
        ('>', '>'),
        ('&lt;', '<'),
    ]

    NER_TASK_WAIT_EXECUTION_INTERVAL = 5

    ner_data = JSONField(
        verbose_name=_("NER data"),
        default={},
        help_text=
        _("Data obtained after recognition of named entities for the given text"
          ))

    def get_ner_source(self):
        '''
        Метод для получения исходных данных для получения именованных сущностей. Требуется перекрыть в модели где осуществляется
        примешивание
        :return:
        '''
        return self.entity_name

    @classmethod
    def get_extracted_types(cls):
        return cls.EXTRACTED_TYPES

    @classmethod
    def get_no_index_types(cls):
        return cls.NO_INDEX_TYPES

    @classmethod
    def get_segmenter(cls):
        segmenter = getattr(cls, "_segmenter", None)
        if not segmenter:
            segmenter = Segmenter()
            cls._segmenter = segmenter
        return segmenter

    @classmethod
    def get_morph_vocab(cls):
        morph_vocab = getattr(cls, "_morph_vocab", None)
        if not morph_vocab:
            morph_vocab = MorphVocab()
            cls._morph_vocab = morph_vocab
        return morph_vocab

    @classmethod
    def get_extractors(cls):
        extractors = getattr(cls, "_extractors", None)
        if not extractors:
            morph_vocab = cls.get_morph_vocab()
            extractors = [
                DatesExtractor(morph_vocab),
                MoneyExtractor(morph_vocab)
            ]
            cls._extractors = extractors
        return extractors

    @classmethod
    def get_embedding(cls):
        embedding = getattr(cls, "_embedding", None)
        if not embedding:
            embedding = NewsEmbedding()
            cls._embedding = embedding
        return embedding

    @classmethod
    def get_morph_tagger(cls):
        morph_tagger = getattr(cls, "_morph_tagger", None)
        if not morph_tagger:
            embedding = cls.get_embedding()
            morph_tagger = NewsMorphTagger(embedding)
            cls._morph_tagger = morph_tagger
        return morph_tagger

    @classmethod
    def get_syntax_parser(cls):
        syntax_parser = getattr(cls, "_syntax_parser", None)
        if not syntax_parser:
            embedding = cls.get_embedding()
            syntax_parser = NewsSyntaxParser(embedding)
            cls._syntax_parser = syntax_parser
        return syntax_parser

    @classmethod
    def get_ner_tagger(cls):
        ner_tagger = getattr(cls, "_ner_tagger", None)
        if not ner_tagger:
            embedding = cls.get_embedding()
            ner_tagger = NewsNERTagger(embedding)
            cls._ner_tagger = ner_tagger
        return ner_tagger

    @staticmethod
    def _extract_ner(doc, morph_tagger, morph_vocab, syntax_parser, ner_tagger,
                     extractors, extracted_types):
        # Apply morph
        doc.tag_morph(morph_tagger)
        # Lemmatize
        for token in doc.tokens:
            token.lemmatize(morph_vocab)
        # Parse syntax
        doc.parse_syntax(syntax_parser)
        # NER extract
        doc.tag_ner(ner_tagger, extractors=extractors)
        # Normalize data
        if doc.spans:
            for span in doc.spans:
                span.normalize(morph_vocab)
        # Extend person data
        if doc.spans:
            names_extractor = NamesExtractor(morph_vocab)
            for span in doc.spans:
                if span.type == PER:
                    span.extract_fact(names_extractor)
        # Get result
        result = {}
        for _ in doc.spans:
            span_type = _.type
            if span_type in extracted_types:
                if not span_type in result:
                    result.update({span_type: []})
                data = _.as_json
                result[span_type].append(data)

        return result

    def extract_ner(self):
        '''
        Данный метод вызывать только через task`и! Если его вызывать из инстанции объекта то это приведет к перерасходу
        памяти из-за того, что для каждого запущенного потока сервера будет создана копия данных нужных для извлечения
        именованных сущностей. Каждая копия использует 250-350 мегабайт оперативной памяти, на боевом сервере создается
        практически столько потоков сколько есть процессорных ядер, у сервером с большим количеством ядер это приведет
        к тому что память будет использоваться крайне неэффективно.
        '''
        doc = Doc(self.get_ner_source())
        doc.segment(self.get_segmenter())

        morph_tagger = self.get_morph_tagger()
        morph_vocab = self.get_morph_vocab()
        syntax_parser = self.get_syntax_parser()
        ner_tagger = self.get_ner_tagger()
        extractors = self.get_extractors()
        extracted_types = self.get_extracted_types()

        return self._extract_ner(doc, morph_tagger, morph_vocab, syntax_parser,
                                 ner_tagger, extractors, extracted_types)

    def extract_ner_by_task(self):
        ner_data = {}
        try:
            result = extract_ner_data.apply_async(
                kwargs={
                    "obj_id": self.id,
                    "obj_model": self.__class__.__name__.lower()
                },
                expires=self.NER_TASK_WAIT_EXECUTION_INTERVAL,
                retry=False,
            )
        except extract_ner_data.OperationalError as exc:
            logger = get_logger('logfile_error')
            logger.exception('Sending task raised: %r', exc)
        else:
            try:
                ner_data = result.get(
                    interval=self.NER_TASK_WAIT_EXECUTION_INTERVAL,
                    propagate=False,
                )
            except Exception:
                pass
        return ner_data

    @property
    def highlighter_context(self):
        result = []
        _already_append = []
        for span_type in self.ner_data.keys():
            for ner_data_by_type in self.ner_data[span_type]:
                text = ner_data_by_type['text']
                if not text in _already_append:
                    _already_append.append(text)
                    result.append({
                        'text': text,
                        'type': span_type.lower(),
                    })
        return result

    def cleaned_text_for_index(self):
        # Получаем данные для индексации тем же методом, что и при распознавании.
        text = self.get_ner_source()
        if self.ner_data:
            # Цикл по всем имеющимся в объекте типам данных NER
            for span_type in self.ner_data.keys():
                # Цикл по всем данным определенного типа
                for ner_data_by_type in self.ner_data[span_type]:
                    # Если данные включены в список исключаемого к индексации - удаляем их
                    if ner_data_by_type['type'] in self.NO_INDEX_TYPES:
                        text = text.replace(ner_data_by_type['text'], ' ')
        return text
Example #2
0
class EnterpriseCatalog(TimeStampedModel):
    """
    Associates a stored catalog query with an enterprise customer.

    .. no_pii:
    """

    uuid = models.UUIDField(
        primary_key=True,
        default=uuid4,
        editable=False,
    )
    title = models.CharField(
        max_length=255,
        blank=False,
        null=False,
    )
    enterprise_uuid = models.UUIDField(
        blank=False,
        null=False,
        db_index=True,
    )
    enterprise_name = models.CharField(
        max_length=255,
        blank=True,
    )
    catalog_query = models.ForeignKey(
        CatalogQuery,
        blank=False,
        null=True,
        related_name='enterprise_catalogs',
        on_delete=models.deletion.SET_NULL,
    )
    enabled_course_modes = JSONField(
        default=json_serialized_course_modes,
        load_kwargs={'object_pairs_hook': collections.OrderedDict},
        dump_kwargs={
            'indent': 4,
            'cls': JSONEncoder,
            'separators': (',', ':')
        },
        help_text=_(
            'Ordered list of enrollment modes which can be displayed to learners for course runs in'
            ' this catalog.'),
    )
    publish_audit_enrollment_urls = models.BooleanField(
        default=False,
        help_text=
        _("Specifies whether courses should be published with direct-to-audit enrollment URLs."
          ),
    )

    history = HistoricalRecords()

    class Meta:
        verbose_name = _("Enterprise Catalog")
        verbose_name_plural = _("Enterprise Catalogs")
        app_label = 'catalog'

    def __str__(self):
        """
        Return human-readable string representation.
        """
        return ("<EnterpriseCatalog with UUID '{uuid}' "
                "for EnterpriseCustomer '{enterprise_uuid}'>".format(
                    uuid=self.uuid, enterprise_uuid=self.enterprise_uuid))

    @property
    def content_metadata(self):
        """
        Helper to retrieve the content metadata associated with the catalog.

        Returns:
            Queryset: The queryset of associated content metadata
        """
        if not self.catalog_query:
            return ContentMetadata.objects.none()
        return self.catalog_query.contentmetadata_set.all()

    def contains_content_keys(self, content_keys):
        """
        Determines whether content_keys are part of the catalog.

        Return True if catalog contains the courses, course runs, and/or programs specified by
        the given content key(s), else False.

        A content key is considered contained within the catalog when:
          - associated metadata contains the specified content key.
          - associated metadata contains the specified content key as a parent (to handle when
            a catalog only contains course runs but a course id is searched).
          - associated metadata contains the specified content key in a nested course run (to
            handle when a catalog only contains courses but a course run id is searched).
        """
        # cannot determine if specified content keys are part of catalog when catalog
        # query doesn't exist or no content keys are provided.
        if not self.catalog_query or not content_keys:
            return False

        content_keys = set(content_keys)

        # construct a query on the associated catalog's content metadata to return metadata
        # where content_key and parent_content_key matches the specified content_keys to
        # handle the following cases where the catalog:
        #   - contains courses and the specified content_keys are course ids
        #   - contains course runs and the specified content_keys are course ids
        #   - contains course runs and the specified content_keys are course run ids
        #   - contains programs and the specified content_keys are program ids
        query = Q(content_key__in=content_keys) | Q(
            parent_content_key__in=content_keys)

        # retrieve content metadata objects for the specified content keys to get a set of
        # parent content keys, i.e. course ids associated with the specified content_keys
        # (if any) to handle the following case:
        #   - catalog contains courses and the specified content_keys are course run ids.
        searched_metadata = ContentMetadata.objects.filter(
            content_key__in=content_keys)
        parent_content_keys = {
            metadata.parent_content_key
            for metadata in searched_metadata if metadata.parent_content_key
        }
        query |= Q(content_key__in=parent_content_keys)

        # if the filtered content metadata exists, the specified content_keys exist in the catalog
        return self.content_metadata.filter(query).exists()

    def get_content_enrollment_url(self, content_resource, content_key,
                                   parent_content_key):
        """
        Return enterprise content enrollment page url with the catalog information for the given content key.

        If the enterprise customer's Learner Portal (LP) is enabled, the LP course page URL is returned.

        Arguments:
            content_resource (str): The content resource to use in the URL (i.e., "course", "program")
            content_key (str): The content key for the course to be displayed.
            parent_content_key (str): The content key for the course that is parent of the given course run key.
                                      This argument will be None if a course or program key is passed.

        Returns:
            (str): Enterprise landing page URL OR Enterprise Learner Portal course page URL.
        """
        if not (content_key and content_resource):
            return None

        params = get_enterprise_utm_context(self.enterprise_name)
        if self.publish_audit_enrollment_urls:
            params['audit'] = 'true'

        enterprise_customer = EnterpriseCustomerDetails(self.enterprise_uuid)
        learner_portal_enabled = enterprise_customer.learner_portal_enabled
        if learner_portal_enabled and content_resource is not PROGRAM:
            # parent_content_key is our way of telling if this is a course run
            # since this function is never called with COURSE_RUN as content_resource
            if parent_content_key:
                course_key = parent_content_key
                # adding course_run_key to the params for rendering the correct info
                # on the LP course page and enrolling in the intended course run
                params['course_run_key'] = content_key
            else:
                course_key = content_key
            url = '{}/{}/course/{}'.format(
                settings.ENTERPRISE_LEARNER_PORTAL_BASE_URL,
                enterprise_customer.slug, course_key)
        else:
            # Catalog param only needed for legacy (non-LP) enrollment URL
            params['catalog'] = self.uuid
            url = '{}/enterprise/{}/{}/{}/enroll/'.format(
                settings.LMS_BASE_URL,
                self.enterprise_uuid,
                content_resource,
                content_key,
            )

        return update_query_parameters(url, params)

    def get_xapi_activity_id(self, content_resource, content_key):
        """
        Return enterprise xAPI activity identifier with the catalog information
        for the given content key.  Note that the xAPI activity identifier is a
        well-formed IRI/URI but not necessarily a resolvable URL.

        Arguments:
            content_resource (str): The content resource to use in the URL (i.e., "course", "program")
            content_key (str): The content key for the course to be displayed.

        Returns:
            (str): Enterprise landing page url.
        """
        if not content_key or not content_resource:
            return None
        xapi_activity_id = '{}/xapi/activities/{}/{}'.format(
            settings.LMS_BASE_URL,
            content_resource,
            content_key,
        )
        return xapi_activity_id
class 訓練過渡格式(models.Model):
    來源 = models.CharField(max_length=100, db_index=True)
    年代 = models.CharField(max_length=30, db_index=True)
    種類 = models.CharField(
        max_length=100,
        db_index=True,
        choices=[
            ('字詞', '字詞'),
            ('語句', '語句'),
        ],
    )

    影音所在 = models.FilePathField(null=True,
                                blank=True,
                                max_length=200,
                                validators=[檢查敢是影音檔案, 檢查敢是wav])
    影音語者 = models.CharField(blank=True, max_length=100)
    文本 = models.TextField(null=True, blank=True, validators=[檢查敢是分詞])
    聽拍 = JSONField(null=True, blank=True, validators=[檢查聽拍內底欄位敢有夠])
    外文 = models.TextField(null=True, blank=True, validators=[檢查敢是分詞])

    @classmethod
    def 資料數量(cls):
        return cls.objects.all().count()

    @classmethod
    def 加一堆資料(cls, 資料陣列, 錯誤輸出=stderr):
        會使的資料 = []
        for 一筆 in 資料陣列:
            try:
                一筆.full_clean()
            except ValidationError as 錯誤:
                print(錯誤, file=錯誤輸出)
            else:
                會使的資料.append(一筆)
        訓練過渡格式.objects.bulk_create(會使的資料)

    def 編號(self):
        return self.pk

    def 聲音檔(self):
        return 聲音檔.對檔案讀(self.影音所在)

    def 影音長度(self):
        return get_duration(filename=self.影音所在)

    def clean(self):
        super().clean()
        if self.影音所在 is not None:
            self.影音所在 = abspath(self.影音所在)
            檢查敢是影音檔案(self.影音所在)
            if self.聽拍:
                檢查聽拍結束時間有超過音檔無(self.影音長度(), self.聽拍)
                if self.文本 is not None:
                    raise ValidationError('有聽拍就袂使有文本')
                if self.影音語者 != '':
                    raise ValidationError('有聽拍就袂使有語者')
        else:
            if self.影音語者:
                raise ValidationError('有指定語者,煞無影音')
            if self.聽拍:
                raise ValidationError('有聽拍,煞無影音')
Example #4
0
class PageInsight(models.Model):
    STRATEGY_DESKTOP = 'desktop'
    STRATEGY_MOBILE = 'mobile'
    STRATEGY_CHOICES = (
        (STRATEGY_DESKTOP, 'Desktop'),
        (STRATEGY_MOBILE, 'Mobile'),
    )

    STATUS_PENDING = 'pending'
    STATUS_CONSUMING = 'consuming'
    STATUS_CONSUMED = 'consumed'
    STATUS_POPULATED = 'populated'
    STATUS_ERROR = 'error'
    STATUS_CHOICES = (
        (STATUS_PENDING, 'Pending'),
        (STATUS_CONSUMING, 'Consuming'),
        (STATUS_CONSUMED, 'Consumed'),
        (STATUS_POPULATED, 'Populated'),
        (STATUS_ERROR, 'Error'),
    )

    # Mandatory fields
    url = models.URLField(_('URL'), max_length=191, db_index=True)
    strategy = models.CharField(_('Strategy'),
                                max_length=50,
                                choices=STRATEGY_CHOICES,
                                db_index=True)
    locale = models.CharField(_('Locale'), max_length=16, default='en_US')
    # Managed fields
    status = models.CharField(_('Status'),
                              max_length=32,
                              choices=STATUS_CHOICES,
                              db_index=True)
    created = models.DateTimeField(_('Date Created'), auto_now_add=True)
    updated = models.DateTimeField(_('Date Updated'), auto_now=True)
    # Populated fields
    response_code = models.IntegerField(_('Response Code'), default=0)
    title = models.CharField(_('Page Title'), max_length=255)
    score = models.IntegerField(_('Score'), default=0)
    number_resources = models.IntegerField(_('Number of Resources'), default=0)
    number_hosts = models.IntegerField(_('Number of Hosts'), default=0)
    total_request_bytes = models.IntegerField(_('Total Request Bytes'),
                                              default=0)
    number_static_resources = models.IntegerField(
        _('Number of Static Resources'), default=0)
    html_response_bytes = models.IntegerField(_('HTML Response Bytes'),
                                              default=0)
    css_response_bytes = models.IntegerField(_('CSS Response Bytes'),
                                             default=0)
    image_response_bytes = models.IntegerField(_('Image Response Bytes'),
                                               default=0)
    javascript_response_bytes = models.IntegerField(
        _('Javascript Response Bytes'), default=0)
    other_response_bytes = models.IntegerField(_('Other Response Bytes'),
                                               default=0)
    number_js_resources = models.IntegerField(_('Number of JS Resources'),
                                              default=0)
    number_css_resources = models.IntegerField(_('Number of CSS Resources'),
                                               default=0)
    json = JSONField(
        _('JSON Response'),
        load_kwargs={'object_pairs_hook': collections.OrderedDict})

    def __unicode__(self):
        return _('%s (%s)') % (self.url, self.created.date())

    def set_consuming(self):
        self.status = self.STATUS_CONSUMING
        self.save()

    def set_consumed(self):
        self.status = self.STATUS_CONSUMED
        self.save()

    def set_populated(self):
        self.status = self.STATUS_POPULATED
        self.save()

    def set_error(self):
        self.status = self.STATUS_ERROR
        self.save()

    def consume(self):
        """
        Retrieve the Google Page Insight data from the API.
        """
        try:
            self.set_consuming()

            # Ensure we have a valid url
            URLValidator(self.url)

            # Build api service
            service = build(serviceName='pagespeedonline',
                            version='v1',
                            developerKey=getattr(settings, 'GOOGLE_API_KEY',
                                                 None))

            # Make request
            data = service.pagespeedapi().runpagespeed(
                url=self.url,
                strategy=self.strategy,
                locale=self.locale,
                screenshot=settings.PSI_SCREENSHOT).execute()
            # Save json before we continue
            self.json = data
            self.set_consumed()
            return data

        except HttpError as e:
            self.json = json.loads(e.content)
            self.set_error()
            raise
        except Exception:
            self.set_error()
            raise

    def populate(self, data=None):
        """
        Populate this insight from the API.
        """
        if data is None:
            data = self.consume()

        self.response_code = data["responseCode"]
        self.title = data["title"]
        self.score = data["score"]
        self.url = data['id']
        self.number_resources = data['pageStats'].get("numberResources")
        self.number_hosts = data['pageStats']["numberHosts"]
        self.total_request_bytes = int(data['pageStats']["totalRequestBytes"])
        self.number_static_resources = data['pageStats'].get(
            "numberStaticResources", 0)
        self.html_response_bytes = int(data['pageStats']["htmlResponseBytes"])
        self.css_response_bytes = int(data['pageStats'].get(
            "cssResponseBytes", 0))
        self.image_response_bytes = int(data['pageStats'].get(
            "imageResponseBytes", 0))
        self.javascript_response_bytes = int(data['pageStats'].get(
            "javascriptResponseBytes", 0))
        self.other_response_bytes = int(data['pageStats'].get(
            "otherResponseBytes", 0))
        self.number_js_resources = int(data['pageStats'].get(
            "numberJsResources", 0))
        self.number_css_resources = int(data['pageStats'].get(
            "numberCssResources", 0))
        self.save()

        if settings.PSI_SCREENSHOT and data.get('screenshot'):
            screenshot = Screenshot()
            screenshot.page_insight = self
            screenshot.width = data.get('screenshot').get('width')
            screenshot.height = data.get('screenshot').get('height')
            screenshot.mime_type = data.get('screenshot').get('mime_type')

            # Write file to memory
            if BytesIO:
                f = BytesIO(
                    b64decode(
                        data.get('screenshot').get('data').replace(
                            '_', '/').replace('-', '+')))
            else:
                f = StringIO.StringIO(
                    b64decode(
                        data.get('screenshot').get('data').replace(
                            '_', '/').replace('-', '+')))
            # Determine the file extension
            ext = mimetypes.guess_extension(screenshot.mime_type)
            # Save to disk
            screenshot.image.save('screenshot%s' % ext, ContentFile(f.read()))

            screenshot.save()

        # Save the rules
        for key, values in data.get('formattedResults').get(
                'ruleResults').items():
            ruleResult = RuleResult()
            ruleResult.page_insight = self
            ruleResult.title = values.get('localizedRuleName')
            ruleResult.impact = values.get('ruleImpact')
            ruleResult.description = values.get(
                'urlBlocks')[0]['header']['format']
            ruleResult.save()

        self.set_populated()
Example #5
0
class User(AbstractUser):
    """Custom user model for use with OIDC."""

    full_name = models.CharField(_('Full Name'),
                                 max_length=255,
                                 blank=True,
                                 null=True)

    @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

    tracking_context = JSONField(blank=True, null=True)

    class Meta(object):
        get_latest_by = 'date_joined'
        db_table = 'ecommerce_user'

    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):
        """
        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 = EdxRestApiClient(get_lms_url('api/credit/v1/'),
                                   oauth_access_token=self.access_token)
            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 = EdxRestApiClient(
                site.siteconfiguration.build_lms_url('api/user/v1/'),
                oauth_access_token=self.access_token)
            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
Example #6
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)
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.

    .. no_pii:
    """

    REQUIREMENT_STATUS_CHOICES = (
        ("satisfied", "satisfied"),
        ("failed", "failed"),
        ("declined", "declined"),
    )

    username = models.CharField(max_length=255, db_index=True)
    requirement = models.ForeignKey(CreditRequirement, related_name="statuses", on_delete=models.CASCADE)
    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={})

    class Meta(object):
        unique_together = ('username', 'requirement')
        verbose_name_plural = ugettext_lazy('Credit requirement statuses')

    @classmethod
    def get_statuses(cls, requirements, username):
        """
        Get credit requirement statuses of given requirement and username

        Args:
            requirements(list of CreditRequirements): 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.atomic
    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:
            # do not update status to `failed` if user has `satisfied` the requirement
            if status == 'failed' and requirement_status.status == 'satisfied':
                log.info(
                    u'Can not change status of credit requirement "%s" from satisfied to failed ',
                    requirement_status.requirement_id
                )
                return
            requirement_status.status = status
            requirement_status.reason = reason
            requirement_status.save()

    @classmethod
    @transaction.atomic
    def remove_requirement_status(cls, username, requirement):
        """
        Remove credit requirement status for given username.

        Args:
            username(str): Username of the user
            requirement(CreditRequirement): 'CreditRequirement' object
        """

        try:
            requirement_status = cls.objects.get(username=username, requirement=requirement)
            requirement_status.delete()
        except cls.DoesNotExist:
            log_msg = (
                u'The requirement status {requirement} does not exist for username {username}.'.format(
                    requirement=requirement,
                    username=username
                )
            )
            log.error(log_msg)
            return

    @classmethod
    def retire_user(cls, retirement):
        """
        Retire a user by anonymizing

        Args:
            retirement: UserRetirementStatus of the user being retired
        """
        requirement_statuses = cls.objects.filter(
            username=retirement.original_username
        ).update(
            username=retirement.retired_username,
            reason={},
        )
        return requirement_statuses > 0
Example #8
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)
Example #9
0
class CourseDiscussionSettings(models.Model):
    """
    Settings for course discussions

    .. no_pii:
    """
    course_id = CourseKeyField(
        unique=True,
        max_length=255,
        db_index=True,
        help_text="Which course are these settings associated with?",
    )
    discussions_id_map = JSONField(
        null=True,
        blank=True,
        help_text=
        "Key/value store mapping discussion IDs to discussion XBlock usage keys.",
    )
    always_divide_inline_discussions = models.BooleanField(default=False)
    _divided_discussions = models.TextField(db_column='divided_discussions',
                                            null=True,
                                            blank=True)  # JSON list

    COHORT = 'cohort'
    ENROLLMENT_TRACK = 'enrollment_track'
    NONE = 'none'
    ASSIGNMENT_TYPE_CHOICES = ((NONE, 'None'), (COHORT, 'Cohort'),
                               (ENROLLMENT_TRACK, 'Enrollment Track'))
    division_scheme = models.CharField(max_length=20,
                                       choices=ASSIGNMENT_TYPE_CHOICES,
                                       default=NONE)

    class Meta:
        # use existing table that was originally created from django_comment_common app
        db_table = 'django_comment_common_coursediscussionsettings'

    @property
    def divided_discussions(self):
        """
        Jsonify the divided_discussions
        """
        return json.loads(self._divided_discussions)

    @divided_discussions.setter
    def divided_discussions(self, value):
        """
        Un-Jsonify the divided_discussions
        """
        self._divided_discussions = json.dumps(value)

    @request_cached()
    @classmethod
    def get(cls, course_key):
        """
        Get and/or create settings
        """
        try:
            course_discussion_settings = cls.objects.get(course_id=course_key)
        except cls.DoesNotExist:
            from openedx.core.djangoapps.course_groups.cohorts import get_legacy_discussion_settings
            legacy_discussion_settings = get_legacy_discussion_settings(
                course_key)
            course_discussion_settings, _ = cls.objects.get_or_create(
                course_id=course_key,
                defaults={
                    'always_divide_inline_discussions':
                    legacy_discussion_settings[
                        'always_cohort_inline_discussions'],
                    'divided_discussions':
                    legacy_discussion_settings['cohorted_discussions'],
                    'division_scheme':
                    cls.COHORT
                    if legacy_discussion_settings['is_cohorted'] else cls.NONE
                },
            )
        return course_discussion_settings

    def update(self, validated_data: dict):
        """
        Set discussion settings for a course

        Returns:
            A CourseDiscussionSettings object
        """
        fields = {
            'division_scheme': (str, )[0],
            'always_divide_inline_discussions': bool,
            'divided_discussions': list,
        }
        for field, field_type in fields.items():
            if field in validated_data:
                if not isinstance(validated_data[field], field_type):
                    raise ValueError(
                        f"Incorrect field type for `{field}`. Type must be `{field_type.__name__}`"
                    )
                setattr(self, field, validated_data[field])
        self.save()
        return self
Example #10
0
class ImagePluginBase(CMSPlugin):
    glossary = JSONField(default={})

    class Meta:
        abstract = True
class SiteConfiguration(models.Model):
    """
    Model for storing site configuration. These configuration override OpenEdx configurations and settings.
    e.g. You can override site name, logo image, favicon etc. using site configuration.

    Fields:
        site (OneToOneField): one to one field relating each configuration to a single site
        site_values (JSONField):  json field to store configurations for a site

    .. no_pii:
    """
    site = models.OneToOneField(Site,
                                related_name='configuration',
                                on_delete=models.CASCADE)
    enabled = models.BooleanField(default=False, verbose_name="Enabled")
    site_values = JSONField(
        null=False,
        blank=True,
        # The actual default value is determined by calling the given callable.
        # Therefore, the default here is just {}, since that is the result of
        # calling `dict`.
        default=dict,
        load_kwargs={'object_pairs_hook': collections.OrderedDict})

    def __str__(self):
        return f"<SiteConfiguration: {self.site} >"  # xss-lint: disable=python-wrap-html

    def __repr__(self):
        return self.__str__()

    def get_value(self, name, default=None):
        """
        Return Configuration value for the key specified as name argument.

        Function logs a message if configuration is not enabled or if there is an error retrieving a key.

        Args:
            name (str): Name of the key for which to return configuration value.
            default: default value tp return if key is not found in the configuration

        Returns:
            Configuration value for the given key or returns `None` if configuration is not enabled.
        """
        if self.enabled:
            try:
                return self.site_values.get(name, default)
            except AttributeError as error:
                logger.exception('Invalid JSON data. \n [%s]', error)
        else:
            logger.info("Site Configuration is not enabled for site (%s).",
                        self.site)

        return default

    @classmethod
    def get_configuration_for_org(cls, org, select_related=None):
        """
        This returns a SiteConfiguration object which has an org_filter that matches
        the supplied org

        Args:
            org (str): Org to use to filter SiteConfigurations
            select_related (list or None): A list of values to pass as arguments to select_related
        """
        query = cls.objects.filter(site_values__contains=org,
                                   enabled=True).all()
        if select_related is not None:
            query = query.select_related(*select_related)
        for configuration in query:
            course_org_filter = configuration.get_value(
                'course_org_filter', [])
            # The value of 'course_org_filter' can be configured as a string representing
            # a single organization or a list of strings representing multiple organizations.
            if not isinstance(course_org_filter, list):
                course_org_filter = [course_org_filter]
            if org in course_org_filter:
                return configuration
        return None

    @classmethod
    def get_value_for_org(cls, org, name, default=None):
        """
        This returns site configuration value which has an org_filter that matches
        what is passed in,

        Args:
            org (str): Course ord filter, this value will be used to filter out the correct site configuration.
            name (str): Name of the key for which to return configuration value.
            default: default value tp return if key is not found in the configuration

        Returns:
            Configuration value for the given key.
        """
        configuration = cls.get_configuration_for_org(org)
        if configuration is None:
            return default
        else:
            return configuration.get_value(name, default)

    @classmethod
    def get_all_orgs(cls):
        """
        This returns all of the orgs that are considered in site configurations, This can be used,
        for example, to do filtering.

        Returns:
            A set of all organizations present in site configuration.
        """
        org_filter_set = set()

        for configuration in cls.objects.filter(
                site_values__contains='course_org_filter', enabled=True).all():
            course_org_filter = configuration.get_value(
                'course_org_filter', [])
            if not isinstance(course_org_filter, list):
                course_org_filter = [course_org_filter]
            org_filter_set.update(course_org_filter)
        return org_filter_set

    @classmethod
    def has_org(cls, org):
        """
        Check if the given organization is present in any of the site configuration.

        Returns:
            True if given organization is present in site configurations otherwise False.
        """
        return org in cls.get_all_orgs()
Example #12
0
class Bookmark(TimeStampedModel):
    """
    Bookmarks model.
    """
    user = models.ForeignKey(User, db_index=True, on_delete=models.CASCADE)
    course_key = CourseKeyField(max_length=255, db_index=True)
    usage_key = UsageKeyField(max_length=255, db_index=True)
    _path = JSONField(db_column='path',
                      help_text='Path in course tree to the block')

    xblock_cache = models.ForeignKey('bookmarks.XBlockCache',
                                     on_delete=models.CASCADE)

    class Meta(object):
        """
        Bookmark metadata.
        """
        unique_together = ('user', 'usage_key')

    def __unicode__(self):
        return self.resource_id

    @classmethod
    def create(cls, data):
        """
        Create a Bookmark object.

        Arguments:
            data (dict): The data to create the object with.

        Returns:
            A Bookmark object.

        Raises:
            ItemNotFoundError: If no block exists for the usage_key.
        """
        data = dict(data)
        usage_key = data.pop('usage_key')

        with modulestore().bulk_operations(usage_key.course_key):
            block = modulestore().get_item(usage_key)

            xblock_cache = XBlockCache.create({
                'usage_key':
                usage_key,
                'display_name':
                block.display_name_with_default,
            })
            data['_path'] = prepare_path_for_serialization(
                Bookmark.updated_path(usage_key, xblock_cache))

        data['course_key'] = usage_key.course_key
        data['xblock_cache'] = xblock_cache

        user = data.pop('user')

        # Sometimes this ends up in data, but newer versions of Django will fail on having unknown keys in defaults
        data.pop('display_name', None)

        bookmark, created = cls.objects.get_or_create(usage_key=usage_key,
                                                      user=user,
                                                      defaults=data)
        return bookmark, created

    @property
    def resource_id(self):
        """
        Return the resource id: {username,usage_id}.
        """
        return u"{0},{1}".format(self.user.username, self.usage_key)  # pylint: disable=no-member

    @property
    def display_name(self):
        """
        Return the display_name from self.xblock_cache.

        Returns:
            String.
        """
        return self.xblock_cache.display_name  # pylint: disable=no-member

    @property
    def path(self):
        """
        Return the path to the bookmark's block after checking self.xblock_cache.

        Returns:
            List of dicts.
        """
        if self.modified < self.xblock_cache.modified:  # pylint: disable=no-member
            path = Bookmark.updated_path(self.usage_key, self.xblock_cache)
            self._path = prepare_path_for_serialization(path)
            self.save()  # Always save so that self.modified is updated.
            return path

        return parse_path_data(self._path)

    @staticmethod
    def updated_path(usage_key, xblock_cache):
        """
        Return the update-to-date path.

        xblock_cache.paths is the list of all possible paths to a block
        constructed by doing a DFS of the tree. However, in case of DAGS,
        which section jump_to_id() takes the user to depends on the
        modulestore. If xblock_cache.paths has only one item, we can
        just use it. Otherwise, we use path_to_location() to get the path
        jump_to_id() will take the user to.
        """
        if xblock_cache.paths and len(xblock_cache.paths) == 1:
            return xblock_cache.paths[0]

        return Bookmark.get_path(usage_key)

    @staticmethod
    def get_path(usage_key):
        """
        Returns data for the path to the block in the course graph.

        Note: In case of multiple paths to the block from the course
        root, this function returns a path arbitrarily but consistently,
        depending on the modulestore. In the future, we may want to
        extend it to check which of the paths, the user has access to
        and return its data.

        Arguments:
            block (XBlock): The block whose path is required.

        Returns:
            list of PathItems
        """
        with modulestore().bulk_operations(usage_key.course_key):
            try:
                path = search.path_to_location(modulestore(),
                                               usage_key,
                                               full_path=True)
            except ItemNotFoundError:
                log.error(u'Block with usage_key: %s not found.', usage_key)
                return []
            except NoPathToItem:
                log.error(u'No path to block with usage_key: %s.', usage_key)
                return []

            path_data = []
            for ancestor_usage_key in path:
                if ancestor_usage_key != usage_key and ancestor_usage_key.block_type != 'course':  # pylint: disable=no-member
                    try:
                        block = modulestore().get_item(ancestor_usage_key)
                    except ItemNotFoundError:
                        return []  # No valid path can be found.
                    path_data.append(
                        PathItem(usage_key=block.location,
                                 display_name=block.display_name_with_default))

        return path_data
Example #13
0
class XBlockCache(TimeStampedModel):
    """
    XBlockCache model to store info about xblocks.
    """

    course_key = CourseKeyField(max_length=255, db_index=True)
    usage_key = UsageKeyField(max_length=255, db_index=True, unique=True)

    display_name = models.CharField(max_length=255, default='')
    _paths = JSONField(
        db_column='paths',
        default=[],
        help_text='All paths in course tree to the corresponding block.')

    def __unicode__(self):
        return unicode(self.usage_key)

    @property
    def paths(self):
        """
        Return paths.

        Returns:
            list of list of PathItems.
        """
        return [parse_path_data(path)
                for path in self._paths] if self._paths else self._paths

    @paths.setter
    def paths(self, value):
        """
        Set paths.

        Arguments:
            value (list of list of PathItems): The list of paths to cache.
        """
        self._paths = [prepare_path_for_serialization(path)
                       for path in value] if value else value

    @classmethod
    def create(cls, data):
        """
        Create an XBlockCache object.

        Arguments:
            data (dict): The data to create the object with.

        Returns:
            An XBlockCache object.
        """
        data = dict(data)

        usage_key = data.pop('usage_key')
        usage_key = usage_key.replace(
            course_key=modulestore().fill_in_run(usage_key.course_key))

        data['course_key'] = usage_key.course_key
        xblock_cache, created = cls.objects.get_or_create(usage_key=usage_key,
                                                          defaults=data)

        if not created:
            new_display_name = data.get('display_name',
                                        xblock_cache.display_name)
            if xblock_cache.display_name != new_display_name:
                xblock_cache.display_name = new_display_name
                xblock_cache.save()

        return xblock_cache
Example #14
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)
    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(object):
        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.warn(
                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.warn(
                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 (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.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 (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
Example #15
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)
Example #16
0
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)

    @cached_property
    def plugin_class(self):
        return self.get_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_instance(self):
        for model in CascadeModelBase._get_cascade_elements():
            try:
                return model.objects.get(id=self.parent_id)
            except model.DoesNotExist:
                continue

    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.
        """
        parent = self.get_parent_instance()
        if parent:
            return parent.get_complete_glossary()
        # otherwise 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 isinstance(self.glossary, str):
            self.glossary = json.loads(self.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 get_num_children(self):
        """
        Returns the number of children for this plugin instance.
        """
        return self.get_children().count()

    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)

    @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
Example #17
0
 def test_formfield_blank_clean_blank(self):
     field = JSONField("test", null=False, blank=True)
     formfield = field.formfield()
     self.assertEqual(formfield.clean(value=''), '')
Example #18
0
class SiteConfiguration(models.Model):
    """
    Model for storing site configuration. These configuration override OpenEdx configurations and settings.
    e.g. You can override site name, logo image, favicon etc. using site configuration.

    Fields:
        site (OneToOneField): one to one field relating each configuration to a single site
        values (JSONField):  json field to store configurations for a site
    """
    site = models.OneToOneField(Site, related_name='configuration')
    enabled = models.BooleanField(default=False, verbose_name="Enabled")
    values = JSONField(
        null=False,
        blank=True,
        load_kwargs={'object_pairs_hook': collections.OrderedDict}
    )

    def __unicode__(self):
        return u"<SiteConfiguration: {site} >".format(site=self.site)

    def __repr__(self):
        return self.__unicode__()

    def get_value(self, name, default=None):
        """
        Return Configuration value for the key specified as name argument.

        Function logs a message if configuration is not enabled or if there is an error retrieving a key.

        Args:
            name (str): Name of the key for which to return configuration value.
            default: default value tp return if key is not found in the configuration

        Returns:
            Configuration value for the given key or returns `None` if configuration is not enabled.
        """
        if self.enabled:
            try:
                return self.values.get(name, default)  # pylint: disable=no-member
            except AttributeError as error:
                logger.exception('Invalid JSON data. \n [%s]', error)
        else:
            logger.info("Site Configuration is not enabled for site (%s).", self.site)

        return default

    @classmethod
    def get_value_for_org(cls, org, name, default=None):
        """
        This returns site configuration value which has an org_filter that matches
        what is passed in,

        Args:
            org (str): Course ord filter, this value will be used to filter out the correct site configuration.
            name (str): Name of the key for which to return configuration value.
            default: default value tp return if key is not found in the configuration

        Returns:
            Configuration value for the given key.
        """
        for configuration in cls.objects.filter(values__contains=org, enabled=True).all():
            org_filter = configuration.get_value('course_org_filter', None)
            if org_filter == org:
                return configuration.get_value(name, default)
        return default

    @classmethod
    def get_all_orgs(cls):
        """
        This returns all of the orgs that are considered in site configurations, This can be used,
        for example, to do filtering.

        Returns:
            Configuration value for the given key.
        """
        org_filter_set = set()
        for configuration in cls.objects.filter(values__contains='course_org_filter', enabled=True).all():
            org_filter = configuration.get_value('course_org_filter', None)
            if org_filter:
                org_filter_set.add(org_filter)
        return org_filter_set
Example #19
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.

    .. no_pii:
    """

    course = models.ForeignKey(CreditCourse, related_name="credit_requirements", on_delete=models.CASCADE)
    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)

    CACHE_NAMESPACE = u"credit.CreditRequirement.cache."

    class Meta(object):
        unique_together = ('namespace', 'name', 'course')
        ordering = ["order"]

    def __unicode__(self):
        return '{course_id} - {name}'.format(course_id=self.course.course_key, name=self.display_name)

    @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
    @request_cached(namespace=CACHE_NAMESPACE)
    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)

        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, None otherwise.

        """
        try:
            return cls.objects.get(
                course__course_key=course_key, active=True, namespace=namespace, name=name
            )
        except cls.DoesNotExist:
            return None
Example #20
0
class PillowError(models.Model):
    doc_id = models.CharField(max_length=255, null=False, db_index=True)
    pillow = models.CharField(max_length=255, null=False, db_index=True)
    date_created = models.DateTimeField()
    date_last_attempt = models.DateTimeField()
    date_next_attempt = models.DateTimeField(db_index=True, null=True)
    total_attempts = models.IntegerField(default=0)
    current_attempt = models.IntegerField(default=0, db_index=True)
    error_type = models.CharField(max_length=255, null=True, db_index=True)
    error_traceback = models.TextField(null=True)
    change = JSONField(null=True)
    change_metadata = JSONField(null=True)

    @property
    def change_object(self):
        change = change_from_couch_row(
            self.change if self.change else {'id': self.doc_id})
        if self.change_metadata:
            change.metadata = ChangeMeta.wrap(self.change_metadata)
        change.document = None
        return change

    class Meta(object):
        app_label = 'pillow_retry'
        unique_together = (
            'doc_id',
            'pillow',
        )

    def add_attempt(self, exception, traceb, date=None):
        self.current_attempt += 1
        self.total_attempts += 1
        self.date_last_attempt = date or datetime.utcnow()
        self.error_type = path_from_object(exception)

        self.error_traceback = "{}\n\n{}".format(
            exception.message, "".join(traceback.format_tb(traceb)))

        if self.current_attempt <= settings.PILLOW_RETRY_QUEUE_MAX_PROCESSING_ATTEMPTS:
            time_till_next = settings.PILLOW_RETRY_REPROCESS_INTERVAL * math.pow(
                self.current_attempt, settings.PILLOW_RETRY_BACKOFF_FACTOR)
            self.date_next_attempt = self.date_last_attempt + timedelta(
                minutes=time_till_next)
        else:
            self.date_next_attempt = None

    def reset_attempts(self):
        self.current_attempt = 0
        self.date_next_attempt = datetime.utcnow()

    def has_next_attempt(self):
        return self.current_attempt == 0 or (
            self.total_attempts <= self.multi_attempts_cutoff()
            and self.current_attempt <=
            settings.PILLOW_RETRY_QUEUE_MAX_PROCESSING_ATTEMPTS)

    @classmethod
    def get_or_create(cls, change, pillow):
        change.document = None
        doc_id = change.id
        try:
            error = cls.objects.get(doc_id=doc_id, pillow=pillow.pillow_id)
        except cls.DoesNotExist:
            now = datetime.utcnow()
            error = PillowError(doc_id=doc_id,
                                pillow=pillow.pillow_id,
                                date_created=now,
                                date_last_attempt=now,
                                date_next_attempt=now,
                                change=change.to_dict())

            if change.metadata:
                error.change_metadata = change.metadata.to_json()

        return error

    @classmethod
    def get_errors_to_process(cls,
                              utcnow,
                              limit=None,
                              skip=0,
                              fetch_full=False):
        """
        Get errors according the following rules:

            date_next_attempt <= utcnow
            AND
            (
                total_attempts <= multi_attempt_cutoff & current_attempt <= max_attempts
                OR
                total_attempts > multi_attempt_cutoff & current_attempt 0
            )

        where:
        * multi_attempt_cutoff = settings.PILLOW_RETRY_MULTI_ATTEMPTS_CUTOFF
        * max_attempts = settings.PILLOW_RETRY_QUEUE_MAX_PROCESSING_ATTEMPTS

        :param utcnow:      The current date and time in UTC.
        :param limit:       Paging limit param.
        :param skip:        Paging skip param.
        :param fetch_full:  If True return the whole PillowError object otherwise return a
                            a dict containing 'id' and 'date_next_attempt' keys.
        """
        max_attempts = settings.PILLOW_RETRY_QUEUE_MAX_PROCESSING_ATTEMPTS
        multi_attempts_cutoff = cls.multi_attempts_cutoff()
        query = PillowError.objects \
            .filter(date_next_attempt__lte=utcnow) \
            .filter(
                models.Q(current_attempt=0) |
                (models.Q(total_attempts__lte=multi_attempts_cutoff) & models.Q(current_attempt__lte=max_attempts))
            )

        # temporarily disable queuing of ConfigurableReportKafkaPillow errors
        query = query.filter(~models.Q(
            pillow=
            'corehq.apps.userreports.pillow.ConfigurableReportKafkaPillow'))

        if not fetch_full:
            query = query.values('id', 'date_next_attempt')
        if limit is not None:
            return query[skip:skip + limit]
        else:
            return query

    @classmethod
    def multi_attempts_cutoff(cls):
        default = settings.PILLOW_RETRY_QUEUE_MAX_PROCESSING_ATTEMPTS * 3
        return getattr(settings, 'PILLOW_RETRY_MULTI_ATTEMPTS_CUTOFF', default)

    @classmethod
    def bulk_reset_attempts(cls, last_attempt_lt, attempts_gte=None):
        if attempts_gte is None:
            attempts_gte = settings.PILLOW_RETRY_QUEUE_MAX_PROCESSING_ATTEMPTS

        multi_attempts_cutoff = cls.multi_attempts_cutoff()
        return PillowError.objects.filter(
            models.Q(date_last_attempt__lt=last_attempt_lt),
            models.Q(current_attempt__gte=attempts_gte)
            | models.Q(total_attempts__gte=multi_attempts_cutoff)).update(
                current_attempt=0, date_next_attempt=datetime.utcnow())

    @classmethod
    def get_pillows(cls):
        results = PillowError.objects.values('pillow').annotate(
            count=Count('pillow'))
        return (p['pillow'] for p in results)

    @classmethod
    def get_error_types(cls):
        results = PillowError.objects.values('error_type').annotate(
            count=Count('error_type'))
        return (e['error_type'] for e in results)
Example #21
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.

    .. no_pii:
    """

    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", on_delete=models.CASCADE)
    provider = models.ForeignKey(CreditProvider, related_name="credit_requests", on_delete=models.CASCADE)
    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
    )

    class Meta(object):
        # 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 retire_user(cls, retirement):
        """
        Obfuscates CreditRecord instances associated with `original_username`.
        Empties the records' `parameters` field and replaces username with its
        anonymized value, `retired_username`.
        """
        num_updated_credit_requests = cls.objects.filter(
            username=retirement.original_username
        ).update(
            username=retirement.retired_username,
            parameters={},
        )
        return num_updated_credit_requests > 0

    @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,
            status=self.status,
        )
Example #22
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=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)

    @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 iteritems(self.changes_dict):
            # 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 hasattr(field, 'choices') and len(field.choices) > 0:
                choices_dict = dict(field.choices)
            if hasattr(field, 'base_field') and getattr(
                    field.base_field, 'choices', False):
                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
Example #23
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)

    @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['SOCIAL_AUTH_EDX_OIDC_KEY'],  # pylint: disable=unsubscriptable-object
            self.oauth_settings['SOCIAL_AUTH_EDX_OIDC_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)
Example #24
0
class Script(models.Model):
    shop = models.ForeignKey("shuup.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 shuup.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: shuup.notify.script.Context
        """
        for step in self.get_steps():
            if step.execute(context) == StepNext.STOP:
                break
Example #25
0
def actstream_register_model(model):
    """
    Set up GenericRelations for a given actionable model.
    """
    for field in ('actor', 'target', 'action_object'):
        generic.GenericRelation(
            Action,
            content_type_field='%s_content_type' % field,
            object_id_field='%s_object_id' % field,
            related_name='actions_with_%s_%s_as_%s' %
            (model._meta.app_label, model._meta.module_name, field),
        ).contribute_to_class(model, '%s_actions' % field)

        # @@@ I'm not entirely sure why this works
        setattr(
            Action, 'actions_with_%s_%s_as_%s' %
            (model._meta.app_label, model._meta.module_name, field), None)


if actstream_settings.USE_JSONFIELD:
    try:
        from jsonfield.fields import JSONField
    except ImportError:
        raise ImproperlyConfigured(
            'You must have django-jsonfield installed '
            'if you wish to use a JSONField on your actions')
    JSONField(blank=True, null=True).contribute_to_class(Action, 'data')

# connect the signal
action.connect(action_handler, dispatch_uid='actstream.models')
Example #26
0
 def test_invalid_json_default(self):
     with self.assertRaises(ValueError):
         JSONField('test', default='{"foo"}')
Example #27
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
                )
Example #28
0
 def test_indent(self):
     JSONField('test', indent=2)
Example #29
0
class Notification(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')

    timestamp = models.DateTimeField(default=timezone.now)

    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:
        ordering = ('-timestamp', )
        app_label = 'notifications'

    def __unicode__(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 __str__(self):  # Adds support for Python 3
        return self.__unicode__()

    def timesince(self, now=None):
        """
        Shortcut for the ``django.utils.timesince.timesince`` function of the
        current timestamp.
        """
        from django.utils.timesince import timesince as timesince_
        return timesince_(self.timestamp, 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()
Example #30
0
class BaseOrderItem(with_metaclass(deferred.ForeignKeyBuilder, models.Model)):
    """
    An item for an order.
    """
    order = deferred.ForeignKey(BaseOrder,
                                related_name='items',
                                verbose_name=_("Order"))
    product_name = models.CharField(
        _("Product name"),
        max_length=255,
        null=True,
        blank=True,
        help_text=_("Product name at the moment of purchase."))
    product_code = models.CharField(
        _("Product code"),
        max_length=255,
        null=True,
        blank=True,
        help_text=_("Product code at the moment of purchase."))
    product = deferred.ForeignKey(BaseProduct,
                                  null=True,
                                  blank=True,
                                  on_delete=models.SET_NULL,
                                  verbose_name=_("Product"))
    _unit_price = models.DecimalField(
        _("Unit price"),
        null=True,  # may be NaN
        help_text=_("Products unit price at the moment of purchase."),
        **BaseOrder.decimalfield_kwargs)
    _line_total = models.DecimalField(
        _("Line Total"),
        null=True,  # may be NaN
        help_text=_("Line total on the invoice at the moment of purchase."),
        **BaseOrder.decimalfield_kwargs)
    extra = JSONField(verbose_name=_("Extra fields"),
                      default={},
                      help_text=_("Arbitrary information for this order item"))

    class Meta:
        abstract = True
        verbose_name = _("Order item")
        verbose_name_plural = _("Order items")

    def __str__(self):
        return self.product_name

    @classmethod
    def perform_model_checks(cls):
        try:
            cart_field = [
                f for f in CartItemModel._meta.fields
                if f.attname == 'quantity'
            ][0]
            order_field = [
                f for f in cls._meta.fields if f.attname == 'quantity'
            ][0]
            if order_field.get_internal_type() != cart_field.get_internal_type(
            ):
                msg = "Field `{}.quantity` must be of one same type `{}.quantity`."
                raise ImproperlyConfigured(
                    msg.format(cls.__name__, CartItemModel.__name__))
        except IndexError:
            msg = "Class `{}` must implement a field named `quantity`."
            raise ImproperlyConfigured(msg.format(cls.__name__))

    @property
    def unit_price(self):
        return MoneyMaker(self.order.currency)(self._unit_price)

    @property
    def line_total(self):
        return MoneyMaker(self.order.currency)(self._line_total)

    def populate_from_cart_item(self, cart_item, request):
        """
        From a given cart item, populate the current order item.
        If the operation was successful, the given item shall be removed from the cart.
        If a CartItem.DoesNotExist exception is raised, discard the order item.
        """
        if cart_item.quantity == 0:
            raise CartItemModel.DoesNotExist("Cart Item is on the Wish List")
        self.product = cart_item.product
        # for historical integrity, store the product's name and price at the moment of purchase
        self.product_name = cart_item.product.product_name
        self._unit_price = Decimal(cart_item.product.get_price(request))
        self._line_total = Decimal(cart_item.line_total)
        self.quantity = cart_item.quantity
        self.extra = dict(cart_item.extra)
        extra_rows = [(modifier, extra_row.data)
                      for modifier, extra_row in cart_item.extra_rows.items()]
        self.extra.update(rows=extra_rows)

    def save(self, *args, **kwargs):
        """
        Before saving the OrderItem object to the database, round the amounts to the given decimal places
        """
        self._unit_price = BaseOrder.round_amount(self._unit_price)
        self._line_total = BaseOrder.round_amount(self._line_total)
        super(BaseOrderItem, self).save(*args, **kwargs)