validate document text on save
[redakcja.git] / apps / forms_builder / forms / models.py
1 from __future__ import unicode_literals
2
3 from django.contrib.sites.models import Site
4 from django.core.exceptions import ValidationError
5 from django.core.urlresolvers import reverse
6 from django.db import models
7 from django.db.models import Q
8 from django.utils.encoding import python_2_unicode_compatible
9 from django.utils.translation import ugettext, ugettext_lazy as _
10 from future.builtins import str
11
12 from forms_builder.forms import fields
13 from forms_builder.forms import settings
14 from forms_builder.forms.utils import now, slugify, unique_slug
15
16
17 STATUS_DRAFT = 1
18 STATUS_PUBLISHED = 2
19 STATUS_CHOICES = (
20     (STATUS_DRAFT, _("Draft")),
21     (STATUS_PUBLISHED, _("Published")),
22 )
23
24
25 class FormManager(models.Manager):
26     """
27     Only show published forms for non-staff users.
28     """
29     def published(self, for_user=None):
30         if for_user is not None and for_user.is_staff:
31             return self.all()
32         filters = [
33             Q(publish_date__lte=now()) | Q(publish_date__isnull=True),
34             Q(expiry_date__gte=now()) | Q(expiry_date__isnull=True),
35             Q(status=STATUS_PUBLISHED),
36         ]
37         if settings.USE_SITES:
38             filters.append(Q(sites=Site.objects.get_current()))
39         return self.filter(*filters)
40
41
42 ######################################################################
43 #                                                                    #
44 #   Each of the models are implemented as abstract to allow for      #
45 #   subclassing. Default concrete implementations are then defined   #
46 #   at the end of this module.                                       #
47 #                                                                    #
48 ######################################################################
49
50 @python_2_unicode_compatible
51 class AbstractForm(models.Model):
52     """
53     A user-built form.
54     """
55
56     sites = models.ManyToManyField(Site,
57         default=[settings.SITE_ID], related_name="%(app_label)s_%(class)s_forms")
58     title = models.CharField(_("Title"), max_length=50)
59     slug = models.SlugField(_("Slug"), editable=settings.EDITABLE_SLUGS,
60         max_length=100, unique=True)
61     intro = models.TextField(_("Intro"), blank=True)
62     button_text = models.CharField(_("Button text"), max_length=50,
63         default=_("Submit"))
64     response = models.TextField(_("Response"), blank=True)
65     redirect_url = models.CharField(_("Redirect url"), max_length=200,
66         null=True, blank=True,
67         help_text=_("An alternate URL to redirect to after form submission"))
68     status = models.IntegerField(_("Status"), choices=STATUS_CHOICES,
69         default=STATUS_PUBLISHED)
70     publish_date = models.DateTimeField(_("Published from"),
71         help_text=_("With published selected, won't be shown until this time"),
72         blank=True, null=True)
73     expiry_date = models.DateTimeField(_("Expires on"),
74         help_text=_("With published selected, won't be shown after this time"),
75         blank=True, null=True)
76     login_required = models.BooleanField(_("Login required"), default=False,
77         help_text=_("If checked, only logged in users can view the form"))
78     send_email = models.BooleanField(_("Send email"), default=True, help_text=
79         _("If checked, the person entering the form will be sent an email"))
80     email_from = models.EmailField(_("From address"), blank=True,
81         help_text=_("The address the email will be sent from"))
82     email_copies = models.CharField(_("Send copies to"), blank=True,
83         help_text=_("One or more email addresses, separated by commas"),
84         max_length=200)
85     email_subject = models.CharField(_("Subject"), max_length=200, blank=True)
86     email_message = models.TextField(_("Message"), blank=True)
87
88     objects = FormManager()
89
90     class Meta:
91         verbose_name = _("Form")
92         verbose_name_plural = _("Forms")
93         abstract = True
94
95     def __str__(self):
96         return str(self.title)
97
98     def save(self, *args, **kwargs):
99         """
100         Create a unique slug from title - append an index and increment if it
101         already exists.
102         """
103         if not self.slug:
104             slug = slugify(self)
105             self.slug = unique_slug(self.__class__.objects, "slug", slug)
106         super(AbstractForm, self).save(*args, **kwargs)
107
108     def published(self, for_user=None):
109         """
110         Mimics the queryset logic in ``FormManager.published``, so we
111         can check a form is published when it wasn't loaded via the
112         queryset's ``published`` method, and is passed to the
113         ``render_built_form`` template tag.
114         """
115         if for_user is not None and for_user.is_staff:
116             return True
117         status = self.status == STATUS_PUBLISHED
118         publish_date = self.publish_date is None or self.publish_date <= now()
119         expiry_date = self.expiry_date is None or self.expiry_date >= now()
120         authenticated = for_user is not None and for_user.is_authenticated()
121         login_required = (not self.login_required or authenticated)
122         return status and publish_date and expiry_date and login_required
123
124     def total_entries(self):
125         """
126         Called by the admin list view where the queryset is annotated
127         with the number of entries.
128         """
129         return self.total_entries
130     total_entries.admin_order_field = "total_entries"
131
132     @models.permalink
133     def get_absolute_url(self):
134         return ("form_detail", (), {"slug": self.slug})
135
136     def admin_links(self):
137         kw = {"args": (self.id,)}
138         links = [
139             (_("View form on site"), self.get_absolute_url()),
140             (_("Filter entries"), reverse("admin:form_entries", **kw)),
141             (_("View all entries"), reverse("admin:form_entries_show", **kw)),
142             (_("Export all entries"), reverse("admin:form_entries_export", **kw)),
143         ]
144         for i, (text, url) in enumerate(links):
145             links[i] = "<a href='%s'>%s</a>" % (url, ugettext(text))
146         return "<br>".join(links)
147     admin_links.allow_tags = True
148     admin_links.short_description = ""
149
150
151 class FieldManager(models.Manager):
152     """
153     Only show visible fields when displaying actual form..
154     """
155     def visible(self):
156         return self.filter(visible=True)
157
158
159 @python_2_unicode_compatible
160 class AbstractField(models.Model):
161     """
162     A field for a user-built form.
163     """
164
165     label = models.CharField(_("Label"), max_length=settings.LABEL_MAX_LENGTH)
166     slug = models.SlugField(_('Slug'), max_length=100, blank=True,
167             default="")
168     field_type = models.IntegerField(_("Type"), choices=fields.NAMES)
169     required = models.BooleanField(_("Required"), default=True)
170     visible = models.BooleanField(_("Visible"), default=True)
171     choices = models.CharField(_("Choices"), max_length=settings.CHOICES_MAX_LENGTH, blank=True,
172         help_text="Comma separated options where applicable. If an option "
173             "itself contains commas, surround the option starting with the %s"
174             "character and ending with the %s character." %
175                 (settings.CHOICES_QUOTE, settings.CHOICES_UNQUOTE))
176     default = models.CharField(_("Default value"), blank=True,
177         max_length=settings.FIELD_MAX_LENGTH)
178     placeholder_text = models.CharField(_("Placeholder Text"), null=True,
179         blank=True, max_length=100, editable=settings.USE_HTML5)
180     help_text = models.CharField(_("Help text"), blank=True, max_length=settings.HELPTEXT_MAX_LENGTH)
181
182     objects = FieldManager()
183
184     class Meta:
185         verbose_name = _("Field")
186         verbose_name_plural = _("Fields")
187         abstract = True
188
189     def __str__(self):
190         return str(self.label)
191
192     def get_choices(self):
193         """
194         Parse a comma separated choice string into a list of choices taking
195         into account quoted choices using the ``settings.CHOICES_QUOTE`` and
196         ``settings.CHOICES_UNQUOTE`` settings.
197         """
198         choice = ""
199         quoted = False
200         for char in self.choices:
201             if not quoted and char == settings.CHOICES_QUOTE:
202                 quoted = True
203             elif quoted and char == settings.CHOICES_UNQUOTE:
204                 quoted = False
205             elif char == "," and not quoted:
206                 choice = choice.strip()
207                 if choice:
208                     yield choice, choice
209                 choice = ""
210             else:
211                 choice += char
212         choice = choice.strip()
213         if choice:
214             yield choice, choice
215
216     def save(self, *args, **kwargs):
217         if not self.slug:
218             slug = slugify(self).replace('-', '_')
219             self.slug = unique_slug(self.form.fields, "slug", slug)
220         return super(AbstractField, self).save(*args, **kwargs)
221
222     def is_a(self, *args):
223         """
224         Helper that returns True if the field's type is given in any arg.
225         """
226         return self.field_type in args
227
228
229 class AbstractFormEntry(models.Model):
230     """
231     An entry submitted via a user-built form.
232     """
233
234     entry_time = models.DateTimeField(_("Date/time"))
235
236     class Meta:
237         verbose_name = _("Form entry")
238         verbose_name_plural = _("Form entries")
239         abstract = True
240
241
242 class AbstractFieldEntry(models.Model):
243     """
244     A single field value for a form entry submitted via a user-built form.
245     """
246
247     field_id = models.IntegerField()
248     value = models.CharField(max_length=settings.FIELD_MAX_LENGTH,
249             null=True)
250
251     class Meta:
252         verbose_name = _("Form field entry")
253         verbose_name_plural = _("Form field entries")
254         abstract = True
255
256
257 ###################################################
258 #                                                 #
259 #   Default concrete implementations are below.   #
260 #                                                 #
261 ###################################################
262
263 class FormEntry(AbstractFormEntry):
264     form = models.ForeignKey("Form", related_name="entries")
265
266
267 class FieldEntry(AbstractFieldEntry):
268     entry = models.ForeignKey("FormEntry", related_name="fields")
269
270
271 class Form(AbstractForm):
272     pass
273
274
275 class Field(AbstractField):
276     """
277     Implements automated field ordering.
278     """
279
280     form = models.ForeignKey("Form", related_name="fields")
281     order = models.IntegerField(_("Order"), null=True, blank=True)
282
283     class Meta(AbstractField.Meta):
284         ordering = ("order",)
285
286     def save(self, *args, **kwargs):
287         if self.order is None:
288             self.order = self.form.fields.count()
289         super(Field, self).save(*args, **kwargs)
290
291     def delete(self, *args, **kwargs):
292         fields_after = self.form.fields.filter(order__gte=self.order)
293         fields_after.update(order=models.F("order") - 1)
294         super(Field, self).delete(*args, **kwargs)