Commit 10fac3f08a6704eab0e9010d25036bc56b41c5eb
1 parent
af99bae5
Isolated abstract models outside of models.py
Fixes #83. Thanks Julen Ruiz Aizpuru for the report and the review.
Showing
3 changed files
with
180 additions
and
169 deletions
django_comments/abstracts.py
0 → 100644
| 1 | +from __future__ import unicode_literals | ||
| 2 | + | ||
| 3 | +from django.conf import settings | ||
| 4 | +from django.contrib.contenttypes.fields import GenericForeignKey | ||
| 5 | +from django.contrib.contenttypes.models import ContentType | ||
| 6 | +from django.contrib.sites.models import Site | ||
| 7 | +from django.core import urlresolvers | ||
| 8 | +from django.db import models | ||
| 9 | +from django.utils import timezone | ||
| 10 | +from django.utils.encoding import python_2_unicode_compatible | ||
| 11 | +from django.utils.translation import ugettext_lazy as _ | ||
| 12 | + | ||
| 13 | +from .managers import CommentManager | ||
| 14 | + | ||
| 15 | +COMMENT_MAX_LENGTH = getattr(settings, 'COMMENT_MAX_LENGTH', 3000) | ||
| 16 | + | ||
| 17 | + | ||
| 18 | +class BaseCommentAbstractModel(models.Model): | ||
| 19 | + """ | ||
| 20 | + An abstract base class that any custom comment models probably should | ||
| 21 | + subclass. | ||
| 22 | + """ | ||
| 23 | + | ||
| 24 | + # Content-object field | ||
| 25 | + content_type = models.ForeignKey(ContentType, | ||
| 26 | + verbose_name=_('content type'), | ||
| 27 | + related_name="content_type_set_for_%(class)s", | ||
| 28 | + on_delete=models.CASCADE) | ||
| 29 | + object_pk = models.TextField(_('object ID')) | ||
| 30 | + content_object = GenericForeignKey(ct_field="content_type", fk_field="object_pk") | ||
| 31 | + | ||
| 32 | + # Metadata about the comment | ||
| 33 | + site = models.ForeignKey(Site, on_delete=models.CASCADE) | ||
| 34 | + | ||
| 35 | + class Meta: | ||
| 36 | + abstract = True | ||
| 37 | + | ||
| 38 | + def get_content_object_url(self): | ||
| 39 | + """ | ||
| 40 | + Get a URL suitable for redirecting to the content object. | ||
| 41 | + """ | ||
| 42 | + return urlresolvers.reverse( | ||
| 43 | + "comments-url-redirect", | ||
| 44 | + args=(self.content_type_id, self.object_pk) | ||
| 45 | + ) | ||
| 46 | + | ||
| 47 | + | ||
| 48 | +@python_2_unicode_compatible | ||
| 49 | +class CommentAbstractModel(BaseCommentAbstractModel): | ||
| 50 | + """ | ||
| 51 | + A user comment about some object. | ||
| 52 | + """ | ||
| 53 | + | ||
| 54 | + # Who posted this comment? If ``user`` is set then it was an authenticated | ||
| 55 | + # user; otherwise at least user_name should have been set and the comment | ||
| 56 | + # was posted by a non-authenticated user. | ||
| 57 | + user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), | ||
| 58 | + blank=True, null=True, related_name="%(class)s_comments", | ||
| 59 | + on_delete=models.SET_NULL) | ||
| 60 | + user_name = models.CharField(_("user's name"), max_length=50, blank=True) | ||
| 61 | + # Explicit `max_length` to apply both to Django 1.7 and 1.8+. | ||
| 62 | + user_email = models.EmailField(_("user's email address"), max_length=254, | ||
| 63 | + blank=True) | ||
| 64 | + user_url = models.URLField(_("user's URL"), blank=True) | ||
| 65 | + | ||
| 66 | + comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH) | ||
| 67 | + | ||
| 68 | + # Metadata about the comment | ||
| 69 | + submit_date = models.DateTimeField(_('date/time submitted'), default=None, db_index=True) | ||
| 70 | + ip_address = models.GenericIPAddressField(_('IP address'), unpack_ipv4=True, blank=True, null=True) | ||
| 71 | + is_public = models.BooleanField(_('is public'), default=True, | ||
| 72 | + help_text=_('Uncheck this box to make the comment effectively ' | ||
| 73 | + 'disappear from the site.')) | ||
| 74 | + is_removed = models.BooleanField(_('is removed'), default=False, | ||
| 75 | + help_text=_('Check this box if the comment is inappropriate. ' | ||
| 76 | + 'A "This comment has been removed" message will ' | ||
| 77 | + 'be displayed instead.')) | ||
| 78 | + | ||
| 79 | + # Manager | ||
| 80 | + objects = CommentManager() | ||
| 81 | + | ||
| 82 | + class Meta: | ||
| 83 | + abstract = True | ||
| 84 | + ordering = ('submit_date',) | ||
| 85 | + permissions = [("can_moderate", "Can moderate comments")] | ||
| 86 | + verbose_name = _('comment') | ||
| 87 | + verbose_name_plural = _('comments') | ||
| 88 | + | ||
| 89 | + def __str__(self): | ||
| 90 | + return "%s: %s..." % (self.name, self.comment[:50]) | ||
| 91 | + | ||
| 92 | + def save(self, *args, **kwargs): | ||
| 93 | + if self.submit_date is None: | ||
| 94 | + self.submit_date = timezone.now() | ||
| 95 | + super(CommentAbstractModel, self).save(*args, **kwargs) | ||
| 96 | + | ||
| 97 | + def _get_userinfo(self): | ||
| 98 | + """ | ||
| 99 | + Get a dictionary that pulls together information about the poster | ||
| 100 | + safely for both authenticated and non-authenticated comments. | ||
| 101 | + | ||
| 102 | + This dict will have ``name``, ``email``, and ``url`` fields. | ||
| 103 | + """ | ||
| 104 | + if not hasattr(self, "_userinfo"): | ||
| 105 | + userinfo = { | ||
| 106 | + "name": self.user_name, | ||
| 107 | + "email": self.user_email, | ||
| 108 | + "url": self.user_url | ||
| 109 | + } | ||
| 110 | + if self.user_id: | ||
| 111 | + u = self.user | ||
| 112 | + if u.email: | ||
| 113 | + userinfo["email"] = u.email | ||
| 114 | + | ||
| 115 | + # If the user has a full name, use that for the user name. | ||
| 116 | + # However, a given user_name overrides the raw user.username, | ||
| 117 | + # so only use that if this comment has no associated name. | ||
| 118 | + if u.get_full_name(): | ||
| 119 | + userinfo["name"] = self.user.get_full_name() | ||
| 120 | + elif not self.user_name: | ||
| 121 | + userinfo["name"] = u.get_username() | ||
| 122 | + self._userinfo = userinfo | ||
| 123 | + return self._userinfo | ||
| 124 | + | ||
| 125 | + userinfo = property(_get_userinfo, doc=_get_userinfo.__doc__) | ||
| 126 | + | ||
| 127 | + def _get_name(self): | ||
| 128 | + return self.userinfo["name"] | ||
| 129 | + | ||
| 130 | + def _set_name(self, val): | ||
| 131 | + if self.user_id: | ||
| 132 | + raise AttributeError(_("This comment was posted by an authenticated " | ||
| 133 | + "user and thus the name is read-only.")) | ||
| 134 | + self.user_name = val | ||
| 135 | + | ||
| 136 | + name = property(_get_name, _set_name, doc="The name of the user who posted this comment") | ||
| 137 | + | ||
| 138 | + def _get_email(self): | ||
| 139 | + return self.userinfo["email"] | ||
| 140 | + | ||
| 141 | + def _set_email(self, val): | ||
| 142 | + if self.user_id: | ||
| 143 | + raise AttributeError(_("This comment was posted by an authenticated " | ||
| 144 | + "user and thus the email is read-only.")) | ||
| 145 | + self.user_email = val | ||
| 146 | + | ||
| 147 | + email = property(_get_email, _set_email, doc="The email of the user who posted this comment") | ||
| 148 | + | ||
| 149 | + def _get_url(self): | ||
| 150 | + return self.userinfo["url"] | ||
| 151 | + | ||
| 152 | + def _set_url(self, val): | ||
| 153 | + self.user_url = val | ||
| 154 | + | ||
| 155 | + url = property(_get_url, _set_url, doc="The URL given by the user who posted this comment") | ||
| 156 | + | ||
| 157 | + def get_absolute_url(self, anchor_pattern="#c%(id)s"): | ||
| 158 | + return self.get_content_object_url() + (anchor_pattern % self.__dict__) | ||
| 159 | + | ||
| 160 | + def get_as_text(self): | ||
| 161 | + """ | ||
| 162 | + Return this comment as plain text. Useful for emails. | ||
| 163 | + """ | ||
| 164 | + d = { | ||
| 165 | + 'user': self.user or self.name, | ||
| 166 | + 'date': self.submit_date, | ||
| 167 | + 'comment': self.comment, | ||
| 168 | + 'domain': self.site.domain, | ||
| 169 | + 'url': self.get_absolute_url() | ||
| 170 | + } | ||
| 171 | + return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d |
| 1 | from django.conf import settings | 1 | from django.conf import settings |
| 2 | -from django.contrib.contenttypes.fields import GenericForeignKey | ||
| 3 | -from django.contrib.contenttypes.models import ContentType | ||
| 4 | -from django.contrib.sites.models import Site | ||
| 5 | -from django.core import urlresolvers | ||
| 6 | from django.db import models | 2 | from django.db import models |
| 7 | -from django.utils.translation import ugettext_lazy as _ | ||
| 8 | from django.utils import timezone | 3 | from django.utils import timezone |
| 9 | from django.utils.encoding import python_2_unicode_compatible | 4 | from django.utils.encoding import python_2_unicode_compatible |
| 5 | +from django.utils.translation import ugettext_lazy as _ | ||
| 10 | 6 | ||
| 11 | -from django_comments.managers import CommentManager | ||
| 12 | - | ||
| 13 | -COMMENT_MAX_LENGTH = getattr(settings, 'COMMENT_MAX_LENGTH', 3000) | ||
| 14 | - | ||
| 15 | - | ||
| 16 | -class BaseCommentAbstractModel(models.Model): | ||
| 17 | - """ | ||
| 18 | - An abstract base class that any custom comment models probably should | ||
| 19 | - subclass. | ||
| 20 | - """ | ||
| 21 | - | ||
| 22 | - # Content-object field | ||
| 23 | - content_type = models.ForeignKey(ContentType, | ||
| 24 | - verbose_name=_('content type'), | ||
| 25 | - related_name="content_type_set_for_%(class)s", | ||
| 26 | - on_delete=models.CASCADE) | ||
| 27 | - object_pk = models.TextField(_('object ID')) | ||
| 28 | - content_object = GenericForeignKey(ct_field="content_type", fk_field="object_pk") | ||
| 29 | - | ||
| 30 | - # Metadata about the comment | ||
| 31 | - site = models.ForeignKey(Site, on_delete=models.CASCADE) | ||
| 32 | - | ||
| 33 | - class Meta: | ||
| 34 | - abstract = True | ||
| 35 | - | ||
| 36 | - def get_content_object_url(self): | ||
| 37 | - """ | ||
| 38 | - Get a URL suitable for redirecting to the content object. | ||
| 39 | - """ | ||
| 40 | - return urlresolvers.reverse( | ||
| 41 | - "comments-url-redirect", | ||
| 42 | - args=(self.content_type_id, self.object_pk) | ||
| 43 | - ) | ||
| 44 | - | ||
| 45 | - | ||
| 46 | -@python_2_unicode_compatible | ||
| 47 | -class CommentAbstractModel(BaseCommentAbstractModel): | ||
| 48 | - """ | ||
| 49 | - A user comment about some object. | ||
| 50 | - """ | ||
| 51 | - | ||
| 52 | - # Who posted this comment? If ``user`` is set then it was an authenticated | ||
| 53 | - # user; otherwise at least user_name should have been set and the comment | ||
| 54 | - # was posted by a non-authenticated user. | ||
| 55 | - user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), | ||
| 56 | - blank=True, null=True, related_name="%(class)s_comments", | ||
| 57 | - on_delete=models.SET_NULL) | ||
| 58 | - user_name = models.CharField(_("user's name"), max_length=50, blank=True) | ||
| 59 | - # Explicit `max_length` to apply both to Django 1.7 and 1.8+. | ||
| 60 | - user_email = models.EmailField(_("user's email address"), max_length=254, | ||
| 61 | - blank=True) | ||
| 62 | - user_url = models.URLField(_("user's URL"), blank=True) | ||
| 63 | - | ||
| 64 | - comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH) | ||
| 65 | - | ||
| 66 | - # Metadata about the comment | ||
| 67 | - submit_date = models.DateTimeField(_('date/time submitted'), default=None, db_index=True) | ||
| 68 | - ip_address = models.GenericIPAddressField(_('IP address'), unpack_ipv4=True, blank=True, null=True) | ||
| 69 | - is_public = models.BooleanField(_('is public'), default=True, | ||
| 70 | - help_text=_('Uncheck this box to make the comment effectively ' | ||
| 71 | - 'disappear from the site.')) | ||
| 72 | - is_removed = models.BooleanField(_('is removed'), default=False, | ||
| 73 | - help_text=_('Check this box if the comment is inappropriate. ' | ||
| 74 | - 'A "This comment has been removed" message will ' | ||
| 75 | - 'be displayed instead.')) | ||
| 76 | - | ||
| 77 | - # Manager | ||
| 78 | - objects = CommentManager() | ||
| 79 | - | ||
| 80 | - class Meta: | ||
| 81 | - abstract = True | ||
| 82 | - ordering = ('submit_date',) | ||
| 83 | - permissions = [("can_moderate", "Can moderate comments")] | ||
| 84 | - verbose_name = _('comment') | ||
| 85 | - verbose_name_plural = _('comments') | ||
| 86 | - | ||
| 87 | - def __str__(self): | ||
| 88 | - return "%s: %s..." % (self.name, self.comment[:50]) | ||
| 89 | - | ||
| 90 | - def save(self, *args, **kwargs): | ||
| 91 | - if self.submit_date is None: | ||
| 92 | - self.submit_date = timezone.now() | ||
| 93 | - super(CommentAbstractModel, self).save(*args, **kwargs) | ||
| 94 | - | ||
| 95 | - def _get_userinfo(self): | ||
| 96 | - """ | ||
| 97 | - Get a dictionary that pulls together information about the poster | ||
| 98 | - safely for both authenticated and non-authenticated comments. | ||
| 99 | - | ||
| 100 | - This dict will have ``name``, ``email``, and ``url`` fields. | ||
| 101 | - """ | ||
| 102 | - if not hasattr(self, "_userinfo"): | ||
| 103 | - userinfo = { | ||
| 104 | - "name": self.user_name, | ||
| 105 | - "email": self.user_email, | ||
| 106 | - "url": self.user_url | ||
| 107 | - } | ||
| 108 | - if self.user_id: | ||
| 109 | - u = self.user | ||
| 110 | - if u.email: | ||
| 111 | - userinfo["email"] = u.email | ||
| 112 | - | ||
| 113 | - # If the user has a full name, use that for the user name. | ||
| 114 | - # However, a given user_name overrides the raw user.username, | ||
| 115 | - # so only use that if this comment has no associated name. | ||
| 116 | - if u.get_full_name(): | ||
| 117 | - userinfo["name"] = self.user.get_full_name() | ||
| 118 | - elif not self.user_name: | ||
| 119 | - userinfo["name"] = u.get_username() | ||
| 120 | - self._userinfo = userinfo | ||
| 121 | - return self._userinfo | ||
| 122 | - | ||
| 123 | - userinfo = property(_get_userinfo, doc=_get_userinfo.__doc__) | ||
| 124 | - | ||
| 125 | - def _get_name(self): | ||
| 126 | - return self.userinfo["name"] | ||
| 127 | - | ||
| 128 | - def _set_name(self, val): | ||
| 129 | - if self.user_id: | ||
| 130 | - raise AttributeError(_("This comment was posted by an authenticated " | ||
| 131 | - "user and thus the name is read-only.")) | ||
| 132 | - self.user_name = val | ||
| 133 | - | ||
| 134 | - name = property(_get_name, _set_name, doc="The name of the user who posted this comment") | ||
| 135 | - | ||
| 136 | - def _get_email(self): | ||
| 137 | - return self.userinfo["email"] | ||
| 138 | - | ||
| 139 | - def _set_email(self, val): | ||
| 140 | - if self.user_id: | ||
| 141 | - raise AttributeError(_("This comment was posted by an authenticated " | ||
| 142 | - "user and thus the email is read-only.")) | ||
| 143 | - self.user_email = val | ||
| 144 | - | ||
| 145 | - email = property(_get_email, _set_email, doc="The email of the user who posted this comment") | ||
| 146 | - | ||
| 147 | - def _get_url(self): | ||
| 148 | - return self.userinfo["url"] | ||
| 149 | - | ||
| 150 | - def _set_url(self, val): | ||
| 151 | - self.user_url = val | ||
| 152 | - | ||
| 153 | - url = property(_get_url, _set_url, doc="The URL given by the user who posted this comment") | ||
| 154 | - | ||
| 155 | - def get_absolute_url(self, anchor_pattern="#c%(id)s"): | ||
| 156 | - return self.get_content_object_url() + (anchor_pattern % self.__dict__) | ||
| 157 | - | ||
| 158 | - def get_as_text(self): | ||
| 159 | - """ | ||
| 160 | - Return this comment as plain text. Useful for emails. | ||
| 161 | - """ | ||
| 162 | - d = { | ||
| 163 | - 'user': self.user or self.name, | ||
| 164 | - 'date': self.submit_date, | ||
| 165 | - 'comment': self.comment, | ||
| 166 | - 'domain': self.site.domain, | ||
| 167 | - 'url': self.get_absolute_url() | ||
| 168 | - } | ||
| 169 | - return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d | 7 | +from .abstracts import ( |
| 8 | + COMMENT_MAX_LENGTH, BaseCommentAbstractModel, CommentAbstractModel, | ||
| 9 | +) | ||
| 170 | 10 | ||
| 171 | 11 | ||
| 172 | class Comment(CommentAbstractModel): | 12 | class Comment(CommentAbstractModel): |
| @@ -61,16 +61,16 @@ the ``my_comment_app`` directory:: | @@ -61,16 +61,16 @@ the ``my_comment_app`` directory:: | ||
| 61 | In the ``models.py`` we'll define a ``CommentWithTitle`` model:: | 61 | In the ``models.py`` we'll define a ``CommentWithTitle`` model:: |
| 62 | 62 | ||
| 63 | from django.db import models | 63 | from django.db import models |
| 64 | - from django_comments.models import CommentAbstractModel | 64 | + from django_comments.abstracts import CommentAbstractModel |
| 65 | 65 | ||
| 66 | class CommentWithTitle(CommentAbstractModel): | 66 | class CommentWithTitle(CommentAbstractModel): |
| 67 | title = models.CharField(max_length=300) | 67 | title = models.CharField(max_length=300) |
| 68 | 68 | ||
| 69 | Most custom comment models will subclass the | 69 | Most custom comment models will subclass the |
| 70 | -:class:`~django_comments.models.CommentAbstractModel` model. However, | 70 | +:class:`~django_comments.abstracts.CommentAbstractModel` model. However, |
| 71 | if you want to substantially remove or change the fields available in the | 71 | if you want to substantially remove or change the fields available in the |
| 72 | -:class:`~django_comments.models.CommentAbstractModel` model, but don't want to | ||
| 73 | -rewrite the templates, you could try subclassing from | 72 | +:class:`~django_comments.abstracts.CommentAbstractModel` model, but don't want |
| 73 | +to rewrite the templates, you could try subclassing from | ||
| 74 | ``BaseCommentAbstractModel``. | 74 | ``BaseCommentAbstractModel``. |
| 75 | 75 | ||
| 76 | Next, we'll define a custom comment form in ``forms.py``. This is a little more | 76 | Next, we'll define a custom comment form in ``forms.py``. This is a little more |
| @@ -137,7 +137,7 @@ however. | @@ -137,7 +137,7 @@ however. | ||
| 137 | 137 | ||
| 138 | Return the :class:`~django.db.models.Model` class to use for comments. This | 138 | Return the :class:`~django.db.models.Model` class to use for comments. This |
| 139 | model should inherit from | 139 | model should inherit from |
| 140 | - ``django_comments.models.BaseCommentAbstractModel``, which | 140 | + ``django_comments.abstracts.BaseCommentAbstractModel``, which |
| 141 | defines necessary core fields. | 141 | defines necessary core fields. |
| 142 | 142 | ||
| 143 | The default implementation returns | 143 | The default implementation returns |
Please
register
or
login
to post a comment