def test_custom_validator(self): # Setup machina_settings.MARKUP_MAX_LENGTH_VALIDATOR = 'django.core.validators.MaxLengthValidator' validator = validators.MarkupMaxLengthValidator(2) # Run & check assert validator('aa') is None with pytest.raises(ValidationError): validator('aaa')
class AbstractForumProfile(models.Model): """ Represents the profile associated with each forum user. """ user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='forum_profile', verbose_name=_('User'), ) # The user's avatar. avatar = ExtendedImageField(null=True, blank=True, upload_to=get_profile_avatar_upload_to, verbose_name=_('Avatar'), **machina_settings.DEFAULT_AVATAR_SETTINGS) # The user's signature. signature = MarkupTextField( verbose_name=_('Signature'), blank=True, null=True, validators=[ validators.MarkupMaxLengthValidator( machina_settings.PROFILE_SIGNATURE_MAX_LENGTH), ], ) # The amount of posts the user has posted (only approved posts are considered here). posts_count = models.PositiveIntegerField(verbose_name=_('Total posts'), blank=True, default=0) class Meta: abstract = True app_label = 'forum_member' verbose_name = _('Forum profile') verbose_name_plural = _('Forum profiles') def __str__(self): return self.user.get_username() def get_avatar_upload_to(self, filename): """ Returns the path to upload the associated avatar to. """ dummy, ext = os.path.splitext(filename) return os.path.join( machina_settings.PROFILE_AVATAR_UPLOAD_TO, '{id}{ext}'.format(id=str(uuid.uuid4()).replace('-', ''), ext=ext), )
class AbstractPost(DatedModel): """ Represents a forum post. A forum post is always linked to a topic. """ topic = models.ForeignKey( 'forum_conversation.Topic', related_name='posts', on_delete=models.CASCADE, verbose_name=_('Topic'), ) poster = models.ForeignKey( settings.AUTH_USER_MODEL, related_name='posts', blank=True, null=True, on_delete=models.CASCADE, verbose_name=_('Poster'), ) anonymous_key = models.CharField( max_length=100, blank=True, null=True, verbose_name=_('Anonymous user forum key'), ) # Each post can have its own subject. The subject of the thread corresponds to the # one associated with the first post subject = models.CharField(verbose_name=_('Subject'), max_length=255) # Content content = MarkupTextField( validators=[ validators.MarkupMaxLengthValidator( machina_settings.POST_CONTENT_MAX_LENGTH), ], verbose_name=_('Content'), ) # Username: if the user creating a topic post is not authenticated, he must enter a username username = models.CharField(max_length=155, blank=True, null=True, verbose_name=_('Username')) # A post can be approved before publishing ; defaults to True approved = models.BooleanField(default=True, db_index=True, verbose_name=_('Approved')) # The user can choose if they want to display their signature with the content of the post enable_signature = models.BooleanField( default=True, db_index=True, verbose_name=_('Attach a signature'), ) # A post can be edited for several reason (eg. moderation) ; the reason why it has been # updated can be specified update_reason = models.CharField( max_length=255, blank=True, null=True, verbose_name=_('Update reason'), ) # Tracking data updated_by = models.ForeignKey( settings.AUTH_USER_MODEL, editable=False, blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_('Lastly updated by'), ) updates_count = models.PositiveIntegerField( editable=False, blank=True, default=0, verbose_name=_('Updates count'), ) objects = models.Manager() approved_objects = ApprovedManager() class Meta: abstract = True app_label = 'forum_conversation' ordering = [ 'created', ] get_latest_by = 'created' verbose_name = _('Post') verbose_name_plural = _('Posts') def __str__(self): return self.subject @property def is_topic_head(self): """ Returns ``True`` if the post is the first post of the topic. """ return self.topic.first_post.id == self.id if self.topic.first_post else False @property def is_topic_tail(self): """ Returns ``True`` if the post is the last post of the topic. """ return self.topic.last_post.id == self.id if self.topic.last_post else False @property def is_alone(self): """ Returns ``True`` if the post is the only single post of the topic. """ return self.topic.posts.count() == 1 @property def position(self): """ Returns an integer corresponding to the position of the post in the topic. """ position = self.topic.posts.filter( Q(created__lt=self.created) | Q(id=self.id)).count() return position def clean(self): """ Validates the post instance. """ super().clean() # At least a poster (user) or a session key must be associated with # the post. if self.poster is None and self.anonymous_key is None: raise ValidationError( _('A user id or an anonymous key must be associated with a post.' ), ) if self.poster and self.anonymous_key: raise ValidationError( _('A user id or an anonymous key must be associated with a post, but not both.' ), ) if self.anonymous_key and not self.username: raise ValidationError( _('A username must be specified if the poster is anonymous')) def save(self, *args, **kwargs): """ Saves the post instance. """ new_post = self.pk is None super().save(*args, **kwargs) # Ensures that the subject of the thread corresponds to the one associated # with the first post. Do the same with the 'approved' flag. if (new_post and self.topic.first_post is None) or self.is_topic_head: if self.subject != self.topic.subject or self.approved != self.topic.approved: self.topic.subject = self.subject self.topic.approved = self.approved # Trigger the topic-level trackers update self.topic.update_trackers() def delete(self, using=None): """ Deletes the post instance. """ if self.is_alone: # The default way of operating is to trigger the deletion of the associated topic # only if the considered post is the only post embedded in the topic self.topic.delete() else: super(AbstractPost, self).delete(using) self.topic.update_trackers()
def test_default_validator(self): # Setup validator = validators.MarkupMaxLengthValidator(None) # Run & check assert validator(faker.text()) is None