forms.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. from __future__ import unicode_literals
  2. from future.builtins import int, str, zip
  3. from django import forms
  4. from django_comments.forms import CommentSecurityForm, CommentForm
  5. from django_comments.signals import comment_was_posted
  6. from django.utils.safestring import mark_safe
  7. from django.utils.translation import ugettext, ugettext_lazy as _
  8. from mezzanine.conf import settings
  9. from mezzanine.core.forms import Html5Mixin
  10. from mezzanine.generic.models import Keyword, ThreadedComment
  11. from mezzanine.utils.cache import add_cache_bypass
  12. from mezzanine.utils.email import split_addresses, send_mail_template
  13. from mezzanine.utils.static import static_lazy as static
  14. from mezzanine.utils.views import ip_for_request
  15. class KeywordsWidget(forms.MultiWidget):
  16. """
  17. Form field for the ``KeywordsField`` generic relation field. Since
  18. the admin with model forms has no form field for generic
  19. relations, this form field provides a single field for managing
  20. the keywords. It contains two actual widgets, a text input for
  21. entering keywords, and a hidden input that stores the ID of each
  22. ``Keyword`` instance.
  23. The attached JavaScript adds behaviour so that when the form is
  24. submitted, an AJAX post is made that passes the list of keywords
  25. in the text input, and returns a list of keyword IDs which are
  26. then entered into the hidden input before the form submits. The
  27. list of IDs in the hidden input is what is used when retrieving
  28. an actual value from the field for the form.
  29. """
  30. class Media:
  31. js = (static("mezzanine/js/admin/keywords_field.js"),)
  32. def __init__(self, attrs=None):
  33. """
  34. Setup the text and hidden form field widgets.
  35. """
  36. widgets = (forms.HiddenInput,
  37. forms.TextInput(attrs={"class": "vTextField"}))
  38. super(KeywordsWidget, self).__init__(widgets, attrs)
  39. self._ids = []
  40. def decompress(self, value):
  41. """
  42. Takes the sequence of ``AssignedKeyword`` instances and splits
  43. them into lists of keyword IDs and titles each mapping to one
  44. of the form field widgets.
  45. """
  46. if hasattr(value, "select_related"):
  47. keywords = [a.keyword for a in value.select_related("keyword")]
  48. if keywords:
  49. keywords = [(str(k.id), k.title) for k in keywords]
  50. self._ids, words = list(zip(*keywords))
  51. return (",".join(self._ids), ", ".join(words))
  52. return ("", "")
  53. def format_output(self, rendered_widgets):
  54. """
  55. Wraps the output HTML with a list of all available ``Keyword``
  56. instances that can be clicked on to toggle a keyword.
  57. """
  58. rendered = super(KeywordsWidget, self).format_output(rendered_widgets)
  59. links = ""
  60. for keyword in Keyword.objects.all().order_by("title"):
  61. prefix = "+" if str(keyword.id) not in self._ids else "-"
  62. links += ("<a href='#'>%s%s</a>" % (prefix, str(keyword)))
  63. rendered += mark_safe("<p class='keywords-field'>%s</p>" % links)
  64. return rendered
  65. def value_from_datadict(self, data, files, name):
  66. """
  67. Return the comma separated list of keyword IDs for use in
  68. ``KeywordsField.save_form_data()``.
  69. """
  70. return data.get("%s_0" % name, "")
  71. class ThreadedCommentForm(CommentForm, Html5Mixin):
  72. name = forms.CharField(label=_("Name"), help_text=_("required"),
  73. max_length=50)
  74. email = forms.EmailField(label=_("Email"),
  75. help_text=_("required (not published)"))
  76. url = forms.URLField(label=_("Website"), help_text=_("optional"),
  77. required=False)
  78. # These are used to get/set prepopulated fields via cookies.
  79. cookie_fields = ("name", "email", "url")
  80. cookie_prefix = "mezzanine-comment-"
  81. def __init__(self, request, *args, **kwargs):
  82. """
  83. Set some initial field values from cookies or the logged in
  84. user, and apply some HTML5 attributes to the fields if the
  85. ``FORMS_USE_HTML5`` setting is ``True``.
  86. """
  87. kwargs.setdefault("initial", {})
  88. user = request.user
  89. for field in ThreadedCommentForm.cookie_fields:
  90. cookie_name = ThreadedCommentForm.cookie_prefix + field
  91. value = request.COOKIES.get(cookie_name, "")
  92. if not value and user.is_authenticated():
  93. if field == "name":
  94. value = user.get_full_name()
  95. if not value and user.username != user.email:
  96. value = user.username
  97. elif field == "email":
  98. value = user.email
  99. kwargs["initial"][field] = value
  100. super(ThreadedCommentForm, self).__init__(*args, **kwargs)
  101. def get_comment_model(self):
  102. """
  103. Use the custom comment model instead of the built-in one.
  104. """
  105. return ThreadedComment
  106. def check_for_duplicate_comment(self, new):
  107. """
  108. We handle duplicates inside ``save``, since django_comments'
  109. `check_for_duplicate_comment` doesn't deal with extra fields
  110. defined on the comment model.
  111. """
  112. return new
  113. def save(self, request):
  114. """
  115. Saves a new comment and sends any notification emails.
  116. """
  117. comment = self.get_comment_object()
  118. obj = comment.content_object
  119. if request.user.is_authenticated():
  120. comment.user = request.user
  121. comment.by_author = request.user == getattr(obj, "user", None)
  122. comment.ip_address = ip_for_request(request)
  123. comment.replied_to_id = self.data.get("replied_to")
  124. # Mezzanine's duplicate check that also checks `replied_to_id`.
  125. lookup = {
  126. "content_type": comment.content_type,
  127. "object_pk": comment.object_pk,
  128. "user_name": comment.user_name,
  129. "user_email": comment.user_email,
  130. "user_url": comment.user_url,
  131. "replied_to_id": comment.replied_to_id,
  132. }
  133. for duplicate in self.get_comment_model().objects.filter(**lookup):
  134. if (duplicate.submit_date.date() == comment.submit_date.date() and
  135. duplicate.comment == comment.comment):
  136. return duplicate
  137. comment.save()
  138. comment_was_posted.send(sender=comment.__class__, comment=comment,
  139. request=request)
  140. notify_emails = split_addresses(settings.COMMENTS_NOTIFICATION_EMAILS)
  141. if notify_emails:
  142. subject = ugettext("New comment for: ") + str(obj)
  143. context = {
  144. "comment": comment,
  145. "comment_url": add_cache_bypass(comment.get_absolute_url()),
  146. "request": request,
  147. "obj": obj,
  148. }
  149. send_mail_template(subject, "email/comment_notification",
  150. settings.DEFAULT_FROM_EMAIL, notify_emails,
  151. context)
  152. return comment
  153. class RatingForm(CommentSecurityForm):
  154. """
  155. Form for a rating. Subclasses ``CommentSecurityForm`` to make use
  156. of its easy setup for generic relations.
  157. """
  158. value = forms.ChoiceField(label="", widget=forms.RadioSelect,
  159. choices=list(zip(
  160. *(settings.RATINGS_RANGE,) * 2)))
  161. def __init__(self, request, *args, **kwargs):
  162. self.request = request
  163. super(RatingForm, self).__init__(*args, **kwargs)
  164. if request and request.user.is_authenticated():
  165. current = self.rating_manager.filter(user=request.user).first()
  166. if current:
  167. self.initial['value'] = current.value
  168. @property
  169. def rating_manager(self):
  170. rating_name = self.target_object.get_ratingfield_name()
  171. return getattr(self.target_object, rating_name)
  172. def clean(self):
  173. """
  174. Check unauthenticated user's cookie as a light check to
  175. prevent duplicate votes.
  176. """
  177. bits = (self.data["content_type"], self.data["object_pk"])
  178. request = self.request
  179. self.current = "%s.%s" % bits
  180. self.previous = request.COOKIES.get("mezzanine-rating", "").split(",")
  181. already_rated = self.current in self.previous
  182. if already_rated and not self.request.user.is_authenticated():
  183. raise forms.ValidationError(ugettext("Already rated."))
  184. return self.cleaned_data
  185. def save(self):
  186. """
  187. Saves a new rating - authenticated users can update the
  188. value if they've previously rated.
  189. """
  190. user = self.request.user
  191. self.undoing = False
  192. rating_value = self.cleaned_data["value"]
  193. manager = self.rating_manager
  194. if user.is_authenticated():
  195. rating_instance, created = manager.get_or_create(user=user,
  196. defaults={'value': rating_value})
  197. if not created:
  198. if rating_instance.value == int(rating_value):
  199. # User submitted the same rating as previously,
  200. # which we treat as undoing the rating (like a toggle).
  201. rating_instance.delete()
  202. self.undoing = True
  203. else:
  204. rating_instance.value = rating_value
  205. rating_instance.save()
  206. else:
  207. rating_instance = manager.create(value=rating_value)
  208. return rating_instance