Example #1
0
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()
Example #2
0
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
Example #3
0
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}
Example #4
0
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
Example #6
0
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"
Example #7
0
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
Example #8
0
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()
Example #9
0
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')
Example #10
0
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
Example #11
0
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))
Example #12
0
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
Example #13
0
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__()
Example #14
0
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()
Example #15
0
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']
Example #16
0
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
Example #17
0
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
Example #18
0
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 '-'
Example #19
0
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)
Example #20
0
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']
Example #21
0
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"]
Example #22
0
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))
Example #23
0
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,
        )
Example #24
0
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()
Example #25
0
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
Example #26
0
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)
Example #27
0
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
Example #28
0
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
Example #29
0
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)
Example #30
0
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']