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})"
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)
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)
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)
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)
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})'
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)
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()
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')
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")
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')