class Step(models.Model): application = models.ForeignKey(Application, related_name='steps', on_delete=models.PROTECT) number = models.PositiveIntegerField(db_index=True) title = models.CharField(max_length=512) instructions = models.TextField(blank=True) widget_type = models.CharField('Widget Type', default='Questions', max_length=128, choices=[(key, key) for key in widgets]) widget_conf = YAMLField('Widget Configuration', blank=True) class Meta: ordering = ('number', ) def __str__(self): return '#{} {}'.format(self.number, self.title) @property def widget(self): if not hasattr(self, '_widget'): self._widget = widgets[self.widget_type](self) return self._widget def clean(self): self.widget.clean_instructions() self.widget.clean_widget_conf()
class ViewEntity(models.Model): name = models.CharField(max_length=30) specYaml = YAMLField(blank=True) target = models.ForeignKey(TargetEntity, on_delete=models.CASCADE) def get_external_spec(self): transforms = [ transform.specYaml for transform in self.transformentity_set.all() ] return {'transforms': transforms} def save(self, *args, **kwargs): self.saveSpec() super().save(*args, **kwargs) self.target.save() def saveSpec(self): transforms = self.get_external_spec()['transforms'] if len(transforms) > 0: self.specYaml['transforms'] = transforms def __repr__(self): keyVals = ", ".join([ "{}={}".format(i[0], i[1]) for i in [v for v in vars(self).items()] ]) return "%s(%s)" % (self.__class__.__name__, keyVals) def to_json(self): pass def to_yaml(self): pass
class TransformEntity(models.Model): name = models.CharField(max_length=30) specYaml = YAMLField(blank=True) view = models.ForeignKey(ViewEntity, on_delete=models.CASCADE) def save(self, *args, **kwargs): self.saveSpec() super().save(*args, **kwargs) self.view.save() def saveSpec(self): transforms = self.get_spec()['transforms'] if transforms == None and self.view.name == 'demo': transforms = { 'type': 'aggregate', 'by': 'statut_code', 'columns': ['nombre'], 'method': 'sum' } self.specYaml = transforms def get_spec(self): transform = None return {'transforms': transform}
class Monitor(models.Model): name = models.CharField(max_length=255) description = models.TextField(blank=True, null=True) engine = models.CharField(max_length=32, default='prometheus') metadata = YAMLField(blank=True, null=True) status = models.CharField(max_length=32, default='active') def client(self): client_class = utils.get_module(self.engine, 'monitor') return client_class(**{ 'name': self.name, 'engine': self.engine, 'metadata': self.metadata }) def widgets(self): return self.metadata.get('widget', {}) def color(self): if self.status == 'active': return 'success' if self.status == 'error': return 'danger' if self.status == 'build': return 'info' else: return 'warning' def __str__(self): return self.name class Meta: ordering = ['name']
class Formulario(models.Model): """ <pre>nome: "Nome do Formulário" campos: - escolha: nome: "nomeprogramatico" label: "Label do Campo" cardinalidade: ("multipla"|"unica") tipo: ("boolean"|"int"|"string") opcoes: - "label1" - "label2" - "label3" valores: - "val1" - "val2" - "val3" - check: nome: "outronomeprogramatico" label: "Outro Label do Campo" default: false</pre> """ nome = models.CharField(max_length=50, unique=True) estrutura = YAMLField(help_text=__doc__) def __str__(self): return self.nome
class Inventory(models.Model): name = models.CharField(max_length=255) description = models.TextField(blank=True, null=True) engine = models.CharField(max_length=32, default='reclass') metadata = YAMLField(blank=True, null=True) status = models.CharField(max_length=32, default='unknown') def client(self): client_class = utils.get_module(self.engine, 'inventory') return client_class(**{ 'name': self.name, 'engine': self.engine, 'metadata': self.metadata }) def class_list(self, resource=None): return self.client().class_list(resource=None) def inventory(self, resource=None): return self.client().inventory(resource=None) def resource_count(self, resource=None): return len(self.client().inventory(resource=None)) def __str__(self): return self.name class Meta: verbose_name_plural = "Inventories"
class FilterEntity(models.Model): name = models.CharField(max_length=30) specYaml = YAMLField(blank=True) target = models.ForeignKey(TargetEntity, on_delete=models.CASCADE) def get_spec(self): filters = None return {'filters': filters} def save(self, *args, **kwargs): self.saveSpec() super().save(*args, **kwargs) self.target.save() def saveSpec(self): filters = self.get_spec()['filters'] if filters == None and self.target.name == 'demo': filters = { 'field': 'Etat administratif', 'type': 'widget', 'multi': True, 'empty_select': True } self.specYaml = filters
class BoardEntity(models.Model): name = models.CharField(max_length=50) slug = models.SlugField(max_length=50, unique=True) config = YAMLField(blank=True) layout = models.CharField(max_length=50, default="grid") logo = models.ImageField(blank=True) template = models.CharField(max_length=50, default="material") ncols = models.IntegerField(default=2) status = models.CharField( max_length=15, choices=[(status.name, status.value ) for status in StatusEnum], default=StatusEnum.DRAFT ) type = models.CharField( max_length=15, choices=[(typeBoard.name, typeBoard.value) for typeBoard in TypeBoardEnum], default = TypeBoardEnum.DASHBOARD ) def __str__(self): return self.name def _get_unique_slug(self): ''' In this method a unique slug is created ''' slug = slugify(self.name) unique_slug = slug num = 1 while BoardEntity.objects.filter(slug=unique_slug).exists(): unique_slug = '{}-{}'.format(slug, num) num += 1 return unique_slug def save(self, *args, **kwargs): if not self.slug: self.slug = self._get_unique_slug() self.saveConfig(kwargs.pop('conf') if kwargs.get('conf') else None) super().save(*args, **kwargs) def saveConfig(self, conf): targets = [target.specYaml for target in self.targetentity_set.all()] if not conf: conf = {'layout': 'grid', 'ncols': 2, 'template': 'material', 'theme': 'default', 'title': 'Nouveau DashBoard' } spec = SpecYamlCreator(config=conf,targets=targets) self.config = spec.to_yaml()
class Problem(RespectedOrder, models.Model): lesson = models.ForeignKey(Lesson, related_name='problems') slug = models.SlugField(verbose_name="Слаг для адресной строки") name = models.CharField(verbose_name="Название", max_length=255) description = models.TextField(verbose_name="Описание", default='', blank=True) test_data = YAMLField(verbose_name="Тесты", default='') class Meta: order_with_respect_to = 'lesson' unique_together = ('lesson', 'slug')
class Monitor(models.Model): name = models.CharField(max_length=255) description = models.TextField(blank=True, null=True) metadata = YAMLField(blank=True, null=True) status = models.CharField(max_length=32, default='active') def widgets(self): return self.metadata.get('widget', {}) def __str__(self): return self.name
class Resource(models.Model): uid = models.CharField(max_length=511) name = models.CharField(max_length=511) manager = models.ForeignKey(Manager, on_delete=models.CASCADE) kind = models.CharField(max_length=32) size = models.IntegerField(default=1) metadata = YAMLField(blank=True, null=True) status = models.CharField(max_length=32, default='unknown') def __str__(self): return '{} {}'.format(self.kind, self.name) def relations(self): return Relationship.objects.filter(Q(source=self) | Q(target=self))
class UploadImage(models.Model): alignment = (('L', 'left'), ('R', 'right')) id = models.AutoField(primary_key=True) filename = models.CharField(max_length=128, default='face_no_id') original_filename = models.CharField(max_length=128, default="no_name") file = models.ImageField(upload_to='uploads/') extension = models.CharField(max_length=10) align = models.CharField(max_length=1, choices=alignment, default='L') width = models.SmallIntegerField(default=0) height = models.SmallIntegerField(default=0) upload_date = models.DateTimeField(auto_now=True) yaml_file = YAMLField() def __str__(self): return "{0}\t{1}".format(self.filename, self.upload_date) def rename(self, count): new_name = '_'.join(['Face', str(self.align), str(count)]) new_name += '.' + str(self.extension) return new_name def get_shape(self): face_path = self.file img = io.imread(face_path) width = img.shape[0] height = img.shape[1] return width, height def extract_feature(self, predictor_path): face_path = self.file detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor(predictor_path) img = resize(io.imread(face_path), (700, 700), mode='reflect') img = img_as_ubyte(img) dets = detector(img, 1) for d in dets: shape = predictor(img, d) try: shape except NameError: print("Not found any face") return [] else: result = [] for i in range(shape.num_parts): point = shape.part(i) result.append({'x': point.x, 'y': point.y, 'z': 0}) yaml_file = dump({'filename': self.filename, 'features': result}) return yaml_file, result
class Relationship(models.Model): manager = models.ForeignKey(Manager, on_delete=models.CASCADE) source = models.ForeignKey(Resource, on_delete=models.CASCADE, related_name='source') target = models.ForeignKey(Resource, on_delete=models.CASCADE, related_name='destination') kind = models.CharField(max_length=32) size = models.IntegerField(default=1) metadata = YAMLField(blank=True, null=True) status = models.CharField(max_length=32, default='unknown') def __str__(self): return '{} > {}'.format(self.source, self.target) def name(self): return self.__str__()
class TargetEntity(models.Model): name = models.CharField(max_length=30) specYaml = YAMLField(blank=True) board = models.ForeignKey(BoardEntity, on_delete=models.CASCADE) def get_spec(self): views = [view.specYaml for view in self.viewentity_set.all()] filters = [filter.specYaml for filter in self.filterentity_set.all()] source = { 'type': 'intake', 'uri': os.path.join(path, 'catalog.yml'), 'dask': False } return { 'name': self.name, 'title': self.name, 'source': source, 'views': views, 'filters': filters } def save(self, *args, **kwargs): self.saveSpec() super().save(*args, **kwargs) def saveSpec(self): views = self.get_spec()['views'] filters = self.get_spec()['filters'] source = self.get_spec()['source'] title = self.get_spec()['title'] name = self.get_spec()['name'] p = {'name': name, 'title': title, 'source': source} if len(views) > 0: if views[0]: p['views'] = views if len(filters) > 0: if filters[0]: p['filters'] = filters spec = SpecYamlCreator(**p) self.specYaml = spec.to_yaml()
class Inventory(models.Model): name = models.CharField(max_length=255) description = models.TextField(blank=True, null=True) engine = models.CharField(max_length=32, default='reclass') metadata = YAMLField(blank=True, null=True) status = models.CharField(max_length=32, default='unknown') def client(self): client_class = utils.get_module(self.engine, 'inventory') return client_class(**{ 'name': self.name, 'engine': self.engine, 'metadata': self.metadata }) def class_list(self, resource=None): return self.client().class_list(resource=None) def inventory(self, resource=None): return self.client().inventory(resource=None) def resource_count(self, resource=None): return len(self.client().inventory(resource=None)) def color(self): if self.status == 'active': return 'success' if self.status == 'error': return 'danger' if self.status == 'build': return 'info' else: return 'warning' def __str__(self): return self.name class Meta: verbose_name_plural = "Inventories" ordering = ['name']
class Option(models.Model): DESTINATIONS = ( (1, 'payment'), (2, 'donation'), (3, 'payment & donation'), ) name = models.CharField( max_length=255, help_text=_('userfriendly name of the payment option')) institution = models.ForeignKey(Institution) provider = models.CharField(max_length=255, choices=get_providers_choices()) # description = models.TextField() settings = YAMLField(blank=True, null=True, help_text="check settings for each provider") currency = models.CharField(max_length=3, choices=CURRENCIES) destination = models.PositiveIntegerField(choices=DESTINATIONS) def get_provider(self, payment): Provider = provider_factory(self.provider) provider = Provider(dict(self.settings or {}), payment) return provider @property def has_automatic_provider(self): return self.provider in ['cash', 'bank_transfer'] def check_settings(self): Provider = provider_factory(self.provider) return Provider.check_settings(self.settings) def clean(self): settings_errors = self.check_settings() if settings_errors: raise ValidationError( _('There errors in settings: %s' % ", ".join(settings_errors))) def __str__(self): return "%s" % self.name
class Document(models.Model): name = models.CharField(max_length=255) description = models.TextField(blank=True, null=True) engine = models.CharField(max_length=32, default='dashboard') metadata = YAMLField(blank=True, null=True) status = models.CharField(max_length=32, default='active') def widgets(self): return self.metadata.get('widget', {}) def color(self): if self.status == 'active': return 'success' if self.status == 'error': return 'danger' if self.status == 'build': return 'info' else: return 'warning' def __str__(self): return self.name
class Manager(models.Model): name = models.CharField(max_length=255) description = models.TextField(blank=True, null=True) engine = models.CharField(max_length=32, default='saltstack') metadata = YAMLField(blank=True, null=True) status = models.CharField(max_length=32, default='unknown') def __str__(self): return self.name def client(self): client_class = utils.get_module(self.engine, 'manager') return client_class(**{ 'name': self.name, 'engine': self.engine, 'metadata': self.metadata }) def get_schema(self): return utils.get_resource_schema(self.engine) def resources_by_kind(self): kinds = self.get_schema()['resource'] output = {} for kind in kinds: output[kind] = Resource.objects.filter(manager=self, kind=kind) return output def url(self): if self.metadata is None: return '-' elif self.engine == 'saltstack': return self.metadata.get('auth_url', '-') elif self.engine == 'kubernetes': return self.metadata.get('cluster', {}).get('server', '-') elif self.engine == 'openstack': return self.metadata.get('auth', {}).get('auth_url', '-') else: return '-'
class StudyCondition(models.Model): '''Groupings of participants within a study.''' study = models.ForeignKey('signalbox.Study', blank=True, null=True) users = models.ManyToManyField(settings.AUTH_USER_MODEL, through='Membership') tag = models.SlugField(max_length=255, default="main") display_name = models.CharField( max_length=30, blank=True, null=True, help_text="""A label for this condition which can be shown to the Participant (e.g. a non-descriptive name used to identify a condition in an experimental session without breaking a blind.""", ) weight = models.IntegerField( default=1, help_text="""Relative weights to allocate users to conditions""") scripts = models.ManyToManyField('signalbox.Script', blank=True) metadata = YAMLField( blank=True, null=True, help_text= """YAML meta data available describing this condition. Can be used on Questionnaires, e.g. to conditionally display questions.""") def expected_n(self): """Return the expected number of participants based on the total currently allocated to the parent study.""" weights = [i.weight for i in self.study.studycondition_set.all()] w = self.weight / sum(weights) return self.study.membership_set.count() * w class Meta: ordering = ['study', 'tag'] app_label = 'signalbox' def __unicode__(self): return "%s > %s " % (self.study.name, self.tag)
class Resource(models.Model): uid = models.CharField(max_length=511) name = models.CharField(max_length=511) monitor = models.ForeignKey(Monitor, on_delete=models.CASCADE) kind = models.CharField(max_length=32) size = models.IntegerField(default=1) metadata = YAMLField(blank=True, null=True) status = models.CharField(max_length=32, default='unknown') def __str__(self): return '{} {}'.format(self.kind, self.name) def color(self): if self.status == 'active': return 'success' if self.status == 'error': return 'danger' if self.status == 'build': return 'info' else: return 'warning' class Meta: ordering = ['name']
class ChoiceSet(models.Model): """The set of options attached to Questions.""" objects = ChoiceSetManager() name = models.SlugField(max_length=64, unique=True) yaml = YAMLField(blank=True, validators=[valid.checkyamlchoiceset], default=DEFAULTYAMLCHOICESET) def natural_key(self): return (self.name, ) def __unicode__(self): return self.natural_key() def dict_for_yaml(self): sd = { i.order: { 'score': i.score, 'label': i.label, 'is_default_value': i.is_default_value } for i in self.get_choices() } # comprehension to filter out null values to make things clearer to edit by hand sd = { k: {a: b for a, b in list(v.items()) if b is not None} for k, v in list(sd.items()) } return {self.name: sd} def to_dict(self): return self.yaml @contract def default_value(self): """ :returns: The default value of the choiceset :rtype: int|None """ choices = [x for x in self.get_choices() if x.is_default_value] if choices: return int(getattr(choices[0], 'is_default_value')) def values_as_json(self): choices = dict([(str(i.score), str(i.label)) for i in self.get_choices()]) return json.dumps(values(self), indent=4, sort_keys=True) @contract def choice_tuples(self): """ :returns: A list of tuples containing scores and labels. :rtype: list(tuple(int, string)) """ return [(int(i.score), i.label) for i in self.get_choices()] MARKDOWN_FORMAT = """{isdefault}{score}{mapped_score}={label} """ def as_markdown(self): if not self.yaml: self.yaml = { i: d for i, d in enumerate([{ 'score': x.score, 'isdefault': x.is_default_value, 'label': x.label } for x in self.get_choices()]) } def _formatmappedscore(c): score = c['score'] mapped_score = c.get('mapped_score', score) return mapped_score != score and "[{}]".format(mapped_score) or "" return "\n".join([ self.MARKDOWN_FORMAT.format( **{ 'isdefault': c.get('isdefault', "") and "*" or "", 'score': c['score'], 'mapped_score': _formatmappedscore(c), 'label': c['label'], }) for i, c in list(self.yaml.items()) ]) # synonym in case we want to change display choices_as_string = lambda self: self.as_markdown() def get_choices(self): """ "returns: A sorted list of Choice objects :rtype: list(a) # this is transitional... choicesets are now specified as Yaml rather than via # db-saved Choice objects, but we recreate them by hand to avoid editing code elsewhere """ if self.yaml: try: return sorted([ Choice(choiceset=self, score=choice.get('score'), mapped_score=choice.get('mapped_score', choice.get('score')), is_default_value=choice.get('isdefault', False), label=choice.get('label', ''), order=i) for i, choice in list(self.yaml.items()) ], key=lambda x: x.order) except: return [] else: # or if no yaml set, do it the old way cset = list( Choice.objects.filter(choiceset=self).order_by("order")) return cset @contract def allowed_responses(self): """ :returns: A list of valid options, e.g. used to validate user input :rtype: list(int) """ return [int(i.score) for i in self.get_choices()] def __unicode__(self): return '%s' % (self.name, ) class Meta: app_label = 'ask' ordering = ["name"]
class FlowRuleException(models.Model): flow_id = models.CharField(max_length=200, blank=False, null=False) participation = models.ForeignKey(Participation, db_index=True) expiration = models.DateTimeField(blank=True, null=True) creator = models.ForeignKey(User, null=True) creation_time = models.DateTimeField(default=now, db_index=True) comment = models.TextField(blank=True, null=True) kind = models.CharField(max_length=50, blank=False, null=False, choices=FLOW_RULE_KIND_CHOICES) rule = YAMLField(blank=False, null=False) active = models.BooleanField(default=True) def __unicode__(self): return "%s exception for '%s' to '%s' in '%s'" % ( self.kind, self.participation.user, self.flow_id, self.participation.course) def clean(self): if (self.kind == flow_rule_kind.grading and self.expiration is not None): raise ValidationError("grading rules may not expire") from course.validation import (ValidationError as ContentValidationError, validate_session_start_rule, validate_session_access_rule, validate_session_grading_rule, ValidationContext) from course.content import (get_course_repo, get_course_commit_sha, get_flow_desc) from relate.utils import dict_to_struct rule = dict_to_struct(self.rule) repo = get_course_repo(self.participation.course) commit_sha = get_course_commit_sha(self.participation.course, self.participation) ctx = ValidationContext(repo=repo, commit_sha=commit_sha) flow_desc = get_flow_desc(repo, self.participation.course, self.flow_id, commit_sha) tags = None if hasattr(flow_desc, "rules"): tags = getattr(flow_desc.rules, "tags", None) try: if self.kind == flow_rule_kind.start: validate_session_start_rule(ctx, unicode(self), rule, tags) elif self.kind == flow_rule_kind.access: validate_session_access_rule(ctx, unicode(self), rule, tags) elif self.kind == flow_rule_kind.grading: validate_session_grading_rule(ctx, unicode(self), rule, tags) else: raise ValidationError("invalid rule kind: " + self.kind) except ContentValidationError as e: raise ValidationError("invalid existing_session_rules: " + str(e))
class WebSite( six.with_metaclass(WebSiteMeta, MPTTModel, DiffMixin, AbstractDuplicateAwareModel)): """ Web site object. Used to hold options for a whole website. """ class Meta: app_label = 'core' verbose_name = _(u'Web site') verbose_name_plural = _(u'Web sites') translate = ( 'short_description', 'description', ) # HEADS UP: we exclude the URL from inplace_edit form, else the # Django URLField will always add a trailing slash to domain-only # websites while we remove them. This is probably a bug in our heads # (we shouldn't remove the trailing slash on domain-only URLs ?), # but we want it to work this way for now. # # For 'mail_warned', it's the traditional JSONField + inplace error. INPLACEEDIT_EXCLUDE = [ 'url', 'mail_warned', ] # class MPTTMeta: # order_insertion_by = ['url'] name = models.CharField(max_length=128, verbose_name=_(u'name'), null=True, blank=True) slug = models.CharField(max_length=128, verbose_name=_(u'slug'), null=True, blank=True) url = models.URLField(unique=True, verbose_name=_(u'url'), blank=True) parent = TreeForeignKey('self', null=True, blank=True, related_name='children') # TODO: move this into Website to avoid too much parallel fetches # when using multiple feeds from the same origin website. fetch_limit_nr = models.IntegerField( default=config.FEED_FETCH_PARALLEL_LIMIT, verbose_name=_(u'fetch limit'), blank=True, help_text=_(u'The maximum number of articles that can be fetched ' u'from the website in parallel. If less than {0}, do ' u'not touch: the workers have already tuned it from ' u'real-life results.').format( config.FEED_FETCH_PARALLEL_LIMIT)) mail_warned = JSONField(default=list, blank=True) date_created = models.DateTimeField( auto_now_add=True, db_index=True, verbose_name=_(u'Date added'), help_text=_(u'When the web site was added to the 1flow database.')) date_updated = models.DateTimeField( auto_now=True, verbose_name=_(u'Date updated'), help_text=_(u'When the web site was updated.')) image = models.ImageField( verbose_name=_(u'Image'), null=True, blank=True, upload_to=get_website_image_upload_path, max_length=256, help_text=_(u'Use either image when 1flow instance hosts the ' u'image, or image_url when hosted elsewhere. If ' u'both are filled, image takes precedence.')) image_url = models.URLField( null=True, blank=True, max_length=384, verbose_name=_(u'Image URL'), help_text=_(u'Full URL of the image displayed in the feed ' u'selector. Can be hosted outside of 1flow.')) short_description = models.CharField( null=True, blank=True, max_length=256, verbose_name=_(u'Short description'), help_text=_(u'Public short description of the feed, for ' u'auto-completer listing. Markdown text.')) description = models.TextField( null=True, blank=True, verbose_name=_(u'Description'), help_text=_(u'Public description of the feed. Markdown text.')) processing_chain = models.ForeignKey(ProcessingChain, null=True, blank=True, related_name='websites') processing_parameters = YAMLField( null=True, blank=True, verbose_name=_(u'Processing parameters'), help_text=_(u'Processing parameters for this website. ' u'Can be left empty. As they are more specific, ' u'the website parameters take precedence over the ' u'processors parameters, but will be overriden by ' u'feed-level or item-level processing parameters, ' u'if any. In YAML format (see ' u'http://en.wikipedia.org/wiki/YAML for details).')) # ————————————————————————————————————————————————————————— Python & Django def __unicode__(self): """ I'm __unicode__, pep257. """ return u'%s #%s (%s)%s' % (self.name or u'WebSite', self.id, self.url, (_(u'(dupe of #%s)') % self.duplicate_of.id) if self.duplicate_of else u'') # ——————————————————————————————————————————————————————————— Class methods @classmethod def get_from_url(cls, url): """ Will get you the ``Website`` object from an :param:`url`. After having striped down the path part (eg. ``http://test.com/my-article`` gives you the web site ``http://test.com``, without the trailing slash). It will return ``None`` if the url is really bad. .. note:: unlike :meth:`get_or_create_website`, this method will harmonize urls: ``Website.get_from_url('http://toto.com')`` and ``Website.get_from_url('http://toto.com/')`` will give you back the same result. This is intended, to avoid duplication. """ try: proto, host_and_port, remaining = split_url(url) except: LOGGER.exception(u'Unable to split url “%s”', url) return None base_url = '%s://%s' % (proto, host_and_port) @cached_as(WebSite, timeout=3600, extra=base_url) def _get_website_from_url(base_url): try: website, _ = WebSite.objects.get_or_create(url=base_url) except: LOGGER.exception( 'Could not get or create website from url ' u'“%s” (via original “%s”)', base_url, url) return None return website try: return _get_website_from_url(base_url) except TypeError: return None def to_json(self, related_to=None): return OrderedDict( id=unicode(self.id), name=self.name, slug=self.slug, url=self.url, image_url=self.image_url, short_description=self.short_description, )
class AnalyzedImage(models.Model): CAROUSEL_DIR = 'carousel/' CAROUSEL_OPTM_DIR = 'carousel-optm/' title = models.CharField(max_length=100, verbose_name=_("Nome")) title_en = models.CharField(max_length=100, verbose_name=_("Nome (EN)"), default='', blank=True) author = models.CharField(max_length=100, verbose_name=_("Autor")) date = models.DateField(verbose_name=_("Data")) action = models.ForeignKey(Action, related_name='carousel', on_delete=models.CASCADE, verbose_name=_('Ação')) image = models.ImageField(upload_to=CAROUSEL_DIR, null=False, blank=False, verbose_name=_('Imagem')) optimized_image = models.ImageField(upload_to=CAROUSEL_OPTM_DIR, null=True, blank=True, verbose_name=_('Imagem otimizada')) info = YAMLField(default='', verbose_name=_('Resultados da Análise')) info_en = YAMLField(default='', verbose_name=_('Resultados da Análise (EN)'), blank=True) order = models.PositiveIntegerField(verbose_name=_('Posição'), default=1) class Meta: verbose_name = _('Imagem Analisada') verbose_name_plural = _('Imagens Analisadas') ordering = ['order'] @property def analysis(self): return self.info.get('analise') or [] @property def products(self): return self.info.get('produtos') or [] @property def category(self): return self.info.get('categoria') or self.info.get('conteúdo') or '' @property def thumbnails(self): return self.info.get('thumbnails') or self.info_en.get('thumbnails') or [] @property def analysis_en(self): return self.info_en.get('analise') or [] @property def products_en(self): return self.info_en.get('produtos') or [] @property def category_en(self): return self.info_en.get('categoria') or self.info_en.get('conteúdo') or '' @property def clean_url(self): url = self.image.url return url.split('?')[0] def optimize_image(self): height, width = self.image.height, self.image.width name = self.image.name.split('/')[1].lower() content = BytesIO(self.image.read()) optimized = Image.open(content) if height > 800: new_height = 800 new_width = int(new_height * width / height) size = (new_width, new_height) optimized = optimized.resize(size, Image.ANTIALIAS) self.optimized_image.name = self.CAROUSEL_OPTM_DIR + name with self.optimized_image.open('wb') as out: optimized.save(out, quality=90) def save(self, *args, **kwargs): super().save(*args, **kwargs) for cache in self.action.caches: cache.clear()
class Processor(six.with_metaclass(ProcessorMeta, MPTTModel, DiffMixin, AbstractDuplicateAwareModel)): """ content processor model. A processor takes some input and does something with it. The input can be anything, from an URI string to a database model ID/klass. It can eventually return something if it's part of a processor chain, or can alter directly the model in some conditions (this needs more thinking and debate, at least for the security implications ?). Examples of processors : - 1flow's historical integrated processor: - does a requests.get() (URL → utf-8 HTML content) - html2text (utf-8 HTML → utf-8 Markdown) - if it fails, it wipes the HTML content, for next processor to find a clean context. - the "newspaper full" one: - does the download() and parse() at the same time. - the newspaper download() only (produces only HTML). - the breadability full processor. - the multipage finder processor: - requests.get() HTML, - inspects the HTML for pages links, - TODO: does it run the website default parser? Then how is the multipage launched? There seem to be a chichen-and-egg like question. """ class Meta: app_label = 'core' verbose_name = _(u'Processor') verbose_name_plural = _(u'Processors') translate = ('short_description', 'description', ) objects = ProcessorManager() name = models.CharField( max_length=128, verbose_name=_(u'Name'), ) slug = models.CharField( max_length=128, verbose_name=_(u'slug'), null=True, blank=True, unique=True, ) user = models.ForeignKey( User, null=True, blank=True, verbose_name=_(u'Creator'), related_name='processors' ) maintainer = models.ForeignKey( User, null=True, blank=True, verbose_name=_(u'Maintainer'), related_name='maintained_processors' ) parent = TreeForeignKey('self', null=True, blank=True, related_name='children') categories = models.ManyToManyField( ProcessorCategory, null=True, blank=True, related_name='processors', help_text=_(u'Any relevant categories. Helps reducing resources ' u'consumtion when looking up processors, and ' u'also helps staff members find processors to manage.') ) languages = models.ManyToManyField( Language, null=True, blank=True, related_name='processors', help_text=_(u'The language(s) in which this processor can ' u'process content. Let empty (None/null) for any ' u'language or if not relevant.'), ) is_active = models.BooleanField( # False upon creation of an empty processor seemsa sane default. default=False, help_text=_(u'Indicates whether this processor is globally active ' u'or not, despite its active status in processor chains ' u'(this field takes precedence).'), ) source_uri = models.CharField( null=True, blank=True, max_length=384, verbose_name=_(u'Source address'), help_text=_(u'The processor home, if any. Either a web URI or a ' u'local pathname.')) short_description = models.CharField( null=True, blank=True, max_length=256, verbose_name=_(u'Short description')) description = models.TextField( null=True, blank=True, verbose_name=_(u'Description')) requirements = models.TextField( null=True, blank=True, verbose_name=_(u'Requirements'), help_text=_(u'PIP-compatible requirements. Can be empty. See ' u'https://github.com/1flow/1flow/wiki/Processors ' u'for details.')) parameters = YAMLField( null=True, blank=True, verbose_name=_(u'Processor parameters'), help_text=_(u'Parameters accepted by this processor, in YAML ' u'format (see http://en.wikipedia.org/wiki/YAML for ' u'details). Can be left empty if none. This information ' u'is purely informative for now; it is meant to help ' u'chains maintainers to setup specific parameters for ' u'website-dedicated chains.')) accept_code = models.TextField( null=True, blank=True, verbose_name=_(u'Accept source code'), help_text=_(u'See https://github.com/1flow/1flow/wiki/Processors ' u'for details.')) process_code = models.TextField( null=True, blank=True, verbose_name=_(u'Processing source code'), help_text=_(u'See https://github.com/1flow/1flow/wiki/Processors ' u'for details.')) chainings = generic.GenericRelation( # Using the model class would imply an import loop. 'ChainedItem', content_type_field='item_type', object_id_field='item_id') # ————————————————————————————————————————————————————————— Python & Django def __unicode__(self): """ I'm __unicode__, pep257. """ return u'{0} ({1})'.format(self.name, self.id) def natural_key(self): """ Helps (de-)serialization. """ return (self.slug, ) # ——————————————————————————————————————————————————————————— Class methods @property def source_uri_format(self): """ Prettify the source_uri if possible for display. """ if self.source_uri is None: return None if self.source_uri.startswith(settings.PROJECT_ROOT): return u'$PROJECT_ROOT{0}'.format( self.source_uri[len(settings.PROJECT_ROOT):]) # ———————————————————————————————————————————————————————— Instance methods def update_changed_slug(self, old_slug, new_slug): """ Eventually update our code with the new slug if relevant. This method is called at `pre_save()` when a processor detects that its slug changed. """ changed = False for attr_name in ('accept_code', 'process_code', 'requirements'): old_code = getattr(self, attr_name) if old_code is None: # Happens when a processor is beiing created # inactive and author didn't write any code yet. continue # pattern, repl, string new_code = re.sub(ur'''['"]{0}['"]'''.format(old_slug), u"'{0}'".format(new_slug), old_code, re.UNICODE) if new_code != old_code: setattr(self, attr_name, new_code) changed = True if changed: self.save() LOGGER.info(u'%s: updated code for changed slug %s → %s.', self, old_slug, new_slug) def _internal_exec(self, code_to_run, instance, **kwargs): """ Create a local scope dict for pseudo-restricted exec calls. """ from oneflow.core.models import reldb as core_models local_scope = OrderedDict({ 'instance': instance, # The communication tunnel between exec() and the processor. 'data': SimpleObject(), # This will ship all exceptions, including ours. 'models': core_models, # TODO: enhance the logger to match the documentation. 'LOGGER': LOGGER, 'statsd': statsd, 'get_processor_by_slug': lambda x: Processor.objects.get(slug=x), # TODO: does copy really copy the LazySettingsObject? 'settings': copy.copy(settings), 'config': copy.copy(config), 'parameters': kwargs.get('parameters', {}), 'verbose': kwargs.get('verbose', True), 'force': kwargs.get('force', False), 'commit': kwargs.get('commit', True), }) function_string = u"""def processor_function({0}): {1} data.result = processor_function({2}) """.format( u', '.join(local_scope.keys()), code_to_run.replace(u'\n', u'\n '), u', '.join(u'%s=%s' % (k, k) for k in local_scope.keys()), ) # LOGGER.info(u'Running: %s', len(function_string)) exec function_string in {}, local_scope # LOGGER.info(u'Executed %s! Returning: %s', len(function_string), # local_scope['data'].result) return local_scope['data'].result def security_check(self, only=None): """ Raise an exception if :param:`code` tries to do bad things. .. note:: it will only detect **obvious** things, and it's probably not clever enough, eg. I'm pretty sure it will also detect false positives. """ def check_for_obvious_crap(code): """ This function will miss complexly hidden things. But it's a start. """ raise NotImplementedError('BOOOOO') if u'import' in code: raise ProcessorSecurityException('%s: ', self) for code_type, code_text in ( ('accept', self.accept_code), ('process', self.process_code), ): if only is None or only == code_type: check_for_obvious_crap(code_text) def accepts(self, instance, **kwargs): """ Return True/False if we can process this instance in its state. """ # TODO: implement the config and uncomment this. # # if config.PROCESSOR_DYNAMIC_SECURITY_CHECK: # self.security_check('accept') verbose = kwargs.get('verbose', True) if verbose and settings.DEBUG: LOGGER.debug(u'%s: testing acceptance of %s %s…', self, instance._meta.verbose_name, instance.id) if self.source_uri.startswith(u'/'): module = import_processor_module(self.source_uri) return module.accepts(self, instance=instance, **kwargs) try: result = self._internal_exec(self.accept_code, instance, **kwargs) except StopProcessingException: # Don't bother sentry with this. raise except Exception, e: LOGGER.error(u'%s: %s raised while running accepts() code.', self, e.__class__.__name__) raise if verbose: if result: LOGGER.info(u'%s: ACCEPTED %s %s.', self, instance._meta.verbose_name, instance.id) else: LOGGER.info(u'%s: REJECTED %s %s.', self, instance._meta.verbose_name, instance.id) return result
class Asker(models.Model): objects = AskerManager() def natural_key(self): return (self.slug, ) allow_unauthenticated_download_of_anonymous_data = models.BooleanField( default=False) anonymous_download_token = ShortUUIDField(auto=True) name = models.CharField(max_length=255, null=True) slug = models.SlugField(max_length=128, help_text="""To use in URLs etc""", unique=True, verbose_name="""Reference code""") success_message = models.TextField( default="Questionnaire complete.", help_text= """Message which appears in banner on users screen on completion of the questionnaire.""" ) redirect_url = models.CharField( max_length=128, default="/profile/", help_text=""""URL to redirect to when Questionnaire is complete.""") show_progress = models.BooleanField(default=False, help_text="""Show a progress field in the header of each page.""") finish_on_last_page = models.BooleanField( default=False, help_text="""Mark the reply as complete when the user gets to the last page. NOTE this implies there should be not questions requiring input on the last page, or these values will never be saved.""" ) step_navigation = models.BooleanField(default=True, help_text="""Allow navigation to steps.""") steps_are_sequential = models.BooleanField(default=True, help_text="""Each page in the questionnaire must be completed before the next; if unchecked then respondents can skip around within the questionnaire and complete the pages 'out of order'.""") hide_menu = models.BooleanField(default=True) system_audio = YAMLField(blank=True, help_text="""A mapping of slugs to text strings or urls which store audio files, for the system to play on errors etc. during IVR calls""" ) width = models.PositiveIntegerField(default=12) def get_phrase(self, key): if self.system_audio: return self.system_audio.get(key, IVR_SYSTEM_MESSAGES.get(key)) else: return IVR_SYSTEM_MESSAGES.get(key, None) def anonymous_replies(self): from signalbox.models import Reply return Reply.objects.filter(asker=self) def anonymous_reply_stats(self): replies = self.anonymous_replies() return { 'Complete total': replies.filter(complete=True).count(), 'Complete past month': replies.filter(complete=True, last_submit__lt=datetime.now() - timedelta(days=30)).count(), 'Incomplete total': replies.filter(complete=False).count(), 'Incomplete past month': replies.filter(complete=False, last_submit__lt=datetime.now() - timedelta(days=30)).count(), } def used_in_studies(self): from signalbox.models import Study return Study.objects.filter(studycondition__scripts__asker=self) def used_in_scripts(self): from signalbox.models import Script return Script.objects.filter(asker=self) def used_in_study_conditions(self): """Return a queryset of the StudyConditions in which this Asker appears.""" from signalbox.models import StudyCondition scripts = self.used_in_scripts() conds = set(StudyCondition.objects.filter(scripts__in=scripts)) return conds def approximate_time_to_complete(self): """Returns the approximate number of minutes the asker will take to complete. Rounded to the nearest 5 minutes (with a minumum of 5 minutes).""" baseround = lambda x, base=5: int(base * round(float(x) / base)) n_questions = Question.objects.filter( page__in=self.askpage_set.all()).count() mins = (n_questions * .5) * .9 return max([5, baseround(mins, 5)]) def scoresheets(self): return itertools.chain( *[i.scoresheets() for i in self.askpage_set.all()]) def summary_scores(self, reply): answers = reply.answer_set.all() return {i.name: i.compute(answers) for i in self.scoresheets()} @contract def questions(self, reply=None): """All questions, filtered by previous answers if a reply is passed in, see methods on Page. :rtype: list """ questionsbypage = [ i.get_questions(reply=reply) for i in self.askpage_set.all() ] for i, pagelist in enumerate(questionsbypage): for q in pagelist: q.on_page = i questions = list(itertools.chain(*questionsbypage)) return questions def first_page(self): return self.askpage_set.all()[0] def page_count(self): return self.askpage_set.all().count() def admin_edit_url(self): return admin_edit_url(self) def get_absolute_url(self): return reverse('preview_asker', kwargs={ 'asker_id': self.id, 'page_num': 0 }) def get_anonymous_url(self, request): return reverse('start_anonymous_survey', kwargs={'asker_id': self.id}) def json_export(self): """Export Asker and related objects as json. Export everything needed to recreate this questionnaire on another signalbox instance (or 3rd party system) with the exception of question assets.""" _asdict = lambda object, fields: { i: str(supergetattr(object, i)) for i in fields } pages = self.askpage_set.all() questionlists = [i.get_questions() for i in pages] questions = list(itertools.chain(*questionlists)) choicesets = set([i.choiceset for i in questions if i.choiceset]) choices = Choice.objects.filter(choiceset__in=choicesets) QUESTION_FIELDS = ('natural_key', 'variable_name', 'order', 'required', 'help_text', 'text', 'choiceset.pk', 'q_type', 'showif') CHOICESET_FIELDS = ('natural_key', 'name') CHOICE_FIELDS = ('choiceset.natural_key', 'is_default_value', 'order', 'label', 'score') qdicts = [_asdict(i, QUESTION_FIELDS) for i in questions] # give a consistent ordering to everything [d.update({'order': i}) for i, d in enumerate(qdicts)] output = { 'meta': { 'name': self.slug, 'show_progress': self.show_progress, 'generated': str(datetime.now()), }, 'questions': qdicts, 'choicesets': [_asdict(i, CHOICESET_FIELDS) for i in choicesets], 'choices': [_asdict(i, CHOICE_FIELDS) for i in choices], 'scoresheets': [i.as_simplified_dict() for i in self.scoresheets()] } jsonstring = json.dumps(output, indent=4) return "{}".format(jsonstring) def reply_count(self): return self.reply_set.all().count() @contract def as_markdown(self): """A helper to convert an asker and associated models to markdown format. :rtype: string """ _fields = "slug name steps_are_sequential redirect_url finish_on_last_page \ show_progress success_message step_navigation system_audio width".split( ) metayaml = yaml.safe_dump({i: getattr(self, i) for i in _fields}, default_flow_style=False) _pages_questions = [(i.as_markdown(), [j.as_markdown() for j in i.get_questions()]) for i in self.askpage_set.all()] qstrings = "\n\n".join(flatten(flatten(_pages_questions))) return "---\n" + metayaml + "---\n\n" + qstrings class Meta: permissions = (("can_preview", "Can preview surveys"), ) verbose_name = "Questionnaire" app_label = "ask" ordering = ['name'] def save(self, *args, **kwargs): # set defaults for system_audio if none exist if self.system_audio: if isinstance(self.system_audio, dict): IVR_SYSTEM_MESSAGES.update(self.system_audio) self.system_audio = IVR_SYSTEM_MESSAGES super(Asker, self).save(*args, **kwargs) def __unicode__(self): return "{}".format(self.name or self.slug)
class Robot(models.Model): # basic name = models.CharField( blank=True, max_length=64, verbose_name=_('name'), help_text=_('Name of the robot.'), ) description = models.TextField(blank=True, verbose_name=_('description')) # owner user = models.ForeignKey( User, blank=True, null=True, related_name='robots', on_delete=models.SET_NULL, verbose_name=_('user'), help_text=_('Owner of the robot.'), ) data = YAMLField(blank=True, verbose_name=_('data')) source = models.BinaryField(verbose_name=_('source')) content_type = models.CharField( max_length=64, default='application/xml', choices=( ('application/xml', 'application/xml'), ('application/zip', 'application/zip'), ), verbose_name=_('content type'), ) # meta created = models.DateTimeField(auto_now_add=True, verbose_name=_('created')) modified = models.DateTimeField(auto_now=True, verbose_name=_('modified')) class Meta: verbose_name = _('robot') verbose_name_plural = _('robots') def __unicode__(self): return self.name def to_dict(self, is_all=False): data = {} data.update(self.data) data.update({ 'id': self.id, 'name': self.name, 'description': self.description, 'user': self.user_id, 'content_type': self.content_type, 'created': self.created, 'modified': self.modified, }) if is_all: data['source'] = base64.b64encode(self.source) return data
class BaseFeed( six.with_metaclass(BaseFeedMeta, PolymorphicModel, AbstractDuplicateAwareModel, AbstractMultipleLanguagesModel, AbstractTaggedModel, DiffMixin)): """ Base 1flow feed. .. todo:: date_added → date_created created_by → user restricted → is_restricted closed → is_active last_fetch → date_last_fetch good_for_use → is_good errors : ListField(StringField) → JSONField """ # This should be overriden by subclasses if needed. REFRESH_LOCK_INTERVAL = None # By default, feeds continue to be fetched, # even if no user is subscribed to them. AUTO_CLOSE_WHEN_NO_SUBSCRIPTION_LEFT = False class Meta: app_label = 'core' translate = ( 'short_description', 'description', ) verbose_name = _(u'Base feed') verbose_name_plural = _(u'Base feeds') INPLACEEDIT_EXCLUDE = ( 'errors', 'options', ) # ———————————————————————————————————————————————————————————————— Managers objects = BaseFeedManager() # # BIG HEADS UP: for an unknown reason, if I define both managers, Django # picks the `good` as the default one, which is not what we # want at all, and goes against the documentation which says # the first defined will be the default one. Thus for now, # both are deactivated and we use only one, that offers # multiple filters at the QuerySet level. # # add the default polymorphic manager first # objects = models.Manager() # objects = PolymorphicManager() # good_feeds = GoodFeedsManager() # —————————————————————————————————————————————————————————————— Attributes # NOTE: keep ID a simple integer/auto # field, this surely helps JOIN operations. # id = models.UUIDField(primary_key=True, # default=uuid.uuid4, editable=False) # We use `.user` attribute name for writing-easyness of permissions classes, # But really the `.user` attribute has different roles. Please always refer # to its verbose_name to exactly know what it is really. user = models.ForeignKey(User, null=True, blank=True, verbose_name=_(u'Creator'), related_name='feeds') name = models.CharField(verbose_name=_(u'name'), null=True, blank=True, max_length=255) slug = models.CharField(verbose_name=_(u'slug'), max_length=255, null=True, blank=True) items = models.ManyToManyField(BaseItem, blank=True, null=True, verbose_name=_(u'Feed items'), related_name='feeds') date_created = models.DateTimeField(auto_now_add=True, blank=True, verbose_name=_(u'Date created')) is_internal = models.BooleanField(verbose_name=_(u'Internal'), blank=True, default=False) is_restricted = models.BooleanField( default=False, verbose_name=_(u'restricted'), blank=True, help_text=_(u'Is this feed available only to paid subscribers on its ' u'publisher\'s web site?')) is_active = models.BooleanField( verbose_name=_(u'active'), default=True, blank=True, help_text=_(u'Is the feed refreshed or dead?')) date_closed = models.DateTimeField(verbose_name=_(u'date closed'), null=True, blank=True) closed_reason = models.TextField(verbose_name=_(u'closed reason'), null=True, blank=True) fetch_interval = models.IntegerField( default=config.FEED_FETCH_DEFAULT_INTERVAL, verbose_name=_(u'fetch interval'), blank=True) date_last_fetch = models.DateTimeField(verbose_name=_(u'last fetch'), null=True, blank=True, db_index=True) errors = JSONField(default=list, blank=True) options = JSONField(default=dict, blank=True) notes = models.TextField( verbose_name=_(u'Notes'), null=True, blank=True, help_text=_(u'Internal notes for 1flow staff related to this feed.')) is_good = models.BooleanField( verbose_name=_(u'Shown in selector'), default=False, db_index=True, help_text=_(u'Make this feed available to new subscribers in the ' u'selector wizard. Without this, the user can still ' u'subscribe but he must know it and manually enter ' u'the feed address.')) thumbnail = models.ImageField( verbose_name=_(u'Thumbnail'), null=True, blank=True, upload_to=get_feed_thumbnail_upload_path, max_length=256, help_text=_(u'Use either thumbnail when 1flow instance hosts the ' u'image, or thumbnail_url when hosted elsewhere. If ' u'both are filled, thumbnail takes precedence.')) thumbnail_url = models.URLField( verbose_name=_(u'Thumbnail URL'), null=True, blank=True, max_length=384, help_text=_(u'Full URL of the thumbnail displayed in the feed ' u'selector. Can be hosted outside of 1flow.')) short_description = models.CharField( null=True, blank=True, max_length=256, verbose_name=_(u'Short description'), help_text=_(u'Public short description of the feed, for ' u'auto-completer listing. Markdown text.')) description = models.TextField( null=True, blank=True, verbose_name=_(u'Description'), help_text=_(u'Public description of the feed. Markdown text.')) processing_chain = models.ForeignKey(ProcessingChain, null=True, blank=True, related_name='feeds') processing_parameters = YAMLField( null=True, blank=True, verbose_name=_(u'Processing parameters'), help_text=_(u'Processing parameters for this Feed. ' u'Can be left empty. As they are more specific, ' u'the feed parameters take precedence over the ' u'processors parameters, but will be overriden by ' u'item-level processing parameters, if any. In YAML ' u'format (see ' u'http://en.wikipedia.org/wiki/YAML for details).')) # ——————————————————————————————————————————— Cached descriptors & updaters # TODO: create an abstract class that will allow to not specify # the attr_name here, but make it automatically created. # This is an underlying implementation detail and doesn't # belong here. latest_item_date_published = DatetimeRedisDescriptor( # 5 years ealier should suffice to get old posts when starting import. attr_name='bf.la_dp', default=now() - timedelta(days=1826)) all_items_count = IntRedisDescriptor( attr_name='bf.ai_c', default=basefeed_all_items_count_default, set_default=True, min_value=0) good_items_count = IntRedisDescriptor( attr_name='bf.gi_c', default=basefeed_good_items_count_default, set_default=True, min_value=0) bad_items_count = IntRedisDescriptor( attr_name='bf.bi_c', default=basefeed_bad_items_count_default, set_default=True, min_value=0) recent_items_count = IntRedisDescriptor( attr_name='bf.ri_c', default=basefeed_recent_items_count_default, set_default=True, min_value=0) subscriptions_count = IntRedisDescriptor( attr_name='bf.s_c', default=basefeed_subscriptions_count_default, set_default=True, min_value=0) def update_latest_item_date_published(self): """ This seems simple, but this operations costs a lot. """ try: # This query should still cost less than the pure and bare # `self.latest_article.date_published` which will first sort # all articles of the feed before getting the first of them. self.latest_item_date_published = self.recent_items.order_by( '-date_published').first().date_published except: # Don't worry, the default value of # the descriptor should fill the gaps. pass def update_all_items_count(self): self.all_items_count = basefeed_all_items_count_default(self) def update_good_items_count(self): self.good_items_count = basefeed_good_items_count_default(self) def update_bad_items_count(self): self.bad_items_count = basefeed_bad_items_count_default(self) def update_subscriptions_count(self): self.subscriptions_count = basefeed_subscriptions_count_default(self) def update_recent_items_count(self, force=False): """ This task is protected to run only once per day, even if is called more. """ urac_lock = RedisExpiringLock(self, lock_name='urac', expire_time=86100) if urac_lock.acquire() or force: self.recent_items_count = self.recent_items.count() elif not force: LOGGER.warning( u'No more than one update_recent_items_count ' u'per day (feed %s).', self) # # Don't bother release the lock, this will # ensure we are not called until tomorrow. # def compute_cached_descriptors(self): self.update_all_items_count() self.update_good_items_count() self.update_bad_items_count() self.update_subscriptions_count() self.update_recent_items_count() # This one costs a lot. # self.update_latest_item_date_published() # ————————————————————————————————————————————————————— Articles properties @property def date_next_fetch(self): """ Return next fetch datetime. It is computed from date_last_fetch and fetch_interval. """ interval_days = self.fetch_interval / 86400 if interval_days: interval_seconds = self.fetch_interval - interval_days * 86400 else: interval_seconds = self.fetch_interval interval = timedelta(days=interval_days, seconds=interval_seconds) return self.date_last_fetch + interval @property def recent_items(self): return self.good_items.filter( date_published__gt=today() - timedelta(days=config.FEED_ADMIN_MEANINGFUL_DELTA)) @property def can_be_refreshed(self): """ A Base feed can be refreshed if it is not internal. Return a bool. This property should be called in subclasses. """ return not self.is_internal @property def native_items(self): """ Return items whose model is related to the feed model. This method is meant to be overriden by polymorphic inherited models. """ return self.items.all() @property def good_items(self): """ Subscriptions should always use :attr:`good_items` to give to users only useful content for them, whereas :class:`Feed` will use :attr:`articles` or :attr:`all_items` to reflect real numbers. """ # # NOTE: sync the conditions with @Article.is_good # and invert them in @BaseFeed.bad_items # return self.items.filter(duplicate_of=None) \ & self.items.instance_of(Article).filter( Article___url_absolute=True) \ | self.items.not_instance_of(Article) @property def bad_items(self): # # NOTE: invert these conditions in @Feed.good_items # return self.items.filter( Q(Article___url_absolute=False) | ~Q(duplicate_of_id=None)) # NOTE for myself: these property & method are provided by Django # bye-bye MongoDB glue code everywhere to mimic relational DB. # # @property # def articles(self): # """ A simple version of :meth:`get_items`. """ # return Article.objects(feeds__contains=self) # # def get_items(self, limit=None): # """ A parameter-able version of the :attr:`articles` property. """ # # if limit: # return self.items.order_by('-date_published').limit(limit) # # return self.items.order_by('-date_published') # —————————————————————————————————————————————————————— Django & Grappelli def __unicode__(self): """ Hello, pep257. I love you so. """ return _(u'BaseFeed {0} (#{1})').format(self.name, self.id) @staticmethod def autocomplete_search_fields(): """ grappelli auto-complete method. """ return ('name__icontains', ) @property def refresh_lock(self): try: return self.__refresh_lock except AttributeError: self.__refresh_lock = RedisExpiringLock( self, lock_name='fetch', expire_time=self.REFRESH_LOCK_INTERVAL or self.fetch_interval) return self.__refresh_lock # —————————————————————————————————————————————————————————— Internal utils def has_option(self, option): """ True if option in self.options. """ return option in self.options def reopen(self, message=None, verbose=True, commit=True): """ Reopen the feed, clearing errors, date closed, etc. """ self.errors = [] self.is_active = True self.date_closed = now() self.closed_reason = u'Reopen on %s' % now().isoformat() if commit: self.save() statsd.gauge('feeds.counts.open', 1, delta=True) if verbose: if message is None: LOGGER.info(u'%s %s: %sre-opened.', self._meta.verbose_name, self.id, u'' if commit else u'temporarily ') else: LOGGER.info(u'%s %s: %s', self._meta.verbose_name, self.id, message) def close(self, reason=None, commit=True): """ Close the feed with or without a reason. """ self.is_active = False self.date_closed = now() self.closed_reason = reason or _(u'NO REASON GIVEN') if commit: self.save() statsd.gauge('feeds.counts.open', -1, delta=True) LOGGER.warning(u'%s %s: closed with reason “%s”.', self._meta.verbose_name, self.id, self.closed_reason) def check_old_closed(self): """ Try to reopen a feed, let it closed if it fails a refresh. """ if self.is_active: LOGGER.warning(u'%s %s: already active, check aborted.', self._meta.verbose_name, self.id) return old_reason = self.closed_reason old_date = self.date_closed old_errors = self.errors # intentionally not kept and overwriten # during current exectution. See below. # old_fetch = self.date_last_fetch # Don't commit, this will avoid save(), # and thus the feed beiing picked up # accidentally by the global refresher task. self.reopen(commit=False) # put back all errors, so that one more # suffices to re-close the feed immediately. self.errors = old_errors # Refresh here (not in a task), to keep control # on the commit=False and not write anything in # the DB while not sure of the reopenable status. # Use force to avoid interval throttling. self.refresh(force=True, commit=False) # Reopened or not, date_last_fetch was updated. # This is intended to manually verify the feed # is checked/refreshed every month. # A failing refresh would have called close(), # or at least error() that would have closed # in turn because errors count was already max. if self.is_active: LOGGER.info(u'%s %s: brought back to life.', self._meta.verbose_name, self.id) self.save() return True # Refresh failed. Keep the feed closed # with original reasons/errors. self.closed_reason = old_reason self.date_closed = old_date self.errors = old_errors self.save() return False def error(self, message, commit=True, last_fetch=False): """ Take note of an error. If the maximum number of errors is reached, close the feed and return ``True``; else just return ``False``. :param last_fetch: as a commodity, set this to ``True`` if you want this method to update the :attr:`last_fetch` attribute with the value of ``now()`` (UTC). Default: ``False``. :param commit: as in any other Django DB-related method, set this to ``False`` if you don't want this method to call ``self.save()``. Default: ``True``. """ LOGGER.error(u'Error on feed %s: %s.', self, message) error_message = u'{0} @@{1}'.format(message, now().isoformat()) # Put the errors more recent first. self.errors.insert(0, error_message) if last_fetch: self.date_last_fetch = now() retval = False if len(self.errors) >= config.FEED_FETCH_MAX_ERRORS: if self.is_active: self.close(u'Too many errors on the feed. Last was: %s' % self.errors[0], commit=False) # LOGGER.critical(u'Too many errors on feed %s, closed.', self) # Keep only the most recent errors. self.errors = self.errors[:config.FEED_FETCH_MAX_ERRORS] retval = True if commit: self.save() statsd.incr('feeds.refresh.global.errors') return retval # —————————————————————————————————————————————————————— High-level methods def refresh_must_abort(self, force=False, commit=True): """ Returns ``True`` if one or more abort conditions is met. Checks the feed cache lock, the ``last_fetch`` date, etc. """ if not self.is_active: LOGGER.info(u'%s %s: is currently inactive, refresh aborted.', self._meta.verbose_name, self.id) return True if self.is_internal: LOGGER.info(u'%s %s: beiing internal, no need to refresh.', self._meta.verbose_name, self.id) return True if config.FEED_FETCH_DISABLED: # we do not raise .retry() because the global refresh # task will call us again anyway at next global check. LOGGER.info(u'%s %s: refresh disabled by configuration.', self._meta.verbose_name, self.id) return True try: if self.refresh_must_abort_internal(): return True except AttributeError: pass # ———————————————————————————————————————————————— Try to acquire lock if not self.refresh_lock.acquire(): if force: LOGGER.warning(u'%s %s: forcing refresh unlocking.', self._meta.verbose_name, self.id) self.refresh_lock.release() self.refresh_lock.acquire() else: LOGGER.info(u'%s %s: refresh already locked, aborting.', self._meta.verbose_name, self.id) return True if self.date_last_fetch is not None and self.date_last_fetch >= ( now() - timedelta(seconds=self.fetch_interval)): if force: LOGGER.warning( u'%s %s: forcing refresh despite recently ' u'fetched.', self._meta.verbose_name, self.id) else: LOGGER.info(u'%s %s: last refresh too recent, aborting.', self._meta.verbose_name, self.id) return True return False def refresh(self, force=False, commit=True): """ Look for new content in a 1flow feed. """ # HEADS UP: refresh_must_abort() has already acquire()'d our lock. if self.refresh_must_abort(force=force): self.refresh_lock.release() return preventive_slow_down = False try: data = self.refresh_feed_internal(force=force, commit=commit) except: LOGGER.exception( u'Could not refresh feed %s, operating ' u'preventive slowdown.', self) preventive_slow_down = True else: if data is None: # An error occured and has already been stored. The feed # has eventually already been closed if too many errors. # In case it's still open, slow down things. preventive_slow_down = True elif data is True: # The feed is handling its internals on his own behalf. # Eg. a Twitter feed will tweak self.last_fetch anyhow # it needs to prevent quota overflows. Just let it go. return if preventive_slow_down: # do not the queue be overflowed by refresh_all_feeds() # checking this feed over and over again. Let the lock # expire slowly until fetch_interval. # # self.refresh_lock.release() # Artificially slow down things to let the remote site # eventually recover while not bothering us too much. if not force: self.throttle_fetch_interval(0, 0, 1) self.update_last_fetch() if commit: self.save() return new_items, duplicates, mutualized = data if new_items == duplicates == mutualized == 0: with statsd.pipeline() as spipe: spipe.incr('feeds.refresh.fetch.global.unchanged') else: with statsd.pipeline() as spipe: spipe.incr('feeds.refresh.fetch.global.updated') if not force: # forcing the refresh is most often triggered by admins # and developers. It should not trigger the adaptative # throttling computations, because it generates a lot # of false-positive duplicates. self.throttle_fetch_interval(new_items, mutualized, duplicates) with statsd.pipeline() as spipe: spipe.incr('feeds.refresh.global.fetched', new_items) spipe.incr('feeds.refresh.global.duplicates', duplicates) spipe.incr('feeds.refresh.global.mutualized', mutualized) # Everything went fine, be sure to reset the "error counter". self.errors = [] self.update_last_fetch() if commit: self.save() with statsd.pipeline() as spipe: spipe.incr('feeds.refresh.fetch.global.done') # As the last_fetch is now up-to-date, we can release the fetch lock. # If any other refresh job comes, it will check last_fetch and will # terminate if called too early. self.refresh_lock.release() def update_last_fetch(self): """ Allow to customize the last fetch datetime. This method exists to be overriden by “under development” classes, to allow not updating the last_fetch attribute, and continue fetching data forever on development machines. """ self.date_last_fetch = now() def throttling_method(self, new_items, mutualized, duplicates): """ Calls throttle_fetch_interval() barely. This method can be safely overriden by subclasses to compute the new fetch interval with better precision. See the :class:`RssAtomFeed` class for a specific implementation. """ return throttle_fetch_interval(self.fetch_interval, new_items, mutualized, duplicates) def throttle_fetch_interval(self, new_items, mutualized, duplicates): """ Compute a new fetch interval. """ new_interval = self.throttling_method(new_items, mutualized, duplicates) if new_interval != self.fetch_interval: LOGGER.info( u'Fetch interval changed from %s to %s ' u'for feed %s (%s new article(s), %s ' u'duplicate(s)).', self.fetch_interval, new_interval, self, new_items, duplicates) self.fetch_interval = new_interval
class ChainedItem(models.Model): """ parameters of a given item in a given chain. Processors and other chains can appear in any chain at any position. This depends on the complexity of the parent chain. The current model holds the position and the optional arguments of all chained items. """ __metaclass__ = TransMeta class Meta: app_label = 'core' verbose_name = _(u'Chained item') verbose_name_plural = _(u'Chained items') translate = ('notes', ) objects = ChainedItemManager() chain = models.ForeignKey(ProcessingChain, related_name='chained_items') item_type = models.ForeignKey( ContentType, null=True, blank=True, limit_choices_to=( models.Q(app_label='core') & ( # HEADS UP: “Processor” (title case, model name) does # not find anything. “processor” (lowercase) works. See: # # ContentType.objects.filter( # app_label='core').values_list( # 'model', flat=True) # # For a complete list of valid values. models.Q(model='processor') | models.Q(model='processingchain') ) ) ) item_id = models.PositiveIntegerField(null=True, blank=True) item = generic.GenericForeignKey('item_type', 'item_id') position = PositionField(collection=('chain', ), default=0, blank=True) is_active = models.BooleanField(default=True) parameters = YAMLField( null=True, blank=True, verbose_name=_(u'Processing parameters'), help_text=_(u'Parameters for this processor, in this chain, at ' u'this position. Can be left empty if the processor ' u'parameters are optional. Can be overridden, by ' u'order of importance, by website, feed or item-level ' u'processing parameters. In YAML format (see ' u'http://en.wikipedia.org/wiki/YAML for details).')) is_valid = models.BooleanField(verbose_name=_(u'Checked and valid'), default=True, blank=True) check_error = models.CharField(max_length=255, null=True, blank=True) notes = models.TextField( null=True, blank=True, verbose_name=_(u'Notes'), help_text=_(u'Things to know about this processor, ' u'in this chain, at this position.')) # ————————————————————————————————————————————————————————— Python & Django def __unicode__(self): """ I'm __unicode__, pep257. """ return ( u'Chain {0} pos. {1}: {2} {3} (#{4})'.format( self.chain.slug, self.position, self.item._meta.verbose_name, self.item.slug, self.id) ) def natural_key(self): """ This is needed for serialization. """ return (self.chain, self.position)
class Question(models.Model): """Question objects; e.g. mutliple choice, text, etc.""" def check_if_protected(self): if not self.modifiable(): raise DataProtectionException("""This question already has answers attached to it and can't be modified.""") def modifiable(self): nonpreviewanswercount = self.answer_set.exclude( reply__entry_method="preview").count() # if we have non-preview answers, don't allow the question to be edited return not nonpreviewanswercount def delete(self, *args, **kwargs): # we delete preview answers to this questions to avoid ProtectedErrors # when deleting questions through the admin or in the yaml interface self.answer_set.filter(reply__entry_method="preview").delete() self.check_if_protected() super(Question, self).delete(*args, **kwargs) def save(self, *args, **kwargs): super(Question, self).save(*args, **kwargs) def index(self): return self.page.asker.questions().index(self) def dict_for_dataframe(self): out = {"variable_name": self.variable_name, "text": self.text} if self.choiceset: dl = list(map(dict, [v for k, v in self.choiceset.yaml.items()])) out.update({ "labels": [i['label'] for i in dl], "scores": [i['score'] for i in dl] }) return out def dict_for_yaml(self): d = { "text": self.text, "q_type": self.q_type, "choiceset": supergetattr(self, "choiceset.name", None), "required": self.required, } return {self.variable_name: {k: v for k, v in list(d.items()) if v}} page = models.ForeignKey('ask.AskPage', null=True, blank=True) scoresheet = models.ForeignKey('signalbox.ScoreSheet', blank=True, null=True) objects = QuestionManager() def natural_key(self): return (self.variable_name, ) natural_key.dependencies = ['ask.choiceset', 'ask.AskPage'] order = models.IntegerField( default=-1, verbose_name="Page order", help_text="""The order in which items will apear in the page.""") allow_not_applicable = models.BooleanField(default=False) required = models.BooleanField(default=False) @contract def show_conditional(self, mapping_of_answers): """ Use pyparsing to match an 'if' keyval on questions. :type mapping_of_answers: dict :rtype: bool """ try: condition = self.extra_attrs.get('if', None) except AttributeError: condition = None show = parse_conditional(condition, mapping_of_answers) return show text = models.TextField(blank=True, null=True, help_text=safe_help( settings.QUESTION_TEXT_HELP_TEXT)) variable_name = models.SlugField( default="", max_length=32, unique=True, validators=[ valid.first_char_is_alpha, valid.illegal_characters, valid.is_lower ], help_text="""Variable names can use characters a-Z, 0-9 and underscore (_), and must be unique within the system.""") choiceset = models.ForeignKey('ask.ChoiceSet', null=True, blank=True) help_text = models.TextField(blank=True, null=True) javascript = models.TextField(blank=True, null=True) @contract def show_as_image_data_url(self): """Say whether the answer is a stored data url for an image. :rtype: bool """ if self.q_type == "webcam": return True return False @contract def display_text(self, reply=None, request=None): """ :type reply: is_reply|None :type request: c|None :return: The html formatted string displaying the question :rtype: string """ templ_header = r"""{% load humanize %}{% load mathfilters %}""" # include these templatetags # in render context templ = Template(templ_header + self.text) context = { 'reply': reply, 'condition': supergetattr(reply, "observation.dyad.condition"), 'user': supergetattr(reply, 'observation.dyad.user', default=None), 'page': self.page, 'scores': {}, 'answers': defaultdict(None), 'answers_label': {} } fc = self.field_class() if reply and reply.asker and fc.compute_scores: context['scores'] = self.page.summary_scores(reply) # put actual values into main namespace too context.update({ k: v.get('score', None) for k, v in list(context['scores'].items()) }) if fc.allow_showing_answers and reply: context['answers'] = { i.variable_name(): int_or_string(i.answer) for i in reply.answer_set.all() } for i in self.questionasset_set.all(): context[i.slug] = str(i) from django.template.base import VariableDoesNotExist try: return markdown.markdown(templ.render(Context(context))) except VariableDoesNotExist: return markdown.markdown(self.text) q_type = models.CharField(choices=[(i, i) for i in FIELD_NAMES], blank=False, max_length=100, default="instruction") def field_class(self): """Return the relevant form field Class""" return getattr(fields, fields.class_name(self.q_type)) def preview_html(self): """Return a form with a single question which can be used to preview questions -> PageForm""" from ask.forms import PageForm # yuck but circular imports request = http.HttpRequest() form = PageForm(None, None, page=None, questions=[self], reply=None, request=request) return form def label_variable(self): return self.field_class().label_variable(self) def set_format(self): return self.field_class().set_format(self) def label_choices(self): return self.field_class().label_choices(self) extra_attrs = YAMLField( blank=True, help_text="""A YAML representation of a python dictionary of attributes which, when deserialised, is passed to the form widget when the questionnaire is rendered. See django-floppyforms docs for options.""") def extra_attrs_as_yaml(self): return yaml.dump(self.extra_attrs, default_flow_style=False) def voice_function(self): """Returns the function to render instructions for external telephony API.""" return self.field_class().voice_function def choices_as_json(self): """Question -> String""" return self.choiceset and self.choiceset.values_as_json() or "" @contract def choices(self): """ :rtype: None|list(tuple) """ return (self.choiceset and self.choiceset.choice_tuples()) or None def response_possible(self): """:: Question -> Bool Indicates whether a user response is possible for this Question""" return self.field_class().response_possible @contract def check_telephone_keypad_answer(self, raw_response_as_string): """Check a user response to see if it is allowed by the question. :type raw_response_as_string: string :rtype: bool """ if not self.response_possible(): # if no response expected we don't care what they pressed return True if self.choiceset: try: return bool( int(raw_response_as_string) in self.choiceset.allowed_responses()) except ValueError: # can't case string to an int, so not allowed return False # If we have no choiceset specified then anything is allowed return True @contract def previous_answer(self, reply): """ Return the an earlier answer if one exists for this reply. :rtype: string|None """ from signalbox.models import Answer # yuck, but otherwise circular import hell if not self.response_possible() or not reply: return None try: return reply.answer_set.get(question=self, page=self.page).answer except Answer.DoesNotExist: return "" def __unicode__(self): return truncatelabel(self.variable_name, 26) MARKDOWN_FORMAT = """~~~{{{variable_name} {classes} {keyvals}}}\n{text}\n{details}\n~~~""" def as_markdown(self): iden = "#" + self.variable_name if self.extra_attrs: classes = self.extra_attrs.pop('classes', {}) classesstring = " ".join( [".{}".format(k) for k, v in list(classes.items()) if v]) else: classesstring = ".{}".format(self.q_type) keyvals = self.extra_attrs or {} keyvalsstring = " ".join( ["""{}="{}\"""".format(k, v) for k, v in list(keyvals.items())]) detailsstring = "" if self.choiceset: detailsstring = ">>>\n" + self.choiceset.as_markdown() elif self.scoresheet: detailsstring = ">>>\n" + self.scoresheet.as_markdown() return self.MARKDOWN_FORMAT.format( **{ 'variable_name': iden, 'classes': classesstring, 'keyvals': keyvalsstring, 'text': self.text, 'details': detailsstring }) def clean(self, *args, **kwargs): self.variable_name = self.variable_name.replace("-", "_") super(Question, self).clean(*args, **kwargs) def clean_fields(self, *args, **kwargs): """Do some extra validation related to form fields used for the question.""" super(Question, self).clean_fields(*args, **kwargs) if not self.q_type: return False fieldclass = self.field_class() errors = {} if self.order is -1: self.order = 1 + self.page.question_set.all().count() if self.q_type in "slider" and not self.widget_kwargs: errors['widget_kwargs'] = [ """No default settings for slider added (need min, max, value). E.g. {"value":50,"min":0,"max":100}""" ] if "range-slider" in self.q_type and not self.widget_kwargs: errors['widget_kwargs'] = [ """Default settings for slider added (need values [a,b], min, max). E.g. {"values":[40,60],"min":0,"max":100}. Optional extra attributes: {"units":"%" } """ ] if (not fieldclass.has_choices) and self.choiceset: errors['choiceset'] = [ "You don't need a choiceset for this type of question." ] if fieldclass.has_choices and (not self.choiceset): errors['choiceset'] = [ "You need a choiceset for this type of question." ] if self.required and (not fieldclass.response_possible): errors['required'] = [ "This type of question (%s) doesn't allow a response." % (self.q_type, ) ] if errors: raise ValidationError(errors) def admin_edit_url(self): return admin_edit_url(self) class Meta: app_label = 'ask' ordering = ['order']