示例#1
0
class StereotypeVote(models.Model):
    """
    Similar to vote, but it is not associated with a comment.

    It forms a m2m relationship between Stereotypes and comments.
    """

    author = models.ForeignKey(
        "Stereotype", related_name="votes", on_delete=models.CASCADE
    )
    comment = models.ForeignKey(
        "ej_conversations.Comment",
        verbose_name=_("Comment"),
        related_name="stereotype_votes",
        on_delete=models.CASCADE,
    )
    choice = EnumField(Choice, _("Choice"))
    stereotype = alias("author")
    objects = StereotypeVoteQuerySet.as_manager()

    class Meta:
        unique_together = [("author", "comment")]

    def __str__(self):
        return f"StereotypeVote({self.author}, value={self.choice})"
示例#2
0
class Vote(models.Model):
    """
    A single vote cast for a comment.
    """

    author = models.ForeignKey(
        settings.AUTH_USER_MODEL, related_name="votes", on_delete=models.PROTECT
    )
    comment = models.ForeignKey(
        "Comment", related_name="votes", on_delete=models.CASCADE
    )
    choice = EnumField(Choice, _("Choice"), help_text=_("Agree, disagree or skip"))
    created = models.DateTimeField(_("Created at"), auto_now_add=True)
    objects = VoteQuerySet.as_manager()

    class Meta:
        unique_together = ("author", "comment")
        ordering = ["id"]

    def __str__(self):
        comment = truncate(self.comment.content, 40)
        return f"{self.author} - {self.choice.name} ({comment})"

    def clean(self, *args, **kwargs):
        if self.comment.is_pending:
            msg = _("non-moderated comments cannot receive votes")
            raise ValidationError(msg)
示例#3
0
class Vote(models.Model):
    """
    A single vote cast for a comment.
    """
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name='votes',
        on_delete=models.PROTECT,
    )
    comment = models.ForeignKey(
        'Comment',
        related_name='votes',
        on_delete=models.CASCADE,
    )
    choice = EnumField(Choice,
                       _('Choice'),
                       help_text=_('Agree, disagree or skip'))
    created = models.DateTimeField(_('Created at'), auto_now_add=True)

    class Meta:
        unique_together = ('author', 'comment')

    def clean(self, *args, **kwargs):
        if self.comment.is_pending:
            msg = _('non-moderated comments cannot receive votes')
            raise ValidationError(msg)
示例#4
0
class Fragment(models.Model):
    """
    Configurable HTML fragments that can be inserted in pages.
    """

    ref = models.CharField(
        _("Identifier"),
        max_length=100,
        unique=True,
        db_index=True,
        help_text=_("Unique identifier for fragment name"),
    )
    title = models.CharField(
        max_length=100,
        blank=True,
        help_text=_(
            "Optional description that helps humans identify the content and "
            "role of the fragment."),
    )
    format = EnumField(Format,
                       _("Format"),
                       help_text=_("Defines how content is interpreted."))
    content = models.TextField(
        _("content"),
        blank=True,
        help_text=_("Raw fragment content in HTML or Markdown"),
    )
    editable = models.BooleanField(default=True, editable=False)

    def __str__(self):
        return self.ref

    def __html__(self):
        return str(self.render())

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        invalidate_cache(self.ref)

    def delete(self, using=None, keep_parents=False):
        super().delete(using, keep_parents)
        invalidate_cache(self.ref)

    def lock(self):
        """
        Prevents fragment from being deleted on the admin.
        """
        FragmentLock.objects.update_or_create(fragment=self)

    def unlock(self):
        """
        Allows fragment being deleted.
        """
        FragmentLock.objects.filter(fragment=self).delete()

    def render(self, request=None, **kwargs):
        """Render element to HTML"""
        return self.format.render(self.content, request, kwargs)
示例#5
0
class Fragment(models.Model):
    """
    Configurable HTML fragments that can be inserted in pages.
    """

    name = models.CharField(
        _('Name'),
        max_length=100,
        unique=True,
        db_index=True,
        help_text=_('Unique identifier for fragment name'),
    )
    format = EnumField(Format)
    content = models.TextField(
        _('content'),
        blank=True,
        help_text=_('Raw fragment content in HTML or Markdown'),
    )
    editable = models.BooleanField(
        default=True,
        editable=False,
    )

    def __html__(self):
        return self.html().__html__()

    def __str__(self):
        return self.name

    def lock(self):
        """
        Prevents fragment from being deleted.
        """
        FragmentLock.objects.update_or_create(fragment=self)

    def unlock(self):
        """
        Allows fragment being deleted.
        """
        FragmentLock.objects.filter(fragment=self).delete()

    def html(self, classes=()):
        if self.format == Format.HTML:
            data = sanitize_html(self.content)
        elif self.format == Format.MARKDOWN:
            data = markdown(self.content)
        text = Text(data, escape=False)
        return div(text, class_=classes)
示例#6
0
class StereotypeVote(models.Model):
    """
    Similar to vote, but it is not associated with a comment.

    It forms a m2m relationship between Stereotypes and comments.
    """
    author = models.ForeignKey(
        'Stereotype',
        related_name='votes',
        on_delete=models.CASCADE,
    )
    comment = models.ForeignKey(
        'ej_conversations.Comment',
        related_name='stereotype_votes',
        on_delete=models.CASCADE,
    )
    choice = EnumField(Choice)
    objects = BoogieManager()

    def __str__(self):
        return f'StereotypeVote({self.stereotype}, value={self.value})'
示例#7
0
class Clusterization(TimeStampedModel):
    """
    Manages clusterization tasks for a given conversation.
    """
    conversation = models.OneToOneField(
        'ej_conversations.Conversation',
        on_delete=models.CASCADE,
        related_name='clusterization',
    )
    cluster_status = EnumField(
        ClusterStatus,
        default=ClusterStatus.PENDING_DATA,
    )
    unprocessed_votes = models.PositiveSmallIntegerField(
        default=0,
        editable=False,
    )
    unprocessed_comments = models.PositiveSmallIntegerField(
        default=0,
        editable=False,
    )

    @property
    def stereotypes(self):
        return (
            Stereotype.objects
                .filter(clusters__in=self.clusters.all())
        )

    class Meta:
        ordering = ['conversation_id']

    def __str__(self):
        clusters = self.clusters.count()
        return f'{self.conversation} ({clusters} clusters)'

    def get_absolute_url(self):
        args = {'conversation': self.conversation}
        return reverse('cluster:index', kwargs=args)

    def update(self, commit=True):
        """
        Update clusters if necessary.
        """
        if self.requires_update():
            self.force_update(commit=False)
            if self.cluster_status == ClusterStatus.PENDING_DATA:
                self.cluster_status = ClusterStatus.ACTIVE
        if commit:
            self.save()

    def force_update(self, commit=True):
        """
        Force a cluster update.

        Used internally by .update() when an update is necessary.
        """
        log.info(f'[clusters] updating cluster: {self.conversation}')

        math.update_clusters(self.conversation, self.clusters.all())
        self.unprocessed_comments = 0
        self.unprocessed_votes = 0
        if commit:
            self.save()

    def requires_update(self):
        """
        Check if update should be recomputed.
        """
        conversation = self.conversation
        if self.cluster_status == ClusterStatus.PENDING_DATA:
            rule = rules.get_rule('ej_clusters.conversation_has_sufficient_data')
            if not rule.test(conversation):
                log.info(f'[clusters] {conversation}: not enough data to start clusterization')
                return False
        elif self.cluster_status == ClusterStatus.DISABLED:
            return False

        rule = rules.get_rule('ej_clusters.must_update_clusters')
        return rule.test(conversation)
示例#8
0
class Clusterization(TimeStampedModel):
    """
    Manages clusterization tasks for a given conversation.
    """

    conversation = models.OneToOneField(
        "ej_conversations.Conversation",
        on_delete=models.CASCADE,
        related_name="clusterization",
    )
    cluster_status = EnumField(ClusterStatus, default=ClusterStatus.PENDING_DATA)
    pending_comments = models.ManyToManyField(
        "ej_conversations.Comment",
        related_name="pending_in_clusterizations",
        editable=False,
        blank=True,
    )
    pending_votes = models.ManyToManyField(
        "ej_conversations.Vote",
        related_name="pending_in_clusterizations",
        editable=False,
        blank=True,
    )

    unprocessed_comments = property(lambda self: self.pending_comments.count())
    unprocessed_votes = property(lambda self: self.pending_votes.count())
    comments = delegate_to("conversation")
    users = delegate_to("conversation")
    votes = delegate_to("conversation")
    owner = delegate_to("conversation", name="author")

    @property
    def stereotypes(self):
        return Stereotype.objects.filter(clusters__in=self.clusters.all())

    @property
    def stereotype_votes(self):
        return StereotypeVote.objects.filter(comment__in=self.comments.all())

    #
    # Statistics and annotated values
    #
    n_clusters = lazy(this.clusters.count())
    n_stereotypes = lazy(this.stereotypes.count())
    n_stereotype_votes = lazy(this.stereotype_votes.count())

    objects = ClusterizationManager()

    class Meta:
        ordering = ["conversation_id"]

    def __str__(self):
        clusters = self.clusters.count()
        return f"{self.conversation} ({clusters} clusters)"

    def get_absolute_url(self):
        return self.conversation.url("cluster:index")

    def update_clusterization(self, force=False, atomic=False):
        """
        Update clusters if necessary, unless force=True, in which it
        unconditionally updates the clusterization.
        """
        if force or rules.test_rule("ej.must_update_clusterization", self):
            log.info(f"[clusters] updating cluster: {self.conversation}")

            if self.clusters.count() == 0:
                if self.cluster_status == ClusterStatus.ACTIVE:
                    self.cluster_status = ClusterStatus.PENDING_DATA
                self.save()
                return

            with use_transaction(atomic=atomic):
                try:
                    self.clusters.clusterize_from_votes()
                except ValueError:
                    return
                self.pending_comments.all().delete()
                self.pending_votes.all().delete()
                if self.cluster_status == ClusterStatus.PENDING_DATA:
                    self.cluster_status = ClusterStatus.ACTIVE
                x = self.id
                y = self.conversation_id
                self.save()
示例#9
0
class Profile(models.Model):
    """
    User profile
    """
    user = models.OneToOneField(User,
                                on_delete=models.CASCADE,
                                related_name='raw_profile')
    race = EnumField(Race, _('Race'), default=Race.UNFILLED)
    ethnicity = models.CharField(_('Ethnicity'), blank=True, max_length=50)
    education = models.CharField(_('Education'), blank=True, max_length=140)
    gender = EnumField(Gender, _('Gender identity'), default=Gender.UNFILLED)
    gender_other = models.CharField(_('User provided gender'),
                                    max_length=50,
                                    blank=True)
    age = models.IntegerField(_('Age'), null=True, blank=True)
    birth_date = models.DateField(_('Birth Date'), null=True, blank=True)
    country = models.CharField(_('Country'), blank=True, max_length=50)
    state = models.CharField(
        _('State'),
        blank=True,
        max_length=settings.EJ_STATE_MAX_LENGTH,
        choices=settings.EJ_STATE_CHOICES,
    )
    city = models.CharField(_('City'), blank=True, max_length=140)
    biography = models.TextField(_('Biography'), blank=True)
    occupation = models.CharField(_('Occupation'), blank=True, max_length=50)
    political_activity = models.TextField(_('Political activity'), blank=True)
    image = models.ImageField(_('Image'),
                              blank=True,
                              null=True,
                              upload_to='profile_images')

    name = delegate_to('user')
    email = delegate_to('user')
    is_active = delegate_to('user')
    is_staff = delegate_to('user')
    is_superuser = delegate_to('user')

    @property
    def age(self):
        if not self.birth_date:
            age = None
        else:
            delta = datetime.datetime.now().date() - self.birth_date
            age = abs(int(delta.days // 365.25))
        return age

    class Meta:
        ordering = ['user__email']

    def __str__(self):
        return __('{name}\'s profile').format(name=self.user.name)

    def __getattr__(self, attr):
        try:
            user = self.user
        except User.DoesNotExist:
            raise AttributeError(attr)
        return getattr(user, attr)

    @property
    def gender_description(self):
        if self.gender != Gender.UNDECLARED:
            return self.gender.description
        return self.gender_other

    @property
    def token(self):
        token = Token.objects.get_or_create(user_id=self.id)
        return token[0].key

    @property
    def image_url(self):
        try:
            return self.image.url
        except ValueError:
            for account in SocialAccount.objects.filter(user=self.user):
                picture = account.get_avatar_url()
                return picture
            return '/static/img/logo/avatar_default.svg'

    @property
    def has_image(self):
        return self.image or SocialAccount.objects.filter(user_id=self.id)

    @property
    def is_filled(self):
        fields = ('race', 'age', 'birth_date', 'education', 'ethnicity',
                  'country', 'state', 'city', 'biography', 'occupation',
                  'political_activity', 'has_image', 'gender_description')
        return bool(all(getattr(self, field) for field in fields))

    def get_absolute_url(self):
        return reverse('user-detail', kwargs={'pk': self.id})

    def profile_fields(self, user_fields=False, blacklist=None):
        """
        Return a list of tuples of (field_description, field_value) for all
        registered profile fields.
        """

        fields = [
            'city', 'country', 'occupation', 'education', 'ethnicity',
            'gender', 'race', 'political_activity', 'biography'
        ]
        field_map = {field.name: field for field in self._meta.fields}

        # Create a tuples of (attribute, human-readable name, value)
        triple_list = []
        for field in fields:
            description = field_map[field].verbose_name
            getter = getattr(self, f'get_{field}_display',
                             lambda: getattr(self, field))
            triple = (field, description.capitalize(), getter())
            triple_list.append(triple)

        # Age is not a real field, but a property. We insert it after occupation
        triple_list.insert(3, ('age', _('Age'), self.age))

        # Add fields in the user profile (e.g., e-mail)
        if user_fields:
            triple_list.insert(0, ('email', _('E-mail'), self.user.email))

        # Prepare blacklist of fields
        if blacklist is None:
            blacklist = settings.EJ_EXCLUDE_PROFILE_FIELDS

        # Remove the attribute name from the list
        return [(b, c) for a, b, c in triple_list if a not in blacklist]

    def statistics(self):
        """
        Return a dictionary with all profile statistics.
        """
        return dict(
            votes=self.user.votes.count(),
            comments=self.user.comments.count(),
            conversations=self.user.conversations.count(),
        )

    def badges(self):
        """
        Return all profile badges.
        """
        return self.user.badges_earned.all()

    def comments(self):
        """
        Return all profile comments.
        """
        return self.user.comments.all()

    def role(self):
        """
        A human-friendly description of the user role in the platform.
        """
        if self.user.is_superuser:
            return _('Root')
        if self.user.is_staff:
            return _('Administrative user')
        return _('Regular user')
示例#10
0
class Profile(models.Model):
    """
    User profile
    """

    user = models.OneToOneField(User,
                                on_delete=models.CASCADE,
                                related_name="profile")
    race = EnumField(Race, _("Race"), default=Race.NOT_FILLED)
    ethnicity = models.CharField(_("Ethnicity"), blank=True, max_length=50)
    education = models.CharField(_("Education"), blank=True, max_length=140)
    gender = EnumField(Gender, _("Gender identity"), default=Gender.NOT_FILLED)
    gender_other = models.CharField(_("User provided gender"),
                                    max_length=50,
                                    blank=True)
    birth_date = models.DateField(_("Birth Date"), null=True, blank=True)
    country = models.CharField(_("Country"), blank=True, max_length=50)
    state = models.CharField(
        _("State"),
        blank=True,
        max_length=settings.EJ_STATE_MAX_LENGTH,
        choices=settings.EJ_STATE_CHOICES,
    )
    city = models.CharField(_("City"), blank=True, max_length=140)
    biography = models.TextField(_("Biography"), blank=True)
    occupation = models.CharField(_("Occupation"), blank=True, max_length=50)
    political_activity = models.TextField(_("Political activity"), blank=True)
    profile_photo = models.ImageField(_("Profile Photo"),
                                      blank=True,
                                      null=True,
                                      upload_to="profile_images")

    name = delegate_to("user")
    email = delegate_to("user")
    is_active = delegate_to("user")
    is_staff = delegate_to("user")
    is_superuser = delegate_to("user")
    limit_board_conversations = delegate_to("user")

    @property
    def age(self):
        return None if self.birth_date is None else years_from(self.birth_date)

    class Meta:
        ordering = ["user__email"]

    def __str__(self):
        return __("{name}'s profile").format(name=self.user.name)

    def __getattr__(self, attr):
        try:
            user = self.user
        except User.DoesNotExist:
            raise AttributeError(attr)
        return getattr(user, attr)

    @property
    def gender_description(self):
        if self.gender != Gender.NOT_FILLED:
            return self.gender.description
        return self.gender_other

    @property
    def token(self):
        token = Token.objects.get_or_create(user_id=self.id)
        return token[0].key

    @property
    def image_url(self):
        try:
            return self.profile_photo.url
        except ValueError:
            if apps.is_installed("allauth.socialaccount"):
                for account in SocialAccount.objects.filter(user=self.user):
                    picture = account.get_avatar_url()
                    return picture
            return staticfiles_storage.url("/img/login/avatar.svg")

    @property
    def has_image(self):
        return bool(self.profile_photo
                    or (apps.is_installed("allauth.socialaccount")
                        and SocialAccount.objects.filter(user_id=self.id)))

    @property
    def is_filled(self):
        fields = (
            "race",
            "age",
            "birth_date",
            "education",
            "ethnicity",
            "country",
            "state",
            "city",
            "biography",
            "occupation",
            "political_activity",
            "has_image",
            "gender_description",
        )
        return bool(all(getattr(self, field) for field in fields))

    def get_absolute_url(self):
        return reverse("user-detail", kwargs={"pk": self.id})

    def profile_fields(self, user_fields=False, blacklist=None):
        """
        Return a list of tuples of (field_description, field_value) for all
        registered profile fields.
        """

        fields = [
            "city",
            "state",
            "country",
            "occupation",
            "education",
            "ethnicity",
            "gender",
            "race",
            "political_activity",
            "biography",
        ]
        field_map = {field.name: field for field in self._meta.fields}
        null_values = {Gender.NOT_FILLED, Race.NOT_FILLED}

        # Create a tuples of (attribute, human-readable name, value)
        triple_list = []
        for field in fields:
            description = field_map[field].verbose_name
            value = getattr(self, field)
            if value in null_values:
                value = None
            elif hasattr(self, f"get_{field}_display"):
                value = getattr(self, f"get_{field}_display")()
            triple_list.append((field, description, value))

        # Age is not a real field, but a property. We insert it after occupation
        triple_list.insert(3, ("age", _("Age"), self.age))

        # Add fields in the user profile (e.g., e-mail)
        if user_fields:
            triple_list.insert(0, ("email", _("E-mail"), self.user.email))

        # Prepare blacklist of fields
        if blacklist is None:
            blacklist = settings.EJ_EXCLUDE_PROFILE_FIELDS

        # Remove the attribute name from the list
        return [(b, c) for a, b, c in triple_list if a not in blacklist]

    def statistics(self):
        """
        Return a dictionary with all profile statistics.
        """
        return dict(
            votes=self.user.votes.count(),
            comments=self.user.comments.count(),
            conversations=self.user.conversations.count(),
        )

    def badges(self):
        """
        Return all profile badges.
        """
        return self.user.badges_earned.all()

    def comments(self):
        """
        Return all profile comments.
        """
        return self.user.comments.all()

    def role(self):
        """
        A human-friendly description of the user role in the platform.
        """
        if self.user.is_superuser:
            return _("Root")
        if self.user.is_staff:
            return _("Administrative user")
        return _("Regular user")
示例#11
0
class Profile(models.Model):
    """
    User profile
    """
    user = models.OneToOneField(User,
                                on_delete=models.CASCADE,
                                related_name='raw_profile')
    race = EnumField(Race, _('Race'), default=Race.UNDECLARED)
    gender = EnumField(Gender, _('Gender identity'), default=Gender.UNDECLARED)
    gender_other = models.CharField(_('User provided gender'),
                                    max_length=50,
                                    blank=True)
    age = models.IntegerField(_('Age'), null=True, blank=True)
    country = models.CharField(_('Country'), blank=True, max_length=50)
    state = models.CharField(_('State'), blank=True, max_length=140)
    city = models.CharField(_('City'), blank=True, max_length=140)
    biography = models.TextField(_('Biography'), blank=True)
    occupation = models.CharField(_('Occupation'), blank=True, max_length=50)
    political_activity = models.TextField(_('Political activity'), blank=True)
    image = models.ImageField(_('Image'),
                              blank=True,
                              null=True,
                              upload_to='profile_images')

    name = delegate_to('user')
    username = delegate_to('user')
    email = delegate_to('user')
    is_active = delegate_to('user')
    is_staff = delegate_to('user')
    is_superuser = delegate_to('user')

    class Meta:
        ordering = ['user__username']

    def __str__(self):
        return __('{name}\'s profile').format(name=self.user.name)

    def __getattr__(self, attr):
        try:
            user = self.user
        except User.DoesNotExist:
            raise AttributeError(attr)
        return getattr(user, attr)

    @property
    def gender_description(self):
        if self.gender != Gender.UNDECLARED:
            return self.gender.description
        return self.gender_other

    @property
    def image_url(self):
        try:
            return self.image.url
        except ValueError:
            for account in SocialAccount.objects.filter(user_id=self.id):
                picture = account.get_avatar_url()
                if picture:
                    return picture
            return avatar_fallback()

    @property
    def has_image(self):
        return self.image or SocialAccount.objects.filter(user_id=self.id)

    @property
    def is_filled(self):
        fields = ('race', 'age', 'country', 'state', 'city', 'biography',
                  'occupation', 'political_activity', 'has_image',
                  'gender_description')
        print([getattr(self, field) for field in fields])
        return bool(all(getattr(self, field) for field in fields))

    def get_absolute_url(self):
        return reverse('user-detail', kwargs={'pk': self.id})

    def profile_fields(self, user_fields=False):
        """
        Return a list of tuples of (field_description, field_value) for all
        registered profile fields.
        """

        fields = [
            'city', 'country', 'occupation', 'age', 'gender', 'race',
            'political_activity', 'biography'
        ]
        field_map = {field.name: field for field in self._meta.fields}
        result = []
        for field in fields:
            description = field_map[field].verbose_name
            getter = getattr(self, f'get_{field}_display',
                             lambda: getattr(self, field))
            result.append((description.capitalize(), getter()))
        if user_fields:
            result = [
                (_('E-mail'), self.user.email),
                *result,
            ]
        return result

    def statistics(self):
        """
        Return a dictionary with all profile statistics.
        """
        return dict(
            votes=self.user.votes.count(),
            comments=self.user.comments.count(),
            conversations=self.user.conversations.count(),
        )

    def badges(self):
        """
        Return all profile badges.
        """
        # FIXME remove this print
        print("See all badges")
        print(self.user.badges_earned.all())
        return self.user.badges_earned.all()

    def comments(self):
        """
        Return all profile comments.
        """
        # FIXME remove this print
        print("See all comments")
        print(self.user.comments.all())
        return self.user.comments.all()

    def role(self):
        """
        A human-friendly description of the user role in the platform.
        """
        if self.user.is_superuser:
            return _('Root')
        if self.user.is_staff:
            return _('Administrative user')
        return _('Regular user')