From 746029d23123569b1645082bee62f0cdef0cb926 Mon Sep 17 00:00:00 2001 From: Ali <56682030+bigsbug@users.noreply.github.com> Date: Mon, 16 Jan 2023 17:14:27 +0330 Subject: [PATCH 1/6] Update README.rst --- README.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 9554b19b..55bf7a8a 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,9 @@ .. image:: https://secure.travis-ci.org/stephenmcd/django-forms-builder.png?branch=master :target: http://travis-ci.org/stephenmcd/django-forms-builder + +Comptible with Django v4 and backward comptible with older versions like v3 -django-forms-builder +advance-django-forms-builder ==================== Created by `Stephen McDonald `_ From 239b75d875b21bc87fde4e1362e22b4964e7bfa1 Mon Sep 17 00:00:00 2001 From: bigsbug Date: Tue, 17 Jan 2023 15:45:25 +0330 Subject: [PATCH 2/6] add apps.py to app --- forms_builder/forms/apps.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forms_builder/forms/apps.py diff --git a/forms_builder/forms/apps.py b/forms_builder/forms/apps.py new file mode 100644 index 00000000..086edd32 --- /dev/null +++ b/forms_builder/forms/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FormsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'forms' From 71d5f60859bb123c16fcdc8d6e5cfa2b2c2d74d7 Mon Sep 17 00:00:00 2001 From: bigsbug Date: Thu, 19 Jan 2023 10:26:41 +0330 Subject: [PATCH 3/6] add app label to forms app --- forms_builder/forms/apps.py | 1 + 1 file changed, 1 insertion(+) diff --git a/forms_builder/forms/apps.py b/forms_builder/forms/apps.py index 086edd32..557bd9a0 100644 --- a/forms_builder/forms/apps.py +++ b/forms_builder/forms/apps.py @@ -4,3 +4,4 @@ class FormsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'forms' + app_label = 'forms' From 8981c5b86afefb495b3e56471ceefb56fc93d920 Mon Sep 17 00:00:00 2001 From: bigsbug Date: Thu, 19 Jan 2023 10:27:05 +0330 Subject: [PATCH 4/6] foramt the models of forms with yapf --- forms_builder/forms/models.py | 170 ++++++++++++++++++++++++---------- 1 file changed, 121 insertions(+), 49 deletions(-) diff --git a/forms_builder/forms/models.py b/forms_builder/forms/models.py index 73c6ae3b..faf1a5cf 100644 --- a/forms_builder/forms/models.py +++ b/forms_builder/forms/models.py @@ -19,7 +19,6 @@ from forms_builder.forms import settings from forms_builder.forms.utils import now, slugify, unique_slug - STATUS_DRAFT = 1 STATUS_PUBLISHED = 2 STATUS_CHOICES = ( @@ -32,6 +31,7 @@ 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() @@ -53,43 +53,96 @@ def published(self, for_user=None): # # ###################################################################### + @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"), + 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"), + 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, + 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) + max_length=200, + ) + email_subject = models.CharField( + _("Subject"), + max_length=200, + blank=True, + ) + email_message = models.TextField( + _("Message"), + blank=True, + ) objects = FormManager() @@ -126,7 +179,8 @@ def published(self, for_user=None): authenticated = for_user is not None and for_user.is_authenticated if DJANGO_VERSION <= (1, 9): # Django 1.8 compatibility, is_authenticated has to be called as a method. - authenticated = for_user is not None and for_user.is_authenticated() + 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 @@ -136,22 +190,25 @@ def total_entries(self): with the number of entries. """ return self.total_entries + total_entries.admin_order_field = "total_entries" def get_absolute_url(self): return reverse("form_detail", kwargs={"slug": self.slug}) def admin_links(self): - kw = {"args": (self.id,)} + 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)), + (_("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 = "" @@ -160,6 +217,7 @@ class FieldManager(models.Manager): """ Only show visible fields when displaying actual form.. """ + def visible(self): return self.filter(visible=True) @@ -171,21 +229,29 @@ class AbstractField(models.Model): """ label = models.CharField(_("Label"), max_length=settings.LABEL_MAX_LENGTH) - slug = models.SlugField(_('Slug'), max_length=2000, blank=True, - default="") + slug = models.SlugField(_('Slug'), max_length=2000, 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, + 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) + "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() @@ -247,8 +313,7 @@ class AbstractFieldEntry(models.Model): """ field_id = models.IntegerField() - value = models.CharField(max_length=settings.FIELD_MAX_LENGTH, - null=True) + value = models.CharField(max_length=settings.FIELD_MAX_LENGTH, null=True) class Meta: verbose_name = _("Form field entry") @@ -262,12 +327,17 @@ class Meta: # # ################################################### + class FormEntry(AbstractFormEntry): - form = models.ForeignKey("Form", related_name="entries", on_delete=models.CASCADE) + form = models.ForeignKey("Form", + related_name="entries", + on_delete=models.CASCADE) class FieldEntry(AbstractFieldEntry): - entry = models.ForeignKey("FormEntry", related_name="fields", on_delete=models.CASCADE) + entry = models.ForeignKey("FormEntry", + related_name="fields", + on_delete=models.CASCADE) class Form(AbstractForm): @@ -279,11 +349,13 @@ class Field(AbstractField): Implements automated field ordering. """ - form = models.ForeignKey("Form", related_name="fields", on_delete=models.CASCADE) + form = models.ForeignKey("Form", + related_name="fields", + on_delete=models.CASCADE) order = models.IntegerField(_("Order"), null=True, blank=True) class Meta(AbstractField.Meta): - ordering = ("order",) + ordering = ("order", ) def save(self, *args, **kwargs): if self.order is None: From 9230081b9e044100a1021d3ede9bbb1bf9c5f0c6 Mon Sep 17 00:00:00 2001 From: bigsbug Date: Thu, 19 Jan 2023 11:31:50 +0330 Subject: [PATCH 5/6] update app name --- forms_builder/forms/apps.py | 2 +- forms_builder/forms/models.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/forms_builder/forms/apps.py b/forms_builder/forms/apps.py index 557bd9a0..9cae2052 100644 --- a/forms_builder/forms/apps.py +++ b/forms_builder/forms/apps.py @@ -3,5 +3,5 @@ class FormsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'forms' + name = 'forms_builder.forms' app_label = 'forms' diff --git a/forms_builder/forms/models.py b/forms_builder/forms/models.py index faf1a5cf..a4ac0d36 100644 --- a/forms_builder/forms/models.py +++ b/forms_builder/forms/models.py @@ -11,7 +11,7 @@ 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 @@ -54,7 +54,6 @@ def published(self, for_user=None): ###################################################################### -@python_2_unicode_compatible class AbstractForm(models.Model): """ A user-built form. @@ -222,7 +221,6 @@ def visible(self): return self.filter(visible=True) -@python_2_unicode_compatible class AbstractField(models.Model): """ A field for a user-built form. From b002a1b1862a685a62a15da0c98ae80d391c452b Mon Sep 17 00:00:00 2001 From: bigsbug Date: Thu, 19 Jan 2023 14:44:32 +0330 Subject: [PATCH 6/6] compitable with django v4 --- .gitignore | 3 + README.rst | 2 +- forms_builder/__init__.py | 2 +- forms_builder/example_project/manage.py | 16 -- forms_builder/example_project/settings.py | 178 +++++++++++------- forms_builder/example_project/urls.py | 6 + forms_builder/forms/admin.py | 18 +- forms_builder/forms/fields.py | 12 +- forms_builder/forms/forms.py | 126 +++++++------ ...er_fieldentry_id_alter_form_id_and_more.py | 39 ++++ forms_builder/forms/models.py | 31 +-- forms_builder/forms/signals.py | 6 +- .../forms/templatetags/forms_builder_tags.py | 4 - forms_builder/forms/urls.py | 8 +- forms_builder/forms/utils.py | 149 +++++++++++++-- forms_builder/forms/views.py | 53 +++--- forms_builder/manage.py | 26 +++ 17 files changed, 435 insertions(+), 244 deletions(-) delete mode 100755 forms_builder/example_project/manage.py create mode 100644 forms_builder/forms/migrations/0004_alter_field_id_alter_fieldentry_id_alter_form_id_and_more.py create mode 100755 forms_builder/manage.py diff --git a/.gitignore b/.gitignore index e3a53f70..e9177284 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ local_settings.py build/ dist/ forms_builder/example_project/static/ +forms_builder/example_project/ +*.sqlite3 + diff --git a/README.rst b/README.rst index 55bf7a8a..53d1b326 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ :target: http://travis-ci.org/stephenmcd/django-forms-builder Comptible with Django v4 and backward comptible with older versions like v3 - +add 'django.contrib.sites' to installed apps advance-django-forms-builder ==================== diff --git a/forms_builder/__init__.py b/forms_builder/__init__.py index 9e78220f..5c4105cd 100644 --- a/forms_builder/__init__.py +++ b/forms_builder/__init__.py @@ -1 +1 @@ -__version__ = "0.14.0" +__version__ = "1.0.1" diff --git a/forms_builder/example_project/manage.py b/forms_builder/example_project/manage.py deleted file mode 100755 index 7b95d5d1..00000000 --- a/forms_builder/example_project/manage.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python -from __future__ import absolute_import, unicode_literals - -import sys -import os - -from settings import PROJECT_ROOT, PROJECT_DIRNAME - -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..')) -sys.path.insert(0, os.path.abspath(os.path.join(PROJECT_ROOT, ".."))) - -if __name__ == "__main__": - settings_module = "%s.settings" % PROJECT_DIRNAME - os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module) - from django.core.management import execute_from_command_line - execute_from_command_line(sys.argv) diff --git a/forms_builder/example_project/settings.py b/forms_builder/example_project/settings.py index 27190666..a8f4cbb4 100644 --- a/forms_builder/example_project/settings.py +++ b/forms_builder/example_project/settings.py @@ -1,93 +1,133 @@ -from __future__ import absolute_import, unicode_literals +""" +Django settings for example_project project. -import os, sys +Generated by 'django-admin startproject' using Django 4.1.5. +For more information on this file, see +https://docs.djangoproject.com/en/4.1/topics/settings/ +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.1/ref/settings/ +""" + +from pathlib import Path +import os +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-5gyb2*ubhp@-g5tz47w%%o4&adp&-yco13m@x9f92*+rf_o#a#' + +# SECURITY WARNING: don't run with debug turned on in production! DEBUG = True + +ALLOWED_HOSTS = [] + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.sites', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + # "forms.apps.FormsConfig", + "forms.apps.FormsConfig", +] + SITE_ID = 1 -PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) -PROJECT_DIRNAME = PROJECT_ROOT.split(os.sep)[-1] -STATIC_URL = "/static/" -STATIC_ROOT = os.path.join(PROJECT_ROOT, STATIC_URL.strip("/")) -MEDIA_URL = STATIC_URL + "media/" -MEDIA_ROOT = os.path.join(PROJECT_ROOT, *MEDIA_URL.strip("/").split("/")) -ADMIN_MEDIA_PREFIX = STATIC_URL + "admin/" -ROOT_URLCONF = "%s.urls" % PROJECT_DIRNAME -TEMPLATE_DIRS = (os.path.join(PROJECT_ROOT, "templates"),) -SECRET_KEY = "asdfa4wtW#$Gse4aGdfs" -ADMINS = () - - -MANAGERS = ADMINS -if "test" not in sys.argv: - LOGIN_URL = "/admin/" +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'dev.db', - } -} +ROOT_URLCONF = 'example_project.urls' +TEMPLATE_DIRS = [ + BASE_DIR / "templates", +] TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'BACKEND': + 'django.template.backends.django.DjangoTemplates', 'DIRS': [ - os.path.join(PROJECT_ROOT, "templates") + BASE_DIR / "templates", + Path(__file__).resolve().parent / "templates", ], - 'APP_DIRS': True, + 'APP_DIRS': + True, 'OPTIONS': { 'context_processors': [ - 'django.contrib.auth.context_processors.auth', 'django.template.context_processors.debug', 'django.template.context_processors.request', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', + 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] -MIDDLEWARE = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', -) -# For Django 1.8 compatibility -MIDDLEWARE_CLASSES = MIDDLEWARE - -TEMPLATE_CONTEXT_PROCESSORS = ( - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - "django.core.context_processors.debug", - "django.core.context_processors.i18n", - "django.core.context_processors.static", - "django.core.context_processors.media", - "django.core.context_processors.request", -) - -INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.staticfiles', - 'forms_builder.forms', -) +WSGI_APPLICATION = 'example_project.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/4.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + +# Password validation +# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': + 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': + 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': + 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': + 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/4.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.1/howto/static-files/ -FORMS_BUILDER_EXTRA_FIELDS = ( - (100, "django.forms.BooleanField", "My cool checkbox"), -) +STATIC_URL = 'static/' -try: - from local_settings import * -except ImportError: - pass +# Default primary key field type +# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field -TEMPLATE_DEBUG = DEBUG +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/forms_builder/example_project/urls.py b/forms_builder/example_project/urls.py index 78627dd9..ba7c45fa 100644 --- a/forms_builder/example_project/urls.py +++ b/forms_builder/example_project/urls.py @@ -10,6 +10,12 @@ from forms_builder.forms.models import Form from forms_builder.forms import urls as form_urls +# from django.urls import path, include + +# urlpatterns = [ +# path('admin/', admin.site.urls), +# path("forms", include("forms.urls")) +# ] admin.autodiscover() diff --git a/forms_builder/forms/admin.py b/forms_builder/forms/admin.py index ed6fee4e..e241eaef 100644 --- a/forms_builder/forms/admin.py +++ b/forms_builder/forms/admin.py @@ -1,6 +1,3 @@ -from __future__ import unicode_literals -from future.builtins import bytes, open - from csv import writer from mimetypes import guess_type from os.path import join @@ -9,16 +6,13 @@ from django.contrib import admin from django.core.files.storage import FileSystemStorage -try: - from django.urls import reverse, re_path -except ImportError: - # For django 1.8 compatiblity - from django.conf.urls import url as re_path - from django.core.urlresolvers import reverse + +from django.urls import reverse, re_path + from django.db.models import Count from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render, get_object_or_404 -from django.utils.translation import ungettext, ugettext_lazy as _ +from django.utils.translation import ngettext, gettext_lazy as _ from forms_builder.forms.forms import EntriesForm from forms_builder.forms.models import Form, Field, FormEntry, FieldEntry @@ -177,8 +171,8 @@ def info(request, message, fail_silently=True): count = entries.count() if count > 0: entries.delete() - message = ungettext("1 entry deleted", - "%(count)s entries deleted", count) + message = ngettext("1 entry deleted", + "%(count)s entries deleted", count) info(request, message % {"count": count}) template = "admin/forms/entries.html" context = {"title": _("View Entries"), "entries_form": entries_form, diff --git a/forms_builder/forms/fields.py b/forms_builder/forms/fields.py index 23d58765..6a509c78 100644 --- a/forms_builder/forms/fields.py +++ b/forms_builder/forms/fields.py @@ -1,13 +1,7 @@ -from __future__ import unicode_literals - -from django.core.exceptions import ImproperlyConfigured from django import forms -try: - from django.forms import SelectDateWidget -except ImportError: - # For Django 1.8 compatibility - from django.forms.extras import SelectDateWidget -from django.utils.translation import ugettext_lazy as _ +from django.forms import SelectDateWidget +from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ImproperlyConfigured from forms_builder.forms.settings import USE_HTML5, EXTRA_FIELDS, EXTRA_WIDGETS from forms_builder.forms.utils import html5_field, import_attr diff --git a/forms_builder/forms/forms.py b/forms_builder/forms/forms.py index b8e3ae53..14a87532 100644 --- a/forms_builder/forms/forms.py +++ b/forms_builder/forms/forms.py @@ -1,38 +1,24 @@ -from __future__ import unicode_literals -from future.builtins import int, range, str - -from datetime import date, datetime -from os.path import join, split from uuid import uuid4 +from os.path import join, split +from datetime import date, datetime -import django from django import forms -try: - from django.forms import SelectDateWidget -except ImportError: - # For Django 1.8 compatibility - from django.forms.extras import SelectDateWidget -from django.core.files.storage import default_storage -try: - from django.urls import reverse -except ImportError: - # For Django 1.8 compatibility - from django.core.urlresolvers import reverse +from django.urls import reverse from django.template import Template +from django.forms import SelectDateWidget from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ +from django.core.files.storage import default_storage from forms_builder.forms import fields from forms_builder.forms.models import FormEntry, FieldEntry from forms_builder.forms import settings from forms_builder.forms.utils import now, split_choices - fs = default_storage if settings.UPLOAD_ROOT is not None: fs = default_storage.__class__(location=settings.UPLOAD_ROOT) - ############################## # Each type of export filter # ############################## @@ -92,36 +78,38 @@ # The filter function for each filter type FILTER_FUNCS = { FILTER_CHOICE_CONTAINS: - lambda val, field: val.lower() in field.lower(), + lambda val, field: val.lower() in field.lower(), FILTER_CHOICE_DOESNT_CONTAIN: - lambda val, field: val.lower() not in field.lower(), + lambda val, field: val.lower() not in field.lower(), FILTER_CHOICE_EQUALS: - lambda val, field: val.lower() == field.lower(), + lambda val, field: val.lower() == field.lower(), FILTER_CHOICE_DOESNT_EQUAL: - lambda val, field: val.lower() != field.lower(), + lambda val, field: val.lower() != field.lower(), FILTER_CHOICE_BETWEEN: - lambda val_from, val_to, field: ( - (not val_from or val_from <= field) and - (not val_to or val_to >= field) - ), + lambda val_from, val_to, field: + ((not val_from or val_from <= field) and (not val_to or val_to >= field)), FILTER_CHOICE_CONTAINS_ANY: - lambda val, field: set(val) & set(split_choices(field)), + lambda val, field: set(val) & set(split_choices(field)), FILTER_CHOICE_CONTAINS_ALL: - lambda val, field: set(val) == set(split_choices(field)), + lambda val, field: set(val) == set(split_choices(field)), FILTER_CHOICE_DOESNT_CONTAIN_ANY: - lambda val, field: not set(val) & set(split_choices(field)), + lambda val, field: not set(val) & set(split_choices(field)), FILTER_CHOICE_DOESNT_CONTAIN_ALL: - lambda val, field: set(val) != set(split_choices(field)), + lambda val, field: set(val) != set(split_choices(field)), } # Export form fields for each filter type grouping -text_filter_field = forms.ChoiceField(label=" ", required=False, +text_filter_field = forms.ChoiceField(label=" ", + required=False, choices=TEXT_FILTER_CHOICES) -choice_filter_field = forms.ChoiceField(label=" ", required=False, +choice_filter_field = forms.ChoiceField(label=" ", + required=False, choices=CHOICE_FILTER_CHOICES) -multiple_filter_field = forms.ChoiceField(label=" ", required=False, +multiple_filter_field = forms.ChoiceField(label=" ", + required=False, choices=MULTIPLE_FILTER_CHOICES) -date_filter_field = forms.ChoiceField(label=" ", required=False, +date_filter_field = forms.ChoiceField(label=" ", + required=False, choices=DATE_FILTER_CHOICES) @@ -152,14 +140,18 @@ def __init__(self, form, context, *args, **kwargs): field_key = field.slug field_class = fields.CLASSES[field.field_type] field_widget = fields.WIDGETS.get(field.field_type) - field_args = {"label": field.label, "required": field.required, - "help_text": field.help_text} + field_args = { + "label": field.label, + "required": field.required, + "help_text": field.help_text + } arg_names = field_class.__init__.__code__.co_varnames if "max_length" in arg_names: field_args["max_length"] = settings.FIELD_MAX_LENGTH if "choices" in arg_names: choices = list(field.get_choices()) - if field.field_type == fields.SELECT and not (field.required and field.default): + if field.field_type == fields.SELECT and not ( + field.required and field.default): # The first OPTION with attr. value="" display only if... # 1. ...the field is not required. # 2. ...the field is required and the default is not set. @@ -202,14 +194,16 @@ def __init__(self, form, context, *args, **kwargs): # Add identifying CSS classes to the field. css_class = field_class.__name__.lower() - # Do not add the 'required' field to the CheckboxSelectMultiple because it will + # Do not add the 'required' field to the CheckboxSelectMultiple because it will # mean that all checkboxes have to be checked instead of the usual use case of - # "at least one". - if field.required and (field_widget != forms.CheckboxSelectMultiple): + # "at least one". + if field.required and (field_widget != + forms.CheckboxSelectMultiple): css_class += " required" if settings.USE_HTML5: # Except Django version 1.10 this is necessary for all versions from 1.8 to 1.11. - self.fields[field_key].widget.attrs["required"] = "required" + self.fields[field_key].widget.attrs[ + "required"] = "required" self.fields[field_key].widget.attrs["class"] = css_class if field.placeholder_text and not field.default and field.field_type != fields.SELECT: @@ -245,11 +239,7 @@ def save(self, **kwargs): new = {"entry": entry, "field_id": field.id, "value": value} new_entry_fields.append(self.field_entry_model(**new)) if new_entry_fields: - if django.VERSION >= (1, 4, 0): - self.field_entry_model.objects.bulk_create(new_entry_fields) - else: - for field_entry in new_entry_fields: - field_entry.save() + self.field_entry_model.objects.bulk_create(new_entry_fields) return entry def email_to(self): @@ -268,8 +258,13 @@ class EntriesForm(forms.Form): filter entries for the given ``forms.models.Form`` instance. """ - def __init__(self, form, request, formentry_model=FormEntry, - fieldentry_model=FieldEntry, *args, **kwargs): + def __init__(self, + form, + request, + formentry_model=FormEntry, + fieldentry_model=FieldEntry, + *args, + **kwargs): """ Iterate through the fields of the ``forms.models.Form`` instance and create the form fields required to control including the field in @@ -283,8 +278,8 @@ def __init__(self, form, request, formentry_model=FormEntry, self.formentry_model = formentry_model self.fieldentry_model = fieldentry_model self.form_fields = form.fields.all() - self.entry_time_name = str(self.formentry_model._meta.get_field( - "entry_time").verbose_name) + self.entry_time_name = str( + self.formentry_model._meta.get_field("entry_time").verbose_name) super(EntriesForm, self).__init__(*args, **kwargs) for field in self.form_fields: field_key = "field_%s" % field.id @@ -297,15 +292,18 @@ def __init__(self, form, request, formentry_model=FormEntry, choices = ((True, _("Checked")), (False, _("Not checked"))) else: choices = field.get_choices() - contains_field = forms.MultipleChoiceField(label=" ", - choices=choices, widget=forms.CheckboxSelectMultiple(), + contains_field = forms.MultipleChoiceField( + label=" ", + choices=choices, + widget=forms.CheckboxSelectMultiple(), required=False) self.fields["%s_filter" % field_key] = choice_filter_field self.fields["%s_contains" % field_key] = contains_field elif field.is_a(*fields.MULTIPLE): # A fixed set of choices to filter by, with multiple # possible values in the entry field. - contains_field = forms.MultipleChoiceField(label=" ", + contains_field = forms.MultipleChoiceField( + label=" ", choices=field.get_choices(), widget=forms.CheckboxSelectMultiple(), required=False) @@ -340,8 +338,10 @@ def __iter__(self): """ for field_id in [f.id for f in self.form_fields] + [0]: prefix = "field_%s_" % field_id - fields = [f for f in super(EntriesForm, self).__iter__() - if f.name.startswith(prefix)] + fields = [ + f for f in super(EntriesForm, self).__iter__() + if f.name.startswith(prefix) + ] yield fields[0], fields[1], fields[2:] def posted_data(self, field): @@ -360,8 +360,10 @@ def columns(self): """ Returns the list of selected column names. """ - fields = [f.label for f in self.form_fields - if self.posted_data("field_%s_export" % f.id)] + fields = [ + f.label for f in self.form_fields + if self.posted_data("field_%s_export" % f.id) + ] if self.posted_data("field_0_export"): fields.append(self.entry_time_name) return fields @@ -393,8 +395,8 @@ def rows(self, csv=False): # Get the field entries for the given form and filter by entry_time # if specified. model = self.fieldentry_model - field_entries = model.objects.filter(entry__form=self.form - ).order_by("-entry__id").select_related("entry") + field_entries = model.objects.filter(entry__form=self.form).order_by( + "-entry__id").select_related("entry") if self.posted_data("field_0_filter") == FILTER_CHOICE_BETWEEN: time_from = self.posted_data("field_0_from") time_to = self.posted_data("field_0_to") @@ -451,7 +453,7 @@ def rows(self, csv=False): valid_row = False # Create download URL for file fields. if field_entry.value and field_id in file_field_ids: - url = reverse("admin:form_file", args=(field_entry.id,)) + url = reverse("admin:form_file", args=(field_entry.id, )) field_value = self.request.build_absolute_uri(url) if not csv: parts = (field_value, split(field_entry.value)[1]) diff --git a/forms_builder/forms/migrations/0004_alter_field_id_alter_fieldentry_id_alter_form_id_and_more.py b/forms_builder/forms/migrations/0004_alter_field_id_alter_fieldentry_id_alter_form_id_and_more.py new file mode 100644 index 00000000..704f1094 --- /dev/null +++ b/forms_builder/forms/migrations/0004_alter_field_id_alter_fieldentry_id_alter_form_id_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 4.1.5 on 2023-01-19 08:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sites', '0002_alter_domain_unique'), + ('forms', '0003_auto_20180522_0820'), + ] + + operations = [ + migrations.AlterField( + model_name='field', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='fieldentry', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='form', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='form', + name='sites', + field=models.ManyToManyField(default=[1], related_name='%(app_label)s_%(class)s_forms', to='sites.site'), + ), + migrations.AlterField( + model_name='formentry', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/forms_builder/forms/models.py b/forms_builder/forms/models.py index a4ac0d36..fc6b683b 100644 --- a/forms_builder/forms/models.py +++ b/forms_builder/forms/models.py @@ -1,19 +1,8 @@ -from __future__ import unicode_literals - -from django import VERSION as DJANGO_VERSION -from django.contrib.sites.models import Site - -try: - from django.urls import reverse -except ImportError: - # For Django 1.8 compatibility - from django.core.urlresolvers import reverse - from django.db import models from django.db.models import Q - -from django.utils.translation import ugettext, ugettext_lazy as _ -from future.builtins import str +from django.urls import reverse +from django.contrib.sites.models import Site +from django.utils.translation import gettext, gettext_lazy as _ from forms_builder.forms import fields from forms_builder.forms import settings @@ -176,10 +165,6 @@ def published(self, for_user=None): 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 - if DJANGO_VERSION <= (1, 9): - # Django 1.8 compatibility, is_authenticated has to be called as a method. - 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 @@ -205,7 +190,7 @@ def admin_links(self): **kw)), ] for i, (text, url) in enumerate(links): - links[i] = "%s" % (url, ugettext(text)) + links[i] = "%s" % (url, gettext(text)) return "
".join(links) admin_links.allow_tags = True @@ -327,9 +312,11 @@ class Meta: class FormEntry(AbstractFormEntry): - form = models.ForeignKey("Form", - related_name="entries", - on_delete=models.CASCADE) + form = models.ForeignKey( + to="Form", + related_name="entries", + on_delete=models.CASCADE, + ) class FieldEntry(AbstractFieldEntry): diff --git a/forms_builder/forms/signals.py b/forms_builder/forms/signals.py index 7da541e0..2d227404 100644 --- a/forms_builder/forms/signals.py +++ b/forms_builder/forms/signals.py @@ -1,6 +1,4 @@ -from __future__ import unicode_literals - from django.dispatch import Signal -form_invalid = Signal(providing_args=["form"]) -form_valid = Signal(providing_args=["form", "entry"]) +form_invalid = Signal(["form"]) +form_valid = Signal(["form", "entry"]) diff --git a/forms_builder/forms/templatetags/forms_builder_tags.py b/forms_builder/forms/templatetags/forms_builder_tags.py index 65ab3bb3..ff68882c 100644 --- a/forms_builder/forms/templatetags/forms_builder_tags.py +++ b/forms_builder/forms/templatetags/forms_builder_tags.py @@ -1,13 +1,9 @@ -from __future__ import unicode_literals -from future.builtins import str - from django import template from django.template.loader import get_template from forms_builder.forms.forms import FormForForm from forms_builder.forms.models import Form, AbstractForm - register = template.Library() diff --git a/forms_builder/forms/urls.py b/forms_builder/forms/urls.py index 6cc93d95..460179a9 100644 --- a/forms_builder/forms/urls.py +++ b/forms_builder/forms/urls.py @@ -1,10 +1,4 @@ -from __future__ import unicode_literals -try: - from django.urls import re_path -except ImportError: - # For Django 1.8 compatibility - from django.conf.urls import url as re_path - +from django.urls import re_path from forms_builder.forms import views diff --git a/forms_builder/forms/utils.py b/forms_builder/forms/utils.py index f8f6a74b..d14be800 100644 --- a/forms_builder/forms/utils.py +++ b/forms_builder/forms/utils.py @@ -1,16 +1,14 @@ -from __future__ import unicode_literals +from os.path import basename +from warnings import warn + +from django.template import loader +from django.core.mail import EmailMultiAlternatives, get_connection from django.template.defaultfilters import slugify as django_slugify from importlib import import_module -from unidecode import unidecode - -# Timezone support with fallback. -try: - from django.utils.timezone import now -except ImportError: - from datetime import datetime - now = datetime.now +from django.utils.timezone import now +from django.http.request import HttpRequest def slugify(s): @@ -18,8 +16,7 @@ def slugify(s): Translates unicode into closest possible ascii chars before slugifying. """ - from future.builtins import str - return django_slugify(unidecode(str(s))) + return django_slugify(str(s)) def unique_slug(manager, slug_field, slug): @@ -55,7 +52,7 @@ def html5_field(name, base): Takes a Django form field class and returns a subclass of it with the given name as its input type. """ - return type(str(""), (base,), {"input_type": name}) + return type(str(""), (base, ), {"input_type": name}) def import_attr(path): @@ -65,3 +62,131 @@ def import_attr(path): """ module_path, attr_name = path.rsplit(".", 1) return getattr(import_module(module_path), attr_name) + + +class EncryptionFailedError(Exception): + pass + + +def addresses_for_key(gpg, key): + """ + Takes a key and extracts the email addresses for it. + """ + fingerprint = key["fingerprint"] + addresses = [] + for key in gpg.list_keys(): + if key["fingerprint"] == fingerprint: + addresses.extend([ + address.split("<")[-1].strip(">") for address in key["uids"] + if address + ]) + return addresses + + +def send_mail(subject, + body_text, + addr_from, + recipient_list, + fail_silently=False, + auth_user=None, + auth_password=None, + attachments=None, + body_html=None, + html_message=None, + connection=None, + headers=None): + """ + Sends a multipart email containing text and html versions which + are encrypted for each recipient that has a valid gpg key + installed. + """ + + # Make sure only one HTML option is specified + if body_html is not None and html_message is not None: # pragma: no cover + raise ValueError("You cannot specify body_html and html_message at " + "the same time. Please only use html_message.") + + # Push users to update their code + if body_html is not None: # pragma: no cover + warn( + "Using body_html is deprecated; use the html_message argument " + "instead. Please update your code.", DeprecationWarning) + html_message = body_html + + # Allow for a single address to be passed in. + if isinstance(recipient_list, str): + recipient_list = [recipient_list] + + connection = connection or get_connection(username=auth_user, + password=auth_password, + fail_silently=fail_silently) + + # Load attachments and create name/data tuples. + attachments_parts = [] + if attachments is not None: + for attachment in attachments: + # Attachments can be pairs of name/data, or filesystem paths. + if not hasattr(attachment, "__iter__"): + with open(attachment, "rb") as f: + attachments_parts.append((basename(attachment), f.read())) + else: + attachments_parts.append(attachment) + + # Send emails - encrypted emails needs to be sent individually, while + # non-encrypted emails can be sent in one send. So the final list of + # lists of addresses to send to looks like: + # [[unencrypted1, unencrypted2, unencrypted3]] + unencrypted = [addr for addr in recipient_list] + unencrypted = [unencrypted] if unencrypted else unencrypted + + for addr_list in unencrypted: + msg = EmailMultiAlternatives(subject, + body_text, + addr_from, + addr_list, + connection=connection, + headers=headers) + if html_message is not None: + mimetype = "text/html" + msg.attach_alternative(html_message, mimetype) + for parts in attachments_parts: + name = parts[0] + msg.attach(name, parts[1]) + msg.send(fail_silently=fail_silently) + + +def send_mail_template(subject, + template, + addr_from, + recipient_list, + fail_silently=False, + attachments=None, + context=None, + connection=None, + headers=None): + """ + Send email rendering text and html versions for the specified + template name using the context dictionary passed in. + """ + + if context is None: + context = {} + + # Loads a template passing in vars as context. + def render(ext): + name = "email_extras/%s.%s" % (template, ext) + return loader.get_template(name).render(context) + + send_mail(subject, + render("txt"), + addr_from, + recipient_list, + fail_silently=fail_silently, + attachments=attachments, + html_message=render("html"), + connection=connection, + headers=headers) + + +def is_ajax(request: HttpRequest) -> bool: + return request.headers.get('x-requested-with') == 'XMLHttpRequest' \ No newline at end of file diff --git a/forms_builder/forms/views.py b/forms_builder/forms/views.py index ac484f45..9ac92304 100644 --- a/forms_builder/forms/views.py +++ b/forms_builder/forms/views.py @@ -1,21 +1,16 @@ -from __future__ import unicode_literals - import json from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME -try: - from django.urls import reverse -except ImportError: - # For Django 1.8 compatibility - from django.core.urlresolvers import reverse + +from django.urls import reverse + from django.http import HttpResponse, HttpResponseBadRequest -from django.shortcuts import get_object_or_404, redirect, render_to_response +from django.shortcuts import get_object_or_404, redirect, render from django.template import RequestContext -from django.utils.http import urlquote +from urllib.parse import quote from django.views.generic.base import TemplateView -from email_extras.utils import send_mail_template - +from forms_builder.forms.utils import send_mail_template, is_ajax from forms_builder.forms.forms import FormForForm from forms_builder.forms.models import Form from forms_builder.forms.settings import EMAIL_FAIL_SILENTLY @@ -37,7 +32,7 @@ def get(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) login_required = context["form"].login_required if login_required and not request.user.is_authenticated: - path = urlquote(request.get_full_path()) + path = quote(request.get_full_path()) bits = (settings.LOGIN_URL, REDIRECT_FIELD_NAME, path) return redirect("%s?%s=%s" % bits) return self.render_to_response(context) @@ -45,9 +40,8 @@ def get(self, request, *args, **kwargs): def post(self, request, *args, **kwargs): published = Form.objects.published(for_user=request.user) form = get_object_or_404(published, slug=kwargs["slug"]) - form_for_form = FormForForm(form, RequestContext(request), - request.POST or None, - request.FILES or None) + form_for_form = FormForForm(form, RequestContext(request), request.POST + or None, request.FILES or None) if not form_for_form.is_valid(): form_invalid.send(sender=request, form=form_for_form) else: @@ -60,14 +54,15 @@ def post(self, request, *args, **kwargs): entry = form_for_form.save() form_valid.send(sender=request, form=form_for_form, entry=entry) self.send_emails(request, form_for_form, form, entry, attachments) - if not self.request.is_ajax(): - return redirect(form.redirect_url or - reverse("form_sent", kwargs={"slug": form.slug})) + if not is_ajax(self.request): + return redirect( + form.redirect_url + or reverse("form_sent", kwargs={"slug": form.slug})) context = {"form": form, "form_for_form": form_for_form} return self.render_to_response(context) def render_to_response(self, context, **kwargs): - if self.request.method == "POST" and self.request.is_ajax(): + if self.request.method == "POST" and is_ajax(self.request): json_context = json.dumps({ "errors": context["form_for_form"].errors, "form": context["form_for_form"].as_p(), @@ -75,7 +70,7 @@ def render_to_response(self, context, **kwargs): }) if context["form_for_form"].errors: return HttpResponseBadRequest(json_context, - content_type="application/json") + content_type="application/json") return HttpResponse(json_context, content_type="application/json") return super(FormDetail, self).render_to_response(context, **kwargs) @@ -97,20 +92,28 @@ def send_emails(self, request, form_for_form, form, entry, attachments): email_from = form.email_from or settings.DEFAULT_FROM_EMAIL email_to = form_for_form.email_to() if email_to and form.send_email: - send_mail_template(subject, "form_response", email_from, - email_to, context=context, + send_mail_template(subject, + "form_response", + email_from, + email_to, + context=context, fail_silently=EMAIL_FAIL_SILENTLY) + headers = None if email_to: headers = {"Reply-To": email_to} email_copies = split_choices(form.email_copies) if email_copies: - send_mail_template(subject, "form_response_copies", email_from, - email_copies, context=context, + send_mail_template(subject, + "form_response_copies", + email_from, + email_copies, + context=context, attachments=attachments, fail_silently=EMAIL_FAIL_SILENTLY, headers=headers) + form_detail = FormDetail.as_view() @@ -120,4 +123,4 @@ def form_sent(request, slug, template="forms/form_sent.html"): """ published = Form.objects.published(for_user=request.user) context = {"form": get_object_or_404(published, slug=slug)} - return render_to_response(template, context, RequestContext(request)) + return render(request, template, context) diff --git a/forms_builder/manage.py b/forms_builder/manage.py new file mode 100755 index 00000000..e459d06e --- /dev/null +++ b/forms_builder/manage.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + +from example_project.settings import BASE_DIR + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..')) +sys.path.insert(0, os.path.abspath(os.path.join(BASE_DIR, ".."))) + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example_project.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?") from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main()