class NERMixin(ModelMixin): """ Миксин для работы с NER. Добавляет в модель методы получения списка именованных сущностей, а также прочие методы, необходимые для работы с ними """ EXTRACTED_TYPES = [PER, LOC, ORG, 'DATE', 'MONEY'] NO_INDEX_TYPES = [PER, LOC, 'DATE', 'MONEY'] REPLACERS = [ (' | | ', ' '), ('"|«|«|»|»|“|”|‘|’|‚|„', '\"'), ('–|—', '-'), ('…', '...'), ('>', '>'), ('<', '<'), ] 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
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('有聽拍,煞無影音')
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()
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
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
class LogEntry(models.Model): """ Represents an entry in the audit log. The content type is saved along with the textual and numeric (if available) primary key, as well as the textual representation of the object when it was saved. It holds the action performed and the fields that were changed in the transaction. If AuditlogMiddleware is used, the actor will be set automatically. Keep in mind that editing / re-saving LogEntry instances may set the actor to a wrong value - editing LogEntry instances is not recommended (and it should not be necessary). """ class Action: """ The actions that Auditlog distinguishes: creating, updating and deleting objects. Viewing objects is not logged. The values of the actions are numeric, a higher integer value means a more intrusive action. This may be useful in some cases when comparing actions because the ``__lt``, ``__lte``, ``__gt``, ``__gte`` lookup filters can be used in queries. The valid actions are :py:attr:`Action.CREATE`, :py:attr:`Action.UPDATE` and :py:attr:`Action.DELETE`. """ CREATE = 0 UPDATE = 1 DELETE = 2 choices = ( (CREATE, _("create")), (UPDATE, _("update")), (DELETE, _("delete")), ) content_type = models.ForeignKey('contenttypes.ContentType', on_delete=models.CASCADE, related_name='+', verbose_name=_("content type")) object_pk = models.CharField(db_index=True, max_length=255, verbose_name=_("object pk")) object_id = models.BigIntegerField(blank=True, db_index=True, null=True, verbose_name=_("object id")) object_repr = models.TextField(verbose_name=_("object representation")) action = models.PositiveSmallIntegerField(choices=Action.choices, verbose_name=_("action")) changes = models.TextField(blank=True, verbose_name=_("change message")) actor = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_("actor")) remote_addr = models.GenericIPAddressField( blank=True, null=True, verbose_name=_("remote address")) timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("timestamp")) additional_data = JSONField(blank=True, null=True, verbose_name=_("additional data")) objects = LogEntryManager() class Meta: get_latest_by = 'timestamp' ordering = ['-timestamp'] verbose_name = _("log entry") verbose_name_plural = _("log entries") def __str__(self): if self.action == self.Action.CREATE: fstring = _("Created {repr:s}") elif self.action == self.Action.UPDATE: fstring = _("Updated {repr:s}") elif self.action == self.Action.DELETE: fstring = _("Deleted {repr:s}") else: fstring = _("Logged {repr:s}") return fstring.format(repr=self.object_repr) @property def changes_dict(self): """ :return: The changes recorded in this log entry as a dictionary object. """ try: return json.loads(self.changes) except ValueError: return {} @property def changes_str(self, colon=': ', arrow=smart_text(' \u2192 '), separator='; '): """ Return the changes recorded in this log entry as a string. The formatting of the string can be customized by setting alternate values for colon, arrow and separator. If the formatting is still not satisfying, please use :py:func:`LogEntry.changes_dict` and format the string yourself. :param colon: The string to place between the field name and the values. :param arrow: The string to place between each old and new value. :param separator: The string to place between each field. :return: A readable string of the changes in this log entry. """ substrings = [] for field, values in iteritems(self.changes_dict): substring = smart_text( '{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}').format( field_name=field, colon=colon, old=values[0], arrow=arrow, new=values[1], ) substrings.append(substring) return separator.join(substrings)
class 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
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()
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
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
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
def test_formfield_null_and_blank_clean_none(self): field = JSONField("test", null=True, blank=True) formfield = field.formfield() self.assertEqual(formfield.clean(value=None), None)
class 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
def test_formfield_blank_clean_blank(self): field = JSONField("test", null=False, blank=True) formfield = field.formfield() self.assertEqual(formfield.clean(value=''), '')
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
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
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)
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, )
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
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)
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
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')
def test_invalid_json_default(self): with self.assertRaises(ValueError): JSONField('test', default='{"foo"}')
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 )
def test_indent(self): JSONField('test', indent=2)
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()
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)