PageRenderTime 64ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/reviews/forms.py

https://github.com/shockflash/reviews
Python | 373 lines | 350 code | 9 blank | 14 comment | 2 complexity | ff79859c4e1b9ecd0f9488745673b0d4 MD5 | raw file
  1. import time
  2. import datetime
  3. from django import forms
  4. from django.forms.util import ErrorDict
  5. from django.conf import settings
  6. from django.contrib.contenttypes.models import ContentType
  7. from django.utils.crypto import salted_hmac, constant_time_compare
  8. from django.utils.encoding import force_unicode
  9. from django.utils.hashcompat import sha_constructor
  10. from django.utils.text import get_text_list
  11. from django.utils.translation import ungettext, ugettext_lazy as _
  12. from django.forms.formsets import formset_factory
  13. from reviews.models import Review, ReviewSegment, Category, CategorySegment
  14. from reviews import signing
  15. REVIEW_MAX_LENGTH = getattr(settings,'REVIEW_MAX_LENGTH', 3000)
  16. REVIEWS_ALLOW_PROFANITIES = getattr(settings,'REVIEWS_ALLOW_PROFANITIES', False)
  17. REVIEW_MIN_RATING = getattr(settings,'REVIEW_MIN_RATING', 1)
  18. REVIEW_MAX_RATING = getattr(settings,'REVIEW_MAX_RATING', 5)
  19. RATING_CHOICES = (
  20. ('1', 'Very poor'),
  21. ('2', 'Not that bad'),
  22. ('3', 'Average'),
  23. ('4', 'Good'),
  24. ('5', 'Perfect'),
  25. )
  26. class SignedCharField(forms.CharField):
  27. """
  28. SignedCharField is a normal char field, but the content is signed with
  29. the django Signer to prevent manipulation. Best used together with
  30. """
  31. def prepare_value(self, value):
  32. # test if the value is already signed. if yes, we do not resign it
  33. try:
  34. signing.loads(value)
  35. return value
  36. except signing.BadSignature:
  37. return signing.dumps(value)
  38. def to_python(self, value):
  39. """ a broken/manipulated value will raise an exception of
  40. type signing.BadSignature. We do not catch it, it should raise """
  41. original = signing.loads(value)
  42. return original
  43. def clean_text(text):
  44. """
  45. If REVIEWS_ALLOW_PROFANITIES is False, check that the review doesn't
  46. contain anything in PROFANITIES_LIST.
  47. """
  48. if REVIEWS_ALLOW_PROFANITIES == False:
  49. bad_words = [w for w in settings.PROFANITIES_LIST if w in text.lower()]
  50. if bad_words:
  51. plural = len(bad_words) > 1
  52. raise forms.ValidationError(ungettext(
  53. "Watch your mouth! The word %s is not allowed here.",
  54. "Watch your mouth! The words %s are not allowed here.", plural) % \
  55. get_text_list(['"%s%s%s"' % (i[0], '-'*(len(i)-2), i[-1]) for i in bad_words], 'and'))
  56. return text
  57. class ReviewSecurityForm(forms.Form):
  58. """
  59. Handles the security aspects (anti-spoofing) for review forms.
  60. """
  61. content_type = forms.CharField(widget=forms.HiddenInput)
  62. object_pk = forms.CharField(widget=forms.HiddenInput)
  63. timestamp = forms.IntegerField(widget=forms.HiddenInput)
  64. security_hash = forms.CharField(min_length=40, max_length=40, widget=forms.HiddenInput)
  65. def __init__(self, target_object, data=None, initial=None, category=None):
  66. self.target_object = target_object
  67. if initial is None:
  68. initial = {}
  69. initial['category'] = category
  70. initial.update(self.generate_security_data())
  71. """ the category of this review. Needed for the form set """
  72. self.category = category
  73. super(ReviewSecurityForm, self).__init__(data=data, initial=initial)
  74. def security_errors(self):
  75. """Return just those errors associated with security"""
  76. errors = ErrorDict()
  77. for f in ["honeypot", "timestamp", "security_hash"]:
  78. if f in self.errors:
  79. errors[f] = self.errors[f]
  80. return errors
  81. def clean_security_hash(self):
  82. """Check the security hash."""
  83. security_hash_dict = {
  84. 'content_type' : self.data.get("content_type", ""),
  85. 'object_pk' : self.data.get("object_pk", ""),
  86. 'timestamp' : self.data.get("timestamp", ""),
  87. }
  88. expected_hash = self.generate_security_hash(**security_hash_dict)
  89. actual_hash = self.cleaned_data["security_hash"]
  90. if not constant_time_compare(expected_hash, actual_hash):
  91. # Fallback to Django 1.2 method for compatibility
  92. # PendingDeprecationWarning <- here to remind us to remove this
  93. # fallback in Django 1.5
  94. expected_hash_old = self._generate_security_hash_old(**security_hash_dict)
  95. if not constant_time_compare(expected_hash_old, actual_hash):
  96. raise forms.ValidationError("Security hash check failed.")
  97. return actual_hash
  98. def clean_timestamp(self):
  99. """Make sure the timestamp isn't too far (> 2 hours) in the past."""
  100. ts = self.cleaned_data["timestamp"]
  101. if time.time() - ts > (2 * 60 * 60):
  102. raise forms.ValidationError("Timestamp check failed")
  103. return ts
  104. def generate_security_data(self):
  105. """Generate a dict of security data for "initial" data."""
  106. timestamp = int(time.time())
  107. security_dict = {
  108. 'content_type' : str(self.target_object._meta),
  109. 'object_pk' : str(self.target_object._get_pk_val()),
  110. 'timestamp' : str(timestamp),
  111. 'security_hash' : self.initial_security_hash(timestamp),
  112. }
  113. return security_dict
  114. def initial_security_hash(self, timestamp):
  115. """
  116. Generate the initial security hash from self.content_object
  117. and a (unix) timestamp.
  118. """
  119. initial_security_dict = {
  120. 'content_type' : str(self.target_object._meta),
  121. 'object_pk' : str(self.target_object._get_pk_val()),
  122. 'timestamp' : str(timestamp),
  123. }
  124. return self.generate_security_hash(**initial_security_dict)
  125. def generate_security_hash(self, content_type, object_pk, timestamp):
  126. """
  127. Generate a HMAC security hash from the provided info.
  128. """
  129. info = (content_type, object_pk, timestamp)
  130. key_salt = "django.contrib.forms.ReviewSecurityForm"
  131. value = "-".join(info)
  132. return salted_hmac(key_salt, value).hexdigest()
  133. def _generate_security_hash_old(self, content_type, object_pk, timestamp):
  134. """Generate a (SHA1) security hash from the provided info."""
  135. # Django 1.2 compatibility
  136. info = (content_type, object_pk, timestamp, settings.SECRET_KEY)
  137. return sha_constructor("".join(info)).hexdigest()
  138. class ReviewDetailsForm(ReviewSecurityForm):
  139. """
  140. Handles the specific details of the review (name, review, etc.).
  141. """
  142. name = forms.CharField(label=_("Name"), max_length=50)
  143. email = forms.EmailField(label=_("Email address"))
  144. title = forms.CharField(label=_("Title"), max_length=200)
  145. rating = forms.ChoiceField(label=_('Rating'), choices=RATING_CHOICES)
  146. text = forms.CharField(label=_('Review'), widget=forms.Textarea,
  147. max_length=REVIEW_MAX_LENGTH)
  148. """ SignedCharField is used to hold the category value, but with a digital
  149. signature attached. This way we can route it through the form, but can be
  150. sure that it is not changed. If not done this way, the user could change the
  151. category of the review, which is not good """
  152. category = SignedCharField(max_length=200, widget=forms.HiddenInput)
  153. def __init__(self, *args, **kwargs):
  154. """
  155. Overwrites ReviewSecurityForm init to add support for the segment
  156. formset.
  157. Later in the template or code, use {formvariable}.formset to access
  158. the formset
  159. """
  160. initial = kwargs.get('initial', [])
  161. category = kwargs.get('category', None)
  162. data = kwargs.get('data', None)
  163. self.formset = self.get_segment_formset(data, initial=initial, category=category)
  164. super(ReviewDetailsForm, self).__init__(*args, **kwargs)
  165. def get_review_object(self):
  166. """
  167. Return a new (unsaved) review object based on the information in this
  168. form. Assumes that the form is already validated and will throw a
  169. ValueError if not.
  170. Does not set any of the fields that would come from a Request object
  171. (i.e. ``user`` or ``ip_address``).
  172. """
  173. if not self.is_valid():
  174. raise ValueError("get_review_object may only be called on valid forms")
  175. ReviewModel = self.get_review_model()
  176. new = ReviewModel(**self.get_review_create_data())
  177. new = self.check_for_duplicate_review(new)
  178. return new
  179. def get_review_model(self):
  180. """
  181. Get the review model to create with this form. Subclasses in custom
  182. review apps should override this, get_review_create_data, and perhaps
  183. check_for_duplicate_review to provide custom review models.
  184. """
  185. return Review
  186. def get_review_create_data(self):
  187. """
  188. Returns the dict of data to be used to create a review. Subclasses in
  189. custom review apps that override get_review_model can override this
  190. method to add extra fields onto a custom review model.
  191. """
  192. return dict(
  193. content_type = ContentType.objects.get_for_model(self.target_object),
  194. object_pk = force_unicode(self.target_object._get_pk_val()),
  195. user_name = self.cleaned_data["name"],
  196. user_email = self.cleaned_data["email"],
  197. text = self.cleaned_data["text"],
  198. title = self.cleaned_data["title"],
  199. rating = self.cleaned_data["rating"],
  200. category = Category.objects.get(code=self.cleaned_data["category"]),
  201. submit_date = datetime.datetime.now(),
  202. site_id = settings.SITE_ID,
  203. is_public = True,
  204. is_removed = False,
  205. )
  206. def get_segment_objects(self):
  207. """
  208. Returns a list of (unsaved) review segment objects based on the
  209. information in this form.
  210. """
  211. segments = []
  212. for form in self.formset:
  213. segments.append(form.get_segment_object())
  214. return segments
  215. def get_segment_form(self):
  216. return ReviewSegmentForm
  217. def get_segment_formset(self, data = {}, initial=[], category = None):
  218. # if category is not a parameter, we try to get it from the instance
  219. # itself. Normaly this function is called in the init-constructor of
  220. # the class, where it is called with category as parameter. If not,
  221. # then self.category is availabel as alternative
  222. if not category:
  223. category = self.category
  224. cat = Category.objects.get(code=category)
  225. for segment in cat.categorysegment_set.order_by('position'):
  226. initial.append({
  227. 'categorysegment_id': segment.id,
  228. 'text': ''
  229. })
  230. ReviewSegmentFormSet = formset_factory(self.get_segment_form(), extra=0)
  231. formset = ReviewSegmentFormSet(data, initial=initial)
  232. return formset
  233. def check_for_duplicate_review(self, new):
  234. """
  235. Check that a submitted review isn't a duplicate. This might be caused
  236. by someone posting a review twice. If it is a dup, silently return the *previous* review.
  237. """
  238. possible_duplicates = self.get_review_model()._default_manager.using(
  239. self.target_object._state.db
  240. ).filter(
  241. content_type = new.content_type,
  242. object_pk = new.object_pk,
  243. user_name = new.user_name,
  244. user_email = new.user_email,
  245. )
  246. for old in possible_duplicates:
  247. if old.submit_date.date() == new.submit_date.date() and old.text == new.text:
  248. return old
  249. return new
  250. def clean_text(self):
  251. return clean_text(self.cleaned_data["text"])
  252. class ReviewForm(ReviewDetailsForm):
  253. honeypot = forms.CharField(required=False,
  254. label=_('If you enter anything in this field '\
  255. 'your review will be treated as spam'))
  256. def clean_honeypot(self):
  257. """Check that nothing's been entered into the honeypot."""
  258. value = self.cleaned_data["honeypot"]
  259. if value:
  260. raise forms.ValidationError(self.fields["honeypot"].label)
  261. return value
  262. class ReviewSegmentBaseForm(forms.Form):
  263. categorysegment_id = forms.CharField(widget=forms.HiddenInput)
  264. def get_categorysegment(self):
  265. """
  266. Gives the category segment for this form.
  267. Can be used to show the title of the category segment and load other
  268. data related to it.
  269. """
  270. if 'segment' in self:
  271. return self.segment
  272. self.segment = CategorySegment.objects.get(pk=self.initial['categorysegment_id'])
  273. return self.segment
  274. class ReviewSegmentForm(ReviewSegmentBaseForm):
  275. rating = forms.ChoiceField(label=_('Rating'), choices=RATING_CHOICES)
  276. text = forms.CharField(label=_('Review'), widget=forms.Textarea,
  277. required=False, max_length=REVIEW_MAX_LENGTH)
  278. def clean_text(self):
  279. return clean_text(self.cleaned_data["text"])
  280. def get_segment_object(self):
  281. """
  282. Returns a new (unsaved) review segment object based on the information
  283. in this form. Assumes that the form is already validated. No validation is
  284. done, should be checked in get_review_object already.
  285. """
  286. categorysegment_id = self.initial['categorysegment_id']
  287. SegmentModel = self.get_segment_model()
  288. new = SegmentModel(**self.get_segment_create_data(categorysegment_id))
  289. return new
  290. def get_segment_model(self):
  291. """
  292. Returns the Review Segment model used with this form. Subclass in custom
  293. review apps should overwrite this and get_segment_create_data to provide
  294. custom segment models.
  295. """
  296. if not self.is_valid():
  297. raise ValueError("get_segment_model may only be called on valid forms")
  298. return ReviewSegment
  299. def get_segment_create_data(self, categorysegment_id):
  300. """
  301. Returns the dict of data to be used to create a review. Subclasses in
  302. custom review apps that override get_review_model can override this
  303. method to add extra fields onto a custom review model.
  304. """
  305. # todo fill dict with right values
  306. return dict(
  307. segment_id = self.initial['categorysegment_id'],
  308. text = self.cleaned_data["text"],
  309. rating = self.cleaned_data["rating"],
  310. )