X-Git-Url: https://git.mdrn.pl/redakcja.git/blobdiff_plain/82c38a77943cf91a084429bf10740edffbd0c195..2f9cb34a07fcd98effda2fa900e48c31813f14c8:/apps/forms_builder/forms/models.py?ds=inline diff --git a/apps/forms_builder/forms/models.py b/apps/forms_builder/forms/models.py new file mode 100644 index 00000000..ea373e60 --- /dev/null +++ b/apps/forms_builder/forms/models.py @@ -0,0 +1,294 @@ +from __future__ import unicode_literals + +from django.contrib.sites.models import Site +from django.core.exceptions import ValidationError +from django.core.urlresolvers import reverse +from django.db import models +from django.db.models import Q +from django.utils.encoding import python_2_unicode_compatible +from django.utils.translation import ugettext, ugettext_lazy as _ +from future.builtins import str + +from forms_builder.forms import fields +from forms_builder.forms import settings +from forms_builder.forms.utils import now, slugify, unique_slug + + +STATUS_DRAFT = 1 +STATUS_PUBLISHED = 2 +STATUS_CHOICES = ( + (STATUS_DRAFT, _("Draft")), + (STATUS_PUBLISHED, _("Published")), +) + + +class FormManager(models.Manager): + """ + Only show published forms for non-staff users. + """ + def published(self, for_user=None): + if for_user is not None and for_user.is_staff: + return self.all() + filters = [ + Q(publish_date__lte=now()) | Q(publish_date__isnull=True), + Q(expiry_date__gte=now()) | Q(expiry_date__isnull=True), + Q(status=STATUS_PUBLISHED), + ] + if settings.USE_SITES: + filters.append(Q(sites=Site.objects.get_current())) + return self.filter(*filters) + + +###################################################################### +# # +# Each of the models are implemented as abstract to allow for # +# subclassing. Default concrete implementations are then defined # +# at the end of this module. # +# # +###################################################################### + +@python_2_unicode_compatible +class AbstractForm(models.Model): + """ + A user-built form. + """ + + sites = models.ManyToManyField(Site, + default=[settings.SITE_ID], related_name="%(app_label)s_%(class)s_forms") + title = models.CharField(_("Title"), max_length=50) + slug = models.SlugField(_("Slug"), editable=settings.EDITABLE_SLUGS, + max_length=100, unique=True) + intro = models.TextField(_("Intro"), blank=True) + button_text = models.CharField(_("Button text"), max_length=50, + default=_("Submit")) + response = models.TextField(_("Response"), blank=True) + redirect_url = models.CharField(_("Redirect url"), max_length=200, + null=True, blank=True, + help_text=_("An alternate URL to redirect to after form submission")) + status = models.IntegerField(_("Status"), choices=STATUS_CHOICES, + default=STATUS_PUBLISHED) + publish_date = models.DateTimeField(_("Published from"), + help_text=_("With published selected, won't be shown until this time"), + blank=True, null=True) + expiry_date = models.DateTimeField(_("Expires on"), + help_text=_("With published selected, won't be shown after this time"), + blank=True, null=True) + login_required = models.BooleanField(_("Login required"), default=False, + help_text=_("If checked, only logged in users can view the form")) + send_email = models.BooleanField(_("Send email"), default=True, help_text= + _("If checked, the person entering the form will be sent an email")) + email_from = models.EmailField(_("From address"), blank=True, + help_text=_("The address the email will be sent from")) + email_copies = models.CharField(_("Send copies to"), blank=True, + help_text=_("One or more email addresses, separated by commas"), + max_length=200) + email_subject = models.CharField(_("Subject"), max_length=200, blank=True) + email_message = models.TextField(_("Message"), blank=True) + + objects = FormManager() + + class Meta: + verbose_name = _("Form") + verbose_name_plural = _("Forms") + abstract = True + + def __str__(self): + return str(self.title) + + def save(self, *args, **kwargs): + """ + Create a unique slug from title - append an index and increment if it + already exists. + """ + if not self.slug: + slug = slugify(self) + self.slug = unique_slug(self.__class__.objects, "slug", slug) + super(AbstractForm, self).save(*args, **kwargs) + + def published(self, for_user=None): + """ + Mimics the queryset logic in ``FormManager.published``, so we + can check a form is published when it wasn't loaded via the + queryset's ``published`` method, and is passed to the + ``render_built_form`` template tag. + """ + if for_user is not None and for_user.is_staff: + return True + status = self.status == STATUS_PUBLISHED + publish_date = self.publish_date is None or self.publish_date <= now() + expiry_date = self.expiry_date is None or self.expiry_date >= now() + authenticated = for_user is not None and for_user.is_authenticated() + login_required = (not self.login_required or authenticated) + return status and publish_date and expiry_date and login_required + + def total_entries(self): + """ + Called by the admin list view where the queryset is annotated + with the number of entries. + """ + return self.total_entries + total_entries.admin_order_field = "total_entries" + + @models.permalink + def get_absolute_url(self): + return ("form_detail", (), {"slug": self.slug}) + + def admin_links(self): + kw = {"args": (self.id,)} + links = [ + (_("View form on site"), self.get_absolute_url()), + (_("Filter entries"), reverse("admin:form_entries", **kw)), + (_("View all entries"), reverse("admin:form_entries_show", **kw)), + (_("Export all entries"), reverse("admin:form_entries_export", **kw)), + ] + for i, (text, url) in enumerate(links): + links[i] = "%s" % (url, ugettext(text)) + return "
".join(links) + admin_links.allow_tags = True + admin_links.short_description = "" + + +class FieldManager(models.Manager): + """ + Only show visible fields when displaying actual form.. + """ + def visible(self): + return self.filter(visible=True) + + +@python_2_unicode_compatible +class AbstractField(models.Model): + """ + A field for a user-built form. + """ + + label = models.CharField(_("Label"), max_length=settings.LABEL_MAX_LENGTH) + slug = models.SlugField(_('Slug'), max_length=100, blank=True, + default="") + field_type = models.IntegerField(_("Type"), choices=fields.NAMES) + required = models.BooleanField(_("Required"), default=True) + visible = models.BooleanField(_("Visible"), default=True) + choices = models.CharField(_("Choices"), max_length=settings.CHOICES_MAX_LENGTH, blank=True, + help_text="Comma separated options where applicable. If an option " + "itself contains commas, surround the option starting with the %s" + "character and ending with the %s character." % + (settings.CHOICES_QUOTE, settings.CHOICES_UNQUOTE)) + default = models.CharField(_("Default value"), blank=True, + max_length=settings.FIELD_MAX_LENGTH) + placeholder_text = models.CharField(_("Placeholder Text"), null=True, + blank=True, max_length=100, editable=settings.USE_HTML5) + help_text = models.CharField(_("Help text"), blank=True, max_length=settings.HELPTEXT_MAX_LENGTH) + + objects = FieldManager() + + class Meta: + verbose_name = _("Field") + verbose_name_plural = _("Fields") + abstract = True + + def __str__(self): + return str(self.label) + + def get_choices(self): + """ + Parse a comma separated choice string into a list of choices taking + into account quoted choices using the ``settings.CHOICES_QUOTE`` and + ``settings.CHOICES_UNQUOTE`` settings. + """ + choice = "" + quoted = False + for char in self.choices: + if not quoted and char == settings.CHOICES_QUOTE: + quoted = True + elif quoted and char == settings.CHOICES_UNQUOTE: + quoted = False + elif char == "," and not quoted: + choice = choice.strip() + if choice: + yield choice, choice + choice = "" + else: + choice += char + choice = choice.strip() + if choice: + yield choice, choice + + def save(self, *args, **kwargs): + if not self.slug: + slug = slugify(self).replace('-', '_') + self.slug = unique_slug(self.form.fields, "slug", slug) + return super(AbstractField, self).save(*args, **kwargs) + + def is_a(self, *args): + """ + Helper that returns True if the field's type is given in any arg. + """ + return self.field_type in args + + +class AbstractFormEntry(models.Model): + """ + An entry submitted via a user-built form. + """ + + entry_time = models.DateTimeField(_("Date/time")) + + class Meta: + verbose_name = _("Form entry") + verbose_name_plural = _("Form entries") + abstract = True + + +class AbstractFieldEntry(models.Model): + """ + A single field value for a form entry submitted via a user-built form. + """ + + field_id = models.IntegerField() + value = models.CharField(max_length=settings.FIELD_MAX_LENGTH, + null=True) + + class Meta: + verbose_name = _("Form field entry") + verbose_name_plural = _("Form field entries") + abstract = True + + +################################################### +# # +# Default concrete implementations are below. # +# # +################################################### + +class FormEntry(AbstractFormEntry): + form = models.ForeignKey("Form", related_name="entries") + + +class FieldEntry(AbstractFieldEntry): + entry = models.ForeignKey("FormEntry", related_name="fields") + + +class Form(AbstractForm): + pass + + +class Field(AbstractField): + """ + Implements automated field ordering. + """ + + form = models.ForeignKey("Form", related_name="fields") + order = models.IntegerField(_("Order"), null=True, blank=True) + + class Meta(AbstractField.Meta): + ordering = ("order",) + + def save(self, *args, **kwargs): + if self.order is None: + self.order = self.form.fields.count() + super(Field, self).save(*args, **kwargs) + + def delete(self, *args, **kwargs): + fields_after = self.form.fields.filter(order__gte=self.order) + fields_after.update(order=models.F("order") - 1) + super(Field, self).delete(*args, **kwargs)