├── .gitignore
├── .travis.yml
├── AUTHORS
├── LICENSE
├── MANIFEST.in
├── README.rst
├── django_common
├── __init__.py
├── admin.py
├── auth_backends.py
├── classmaker.py
├── compat.py
├── context_processors.py
├── db_fields.py
├── decorators.py
├── email_backends.py
├── helper.py
├── http.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ ├── generate_secret_key.py
│ │ └── scaffold.py
├── middleware.py
├── mixin.py
├── scaffold.py
├── session.py
├── settings.py
├── static
│ └── django_common
│ │ └── js
│ │ ├── ajax_form.js
│ │ └── common.js
├── templates
│ └── common
│ │ ├── admin
│ │ ├── nested.html
│ │ └── nested_tabular.html
│ │ └── fragments
│ │ ├── checkbox_field.html
│ │ ├── form_field.html
│ │ ├── multi_checkbox_field.html
│ │ └── radio_field.html
├── templatetags
│ ├── __init__.py
│ └── custom_tags.py
├── tests.py
└── tzinfo.py
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.DS_Store
3 | dist
4 | build
5 | django_common_helpers.egg-info
6 | .idea/
7 | *.egg-info
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "2.6"
4 | - "2.7"
5 | - "3.4"
6 | env:
7 | - DJANGO=1.6.9 DJANGO_SETTINGS_MODULE="django_common.test_settings"
8 | - DJANGO=1.7.4 DJANGO_SETTINGS_MODULE="django_common.test_settings"
9 | matrix:
10 | exclude:
11 | - python: "2.6"
12 | env: DJANGO=1.7.4
13 | - python: "3.4"
14 | env: DJANGO=1.6.9
15 | install:
16 | - pip install -q Django==$DJANGO --use-mirrors
17 | script:
18 | - python setup.py test
--------------------------------------------------------------------------------
/AUTHORS:
--------------------------------------------------------------------------------
1 | http://github.com/Tivix/django-common/contributors
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011 Tivix, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include django_common/templates *
2 | recursive-include django_common/static *
3 | include AUTHORS
4 | include LICENSE
5 | include MANIFEST.in
6 | include README.rst
7 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | =====================
2 | django-common-helpers
3 | =====================
4 |
5 |
6 | Overview
7 | ---------
8 |
9 | Django-common consists of the following things:
10 |
11 | - A middleware that makes sure your web-app runs either on or without 'www' in the domain.
12 |
13 | - A ``SessionManagerBase`` base class, that helps in keeping your session related code object-oriented and clean! See session.py for usage details.
14 |
15 | - An ``EmailBackend`` for authenticating users based on their email, apart from username.
16 |
17 | - Some custom db fields that you can use in your models including a ``UniqueHashField`` and ``RandomHashField``.
18 |
19 | - Bunch of helpful functions in helper.py
20 |
21 | - A ``render_form_field`` template tag that makes rendering form fields easy and DRY.
22 |
23 | - A couple of dry response classes: ``JsonResponse`` and ``XMLResponse`` in the django_common.http that can be used in views that give json/xml responses.
24 |
25 |
26 | Installation
27 | -------------
28 |
29 | - Install django_common (ideally in your virtualenv!) using pip or simply getting a copy of the code and putting it in a directory in your codebase.
30 |
31 | - Add ``django_common`` to your Django settings ``INSTALLED_APPS``::
32 |
33 | INSTALLED_APPS = [
34 | # ...
35 | "django_common",
36 | ]
37 |
38 | - Add the following to your settings.py with appropriate values:
39 |
40 | - IS_DEV
41 | - IS_PROD
42 | - DOMAIN_NAME
43 | - WWW_ROOT
44 |
45 | - Add ``common_settings`` to your Django settings ``TEMPLATE_CONTEXT_PROCESSORS``::
46 |
47 | TEMPLATE_CONTEXT_PROCESSORS = [
48 | # ...
49 | 'django_common.context_processors.common_settings',
50 | ]
51 |
52 | - Add ``EmailBackend`` to the Django settings ``AUTHENTICATION_BACKENDS``::
53 |
54 | AUTHENTICATION_BACKENDS = (
55 | 'django_common.auth_backends.EmailBackend',
56 | 'django.contrib.auth.backends.ModelBackend'
57 | )
58 |
59 | - Add ``WWWRedirectMiddleware`` if required to the list of middlewares::
60 |
61 | MIDDLEWARE_CLASSES = [
62 | # ...
63 | "WWWRedirectMiddleware",
64 | ]
65 |
66 | - Scaffolds / ajax_form.js (ajax forms) etc. require jQuery
67 |
68 |
69 | Scaffolding feature
70 | -------------------
71 |
72 | 1. Installing
73 |
74 | To get scaffold just download ``scaffold`` branch of django-common, add it to ``INSTALLED_APPS`` and set up ``SCAFFOLD_APPS_DIR`` in settings.
75 |
76 | Default is set to main app directory. However if you use django_base_project you must set up this to ``SCAFFOLD_APPS_DIR = 'apps/'``.
77 |
78 | 2. Run
79 |
80 | To run scaffold type::
81 |
82 | python manage.py scaffold APPNAME --model MODELNAME [fields]
83 |
84 | APPNAME is app name. If app does not exists it will be created.
85 | MODELNAME is model name. Just enter model name that you want to create (for example: Blog, Topic, Post etc). It must be alphanumerical. Only one model per run is allowed!
86 |
87 | [fields] - list of the model fields.
88 |
89 | 3. Field types
90 |
91 | Available fields::
92 |
93 | char - CharField
94 | text - TextField
95 | int - IntegerFIeld
96 | decimal -DecimalField
97 | datetime - DateTimeField
98 | foreign - ForeignKey
99 |
100 | All fields requires name that is provided after ``:`` sign, for example::
101 |
102 | char:title text:body int:posts datetime:create_date
103 |
104 | Two fields ``foreign`` and ``decimal`` requires additional parameters:
105 |
106 | - "foreign" as third argument takes foreignkey model, example::
107 |
108 | foreign:blog:Blog, foreign:post:Post, foreign:added_by:User
109 |
110 | NOTICE: All foreign key models must alread exist in project. User and Group model are imported automatically.
111 |
112 | - decimal field requires two more arguments ``max_digits`` and ``decimal_places``, example::
113 |
114 | decimal:total_cost:10:2
115 |
116 | NOTICE: To all models scaffold automatically adds two fields: update_date and create_date.
117 |
118 | 4. How it works?
119 |
120 | Scaffold creates models, views (CRUD), forms, templates, admin, urls and basic tests (CRUD). Scaffold templates are using two blocks extending from base.html::
121 |
122 | {% extends "base.html" %}
123 | {% block page-title %} {% endblock %}
124 | {% block conent %} {% endblock %}
125 |
126 | So be sure you have your base.html set up properly.
127 |
128 | Scaffolding example usage
129 | -------------------------
130 |
131 | Let's create very simple ``forum`` app. We need ``Forum``, ``Topic`` and ``Post`` model.
132 |
133 | - Forum model
134 |
135 | Forum model needs just one field ``name``::
136 |
137 | python manage.py scaffold forum --model Forum char:name
138 |
139 | - Topic model
140 |
141 | Topics are created by site users so we need: ``created_by``, ``title`` and ``Forum`` foreign key (``update_date`` and ``create_date`` are always added to models)::
142 |
143 | python manage.py scaffold forum --model Topic foreign:created_by:User char:title foreign:forum:Forum
144 |
145 | - Post model
146 |
147 | Last one are Posts. Posts are related to Topics. Here we need: ``title``, ``body``, ``created_by`` and foreign key to ``Topic``::
148 |
149 | python manage.py scaffold forum --model Post char:title text:body foreign:created_by:User foreign:topic:Topic
150 |
151 | All data should be in place!
152 |
153 | Now you must add ``forum`` app to ``INSTALLED_APPS`` and include app in ``urls.py`` file by adding into urlpatterns::
154 |
155 | urlpatterns = [
156 | ...
157 | url(r'^', include('forum.urls')),
158 | ]
159 |
160 | Now syncdb new app and you are ready to go::
161 |
162 | python manage.py syncdb
163 |
164 | Run your server::
165 |
166 | python manage.py runserver
167 |
168 | And go to forum main page::
169 |
170 | http://localhost:8000/forum/
171 |
172 | All structure are in place. Now you can personalize models, templates and urls.
173 |
174 | At the end you can test new app by runing test::
175 |
176 | python manage.py test forum
177 |
178 | Creating test database for alias 'default'...
179 | .......
180 | ----------------------------------------------------------------------
181 | Ran 7 tests in 0.884s
182 |
183 | OK
184 |
185 | Happy scaffolding!
186 |
187 | Generation of SECRET_KEY
188 | ------------------------
189 |
190 | Sometimes you need to generate a new ``SECRET_KEY`` so now you can generate it using this command:
191 |
192 | $ python manage.py generate_secret_key
193 |
194 | Sample output:
195 |
196 | $ python manage.py generate_secret_key
197 |
198 | SECRET_KEY: 7,=_3t?n@'wV=p`ITIA6"CUgJReZf?s:`f~Jtl#2i=i^z%rCp-
199 |
200 | Optional arguments
201 |
202 | 1. ``--length`` - is the length of the key ``default=50``
203 | 2. ``--alphabet`` - is the alphabet to use to generate the key ``default=ascii letters + punctuation symbols``
204 |
205 | Django settings keys
206 | --------------------
207 |
208 | - DOMAIN_NAME - Domain name, ``"www.example.com"``
209 | - WWW_ROOT - Root website url, ``"https://www.example.com/"``
210 | - IS_DEV - Current environment is development environment
211 | - IS_PROD - Current environment is production environment
212 |
213 |
214 | This open-source app is brought to you by Tivix, Inc. ( http://tivix.com/ )
215 |
216 |
217 | Changelog
218 | =========
219 |
220 | 0.9.2
221 | -----
222 | - Change for Django 2.X
223 |
224 | 0.9.1
225 | -----
226 | - Change for Django 1.10 - render() must be called with a dict, not a Context
227 |
228 | 0.9.0
229 | -----
230 | - Django 1.10 support
231 | - README.txt invalid characters fix
232 | - Add support for custom user model in EmailBackend
233 | - Fixes for DB fields and management commands
234 |
235 | 0.8.0
236 | -----
237 | - compatability code moved to compat.py
238 | - ``generate_secret_key`` management command.
239 | - Fix relating to https://code.djangoproject.com/ticket/17627, package name change.
240 | - Pass form fields with HiddenInput widget through render_form_field
241 | - string.format usage / other refactoring / more support for Python 3
242 |
243 |
244 | 0.7.0
245 | -----
246 | - PEP8 codebase cleanup.
247 | - Improved python3 support.
248 | - Django 1.8 support.
249 |
250 | 0.6.4
251 | -----
252 | - Added python3 support.
253 |
254 | 0.6.3
255 | -----
256 | - Changed mimetype to content_type in class JsonReponse to reflect Django 1.7 deprecation.
257 |
258 | 0.6.2
259 | -----
260 | - Django 1.7 compatability using simplejson as fallback
261 |
262 |
263 | 0.6.1
264 | -----
265 | - Added support for attaching content to emails manually (without providing path to file).
266 |
267 | - Added LoginRequiredMixin
268 |
269 |
270 | 0.6
271 | ---
272 | - Added support for Django 1.5
273 |
274 | - Added fixes in nested inlines
275 |
276 | - Added support for a multi-select checkbox field template and radio button in render_form_field
277 |
278 | - Added Test Email Backend for overwrite TO, CC and BCC fields in all outgoing emails
279 |
280 | - Added Custom File Email Backend to save emails as file with custom extension
281 |
282 | - Rewrote fragments to be Bootstrap-compatible
283 |
284 |
285 | 0.5.1
286 | -----
287 |
288 | - root_path deprecated in Django 1.4+
289 |
290 |
291 | 0.5
292 | ---
293 |
294 | - Added self.get_inline_instances() usages instead of self.inline_instances
295 |
296 | - Changed minimum requirement to Django 1.4+ because of the above.
297 |
298 |
299 | 0.4
300 | ---
301 |
302 | - Added nested inline templates, js and full ajax support. Now we can add/remove nested fields dynamically.
303 |
304 | - JsonpResponse object for padded JSON
305 |
306 | - User time tracking feature - how long the user has been on site, associated middleware etc.
307 |
308 | - @anonymous_required decorator: for views that should not be accessed by a logged-in user.
309 |
310 | - Added EncryptedTextField and EncryptedCharField
311 |
312 | - Misc. bug fixes
313 |
--------------------------------------------------------------------------------
/django_common/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tivix/django-common/407d208121011a8425139e541629554114d96c18/django_common/__init__.py
--------------------------------------------------------------------------------
/django_common/admin.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | from django.db import models
4 | from django.views.decorators.csrf import csrf_protect
5 | from django.utils.decorators import method_decorator
6 | from django.contrib.admin.options import BaseModelAdmin, ModelAdmin
7 | from django.contrib.admin.helpers import AdminForm
8 | from django.core.exceptions import PermissionDenied
9 | from django.http import Http404
10 | from django.utils.translation import ugettext as _
11 | from django.utils.html import escape
12 | from django.forms.formsets import all_valid
13 | from django.contrib.admin import helpers
14 | from django.utils.safestring import mark_safe
15 | from django.forms.models import (inlineformset_factory, BaseInlineFormSet)
16 | from django import forms
17 | from django.utils.functional import curry
18 |
19 | from django_common.compat import (atomic_decorator, force_unicode,
20 | unquote, flatten_fieldsets)
21 |
22 |
23 | csrf_protect_m = method_decorator(csrf_protect)
24 |
25 |
26 | def __init__(self, form, fieldsets, prepopulated_fields, readonly_fields=None, model_admin=None):
27 | """
28 | Monkey-patch for django 1.5
29 | """
30 | def normalize_fieldsets(fieldsets):
31 | """
32 | Make sure the keys in fieldset dictionaries are strings. Returns the
33 | normalized data.
34 | """
35 | result = []
36 |
37 | for name, options in fieldsets:
38 | result.append((name, normalize_dictionary(options)))
39 |
40 | return result
41 |
42 | def normalize_dictionary(data_dict):
43 | """
44 | Converts all the keys in "data_dict" to strings. The keys must be
45 | convertible using str().
46 | """
47 | for key, value in data_dict.items():
48 | if not isinstance(key, str):
49 | del data_dict[key]
50 | data_dict[str(key)] = value
51 |
52 | return data_dict
53 |
54 | if isinstance(prepopulated_fields, list):
55 | prepopulated_fields = dict()
56 |
57 | self.form, self.fieldsets = form, normalize_fieldsets(fieldsets)
58 | self.prepopulated_fields = [{
59 | 'field': form[field_name],
60 | 'dependencies': [form[f] for f in dependencies]
61 | } for field_name, dependencies in prepopulated_fields.items()]
62 |
63 | self.model_admin = model_admin
64 |
65 | if readonly_fields is None:
66 | readonly_fields = ()
67 |
68 | self.readonly_fields = readonly_fields
69 |
70 | AdminForm.__init__ = __init__
71 |
72 |
73 | class NestedModelAdmin(ModelAdmin):
74 |
75 | @csrf_protect_m
76 | @atomic_decorator
77 | def add_view(self, request, form_url='', extra_context=None):
78 | """The 'add' admin view for this model."""
79 | model = self.model
80 | opts = model._meta
81 |
82 | if not self.has_add_permission(request):
83 | raise PermissionDenied
84 |
85 | ModelForm = self.get_form(request)
86 | formsets = []
87 |
88 | if request.method == 'POST':
89 | form = ModelForm(request.POST, request.FILES)
90 |
91 | if form.is_valid():
92 | new_object = self.save_form(request, form, change=False)
93 | form_validated = True
94 | else:
95 | form_validated = False
96 | new_object = self.model()
97 |
98 | prefixes = {}
99 |
100 | for FormSet, inline in zip(self.get_formsets(request),
101 | self.get_inline_instances(request)):
102 | prefix = FormSet.get_default_prefix()
103 | prefixes[prefix] = prefixes.get(prefix, 0) + 1
104 |
105 | if prefixes[prefix] != 1:
106 | prefix = "{0}-{1}".format(prefix, prefixes[prefix])
107 |
108 | formset = FormSet(data=request.POST, files=request.FILES,
109 | instance=new_object,
110 | save_as_new="_saveasnew" in request.POST,
111 | prefix=prefix, queryset=inline.queryset(request))
112 |
113 | formsets.append(formset)
114 |
115 | for inline in self.get_inline_instances(request):
116 | # If this is the inline that matches this formset, and
117 | # we have some nested inlines to deal with, then we need
118 | # to get the relevant formset for each of the forms in
119 | # the current formset.
120 | if inline.inlines and inline.model == formset.model:
121 | for nested in inline.inline_instances:
122 | for the_form in formset.forms:
123 | InlineFormSet = nested.get_formset(request, the_form.instance)
124 | prefix = "{0}-{1}".format(the_form.prefix,
125 | InlineFormSet.get_default_prefix())
126 | formsets.append(InlineFormSet(request.POST, request.FILES,
127 | instance=the_form.instance,
128 | prefix=prefix))
129 | if all_valid(formsets) and form_validated:
130 | self.save_model(request, new_object, form, change=False)
131 | form.save_m2m()
132 |
133 | for formset in formsets:
134 | self.save_formset(request, form, formset, change=False)
135 |
136 | self.log_addition(request, new_object)
137 |
138 | return self.response_add(request, new_object)
139 | else:
140 | # Prepare the dict of initial data from the request.
141 | # We have to special-case M2Ms as a list of comma-separated PKs.
142 | initial = dict(request.GET.items())
143 |
144 | for k in initial:
145 | try:
146 | f = opts.get_field(k)
147 | except models.FieldDoesNotExist:
148 | continue
149 |
150 | if isinstance(f, models.ManyToManyField):
151 | initial[k] = initial[k].split(",")
152 |
153 | form = ModelForm(initial=initial)
154 | prefixes = {}
155 |
156 | for FormSet, inline in zip(self.get_formsets(request),
157 | self.get_inline_instances(request)):
158 | prefix = FormSet.get_default_prefix()
159 | prefixes[prefix] = prefixes.get(prefix, 0) + 1
160 |
161 | if prefixes[prefix] != 1:
162 | prefix = "{0}-{1}".format(prefix, prefixes[prefix])
163 |
164 | formset = FormSet(instance=self.model(), prefix=prefix,
165 | queryset=inline.queryset(request))
166 | formsets.append(formset)
167 |
168 | adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
169 | self.prepopulated_fields, self.get_readonly_fields(request),
170 | model_admin=self)
171 |
172 | media = self.media + adminForm.media
173 | inline_admin_formsets = []
174 |
175 | for inline, formset in zip(self.get_inline_instances(request), formsets):
176 | fieldsets = list(inline.get_fieldsets(request))
177 | readonly = list(inline.get_readonly_fields(request))
178 | inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
179 | fieldsets, readonly,
180 | model_admin=self)
181 | if inline.inlines:
182 | for form in formset.forms:
183 | if form.instance.pk:
184 | instance = form.instance
185 | else:
186 | instance = None
187 |
188 | form.inlines = inline.get_inlines(request, instance, prefix=form.prefix)
189 |
190 | inline_admin_formset.inlines = inline.get_inlines(request)
191 |
192 | inline_admin_formsets.append(inline_admin_formset)
193 | media = media + inline_admin_formset.media
194 |
195 | context = {
196 | 'title': _('Add %s') % force_unicode(opts.verbose_name),
197 | 'adminform': adminForm,
198 | 'is_popup': "_popup" in request.REQUEST,
199 | 'show_delete': False,
200 | 'media': mark_safe(media),
201 | 'inline_admin_formsets': inline_admin_formsets,
202 | 'errors': helpers.AdminErrorList(form, formsets),
203 | 'app_label': opts.app_label,
204 | }
205 |
206 | context.update(extra_context or {})
207 |
208 | return self.render_change_form(request, context, form_url=form_url, add=True)
209 |
210 | @csrf_protect_m
211 | @atomic_decorator
212 | def change_view(self, request, object_id, extra_context=None, **kwargs):
213 | "The 'change' admin view for this model."
214 | model = self.model
215 | opts = model._meta
216 | obj = self.get_object(request, unquote(object_id))
217 |
218 | if not self.has_change_permission(request, obj):
219 | raise PermissionDenied
220 |
221 | if obj is None:
222 | raise Http404(_('%(name)s object with primary key %(key)r does not exist.') %
223 | {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
224 |
225 | if request.method == 'POST' and "_saveasnew" in request.POST:
226 | return self.add_view(request, form_url='../add/')
227 |
228 | ModelForm = self.get_form(request, obj)
229 | formsets = []
230 |
231 | if request.method == 'POST':
232 | form = ModelForm(request.POST, request.FILES, instance=obj)
233 |
234 | if form.is_valid():
235 | form_validated = True
236 | new_object = self.save_form(request, form, change=True)
237 | else:
238 | form_validated = False
239 | new_object = obj
240 |
241 | prefixes = {}
242 |
243 | for FormSet, inline in zip(self.get_formsets(request, new_object),
244 | self.get_inline_instances(request)):
245 | prefix = FormSet.get_default_prefix()
246 | prefixes[prefix] = prefixes.get(prefix, 0) + 1
247 |
248 | if prefixes[prefix] != 1:
249 | prefix = "{0}-{1}".format(prefix, prefixes[prefix])
250 | formset = FormSet(request.POST, request.FILES,
251 | instance=new_object, prefix=prefix,
252 | queryset=inline.queryset(request))
253 |
254 | formsets.append(formset)
255 |
256 | for inline in self.get_inline_instances(request):
257 | # If this is the inline that matches this formset, and
258 | # we have some nested inlines to deal with, then we need
259 | # to get the relevant formset for each of the forms in
260 | # the current formset.
261 | if inline.inlines and inline.model == formset.model:
262 | for nested in inline.inline_instances:
263 | for the_form in formset.forms:
264 | InlineFormSet = nested.get_formset(request, the_form.instance)
265 | prefix = "{0}-{1}".format(the_form.prefix,
266 | InlineFormSet.get_default_prefix())
267 | formsets.append(InlineFormSet(request.POST, request.FILES,
268 | instance=the_form.instance,
269 | prefix=prefix))
270 | if all_valid(formsets) and form_validated:
271 | self.save_model(request, new_object, form, change=True)
272 | form.save_m2m()
273 |
274 | for formset in formsets:
275 | self.save_formset(request, form, formset, change=True)
276 |
277 | change_message = self.construct_change_message(request, form, formsets)
278 | self.log_change(request, new_object, change_message)
279 |
280 | return self.response_change(request, new_object)
281 |
282 | else:
283 | form = ModelForm(instance=obj)
284 | prefixes = {}
285 |
286 | for FormSet, inline in zip(self.get_formsets(request, obj),
287 | self.get_inline_instances(request)):
288 | prefix = FormSet.get_default_prefix()
289 | prefixes[prefix] = prefixes.get(prefix, 0) + 1
290 | if prefixes[prefix] != 1:
291 | prefix = "{0}-{1}".format(prefix, prefixes[prefix])
292 | formset = FormSet(instance=obj, prefix=prefix,
293 | queryset=inline.queryset(request))
294 | formsets.append(formset)
295 |
296 | adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
297 | self.prepopulated_fields,
298 | self.get_readonly_fields(request, obj),
299 | model_admin=self)
300 | media = self.media + adminForm.media
301 | inline_admin_formsets = []
302 |
303 | for inline, formset in zip(self.get_inline_instances(request), formsets):
304 | fieldsets = list(inline.get_fieldsets(request, obj))
305 | readonly = list(inline.get_readonly_fields(request, obj))
306 | inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets,
307 | readonly, model_admin=self)
308 | if inline.inlines:
309 | for form in formset.forms:
310 | if form.instance.pk:
311 | instance = form.instance
312 | else:
313 | instance = None
314 |
315 | form.inlines = inline.get_inlines(request, instance, prefix=form.prefix)
316 |
317 | inline_admin_formset.inlines = inline.get_inlines(request)
318 |
319 | inline_admin_formsets.append(inline_admin_formset)
320 | media = media + inline_admin_formset.media
321 |
322 | context = {
323 | 'title': _('Change %s') % force_unicode(opts.verbose_name),
324 | 'adminform': adminForm,
325 | 'object_id': object_id,
326 | 'original': obj,
327 | 'is_popup': "_popup" in request.REQUEST,
328 | 'media': mark_safe(media),
329 | 'inline_admin_formsets': inline_admin_formsets,
330 | 'errors': helpers.AdminErrorList(form, formsets),
331 | 'app_label': opts.app_label,
332 | }
333 |
334 | context.update(extra_context or {})
335 |
336 | return self.render_change_form(request, context, change=True, obj=obj)
337 |
338 | def get_inlines(self, request, obj=None, prefix=None):
339 | nested_inlines = []
340 |
341 | for inline in self.get_inline_instances(request):
342 | FormSet = inline.get_formset(request, obj)
343 | prefix = "{0}-{1}".format(prefix, FormSet.get_default_prefix())
344 | formset = FormSet(instance=obj, prefix=prefix)
345 | fieldsets = list(inline.get_fieldsets(request, obj))
346 | nested_inline = helpers.InlineAdminFormSet(inline, formset, fieldsets)
347 | nested_inlines.append(nested_inline)
348 |
349 | return nested_inlines
350 |
351 |
352 | class NestedTabularInline(BaseModelAdmin):
353 | """
354 | Options for inline editing of ``model`` instances.
355 |
356 | Provide ``name`` to specify the attribute name of the ``ForeignKey`` from
357 | ``model`` to its parent. This is required if ``model`` has more than one
358 | ``ForeignKey`` to its parent.
359 | """
360 | model = None
361 | fk_name = None
362 | formset = BaseInlineFormSet
363 | extra = 3
364 | max_num = None
365 | template = None
366 | verbose_name = None
367 | verbose_name_plural = None
368 | can_delete = True
369 | template = 'common/admin/nested_tabular.html'
370 | inlines = []
371 |
372 | def __init__(self, parent_model, admin_site):
373 | self.admin_site = admin_site
374 | self.parent_model = parent_model
375 | self.opts = self.model._meta
376 | super(NestedTabularInline, self).__init__()
377 |
378 | if self.verbose_name is None:
379 | self.verbose_name = self.model._meta.verbose_name
380 |
381 | if self.verbose_name_plural is None:
382 | self.verbose_name_plural = self.model._meta.verbose_name_plural
383 |
384 | self.inline_instances = []
385 |
386 | for inline_class in self.inlines:
387 | inline_instance = inline_class(self.model, self.admin_site)
388 | self.inline_instances.append(inline_instance)
389 |
390 | def _media(self):
391 | from django.conf import settings
392 |
393 | js = ['js/jquery.min.js', 'js/jquery.init.js', 'js/inlines.min.js']
394 |
395 | if self.prepopulated_fields:
396 | js.append('js/urlify.js')
397 | js.append('js/prepopulate.min.js')
398 |
399 | if self.filter_vertical or self.filter_horizontal:
400 | js.extend(['js/SelectBox.js', 'js/SelectFilter2.js'])
401 |
402 | return forms.Media(js=['{0}{1}'.format(settings.ADMIN_MEDIA_PREFIX, url) for url in js])
403 |
404 | media = property(_media)
405 |
406 | def get_formset(self, request, obj=None, **kwargs):
407 | """
408 | Returns a BaseInlineFormSet class for use in admin add/change views.
409 | """
410 | if self.declared_fieldsets:
411 | fields = flatten_fieldsets(self.declared_fieldsets)
412 | else:
413 | fields = None
414 | if self.exclude is None:
415 | exclude = []
416 | else:
417 | exclude = list(self.exclude)
418 |
419 | exclude.extend(kwargs.get("exclude", []))
420 | exclude.extend(self.get_readonly_fields(request, obj))
421 |
422 | # if exclude is an empty list we use None, since that's the actual
423 | # default
424 | exclude = exclude or None
425 | defaults = {
426 | "form": self.form,
427 | "formset": self.formset,
428 | "fk_name": self.fk_name,
429 | "fields": fields,
430 | "exclude": exclude,
431 | "formfield_callback": curry(self.formfield_for_dbfield, request=request),
432 | "extra": self.extra,
433 | "max_num": self.max_num,
434 | "can_delete": self.can_delete,
435 | }
436 | defaults.update(kwargs)
437 |
438 | return inlineformset_factory(self.parent_model, self.model, **defaults)
439 |
440 | def get_fieldsets(self, request, obj=None):
441 | if self.declared_fieldsets:
442 | return self.declared_fieldsets
443 |
444 | form = self.get_formset(request).form
445 | fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
446 |
447 | return [(None, {'fields': fields})]
448 |
449 | def get_inlines(self, request, obj=None, prefix=None):
450 | nested_inlines = []
451 |
452 | for inline in self.inline_instances:
453 | FormSet = inline.get_formset(request, obj)
454 | prefix = "{0}-{1}".format(prefix, FormSet.get_default_prefix())
455 | formset = FormSet(instance=obj, prefix=prefix)
456 | fieldsets = list(inline.get_fieldsets(request, obj))
457 | nested_inline = helpers.InlineAdminFormSet(inline, formset, fieldsets)
458 | nested_inlines.append(nested_inline)
459 |
460 | return nested_inlines
461 |
--------------------------------------------------------------------------------
/django_common/auth_backends.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | import logging
4 |
5 | from django.contrib.auth import get_user_model
6 | from django.contrib.auth.backends import ModelBackend
7 |
8 | User = get_user_model()
9 |
10 |
11 | class EmailBackend(ModelBackend):
12 | def authenticate(self, username=None, password=None, **kwargs):
13 | """
14 | "username" being passed is really email address and being compared to as such.
15 | """
16 | try:
17 | user = User.objects.get(email=username)
18 | if user.check_password(password):
19 | return user
20 | except (User.DoesNotExist, User.MultipleObjectsReturned):
21 | logging.warning('Unsuccessful login attempt using username/email: {0}'.format(username))
22 |
23 | return None
24 |
--------------------------------------------------------------------------------
/django_common/classmaker.py:
--------------------------------------------------------------------------------
1 | # From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/204197
2 | from __future__ import print_function, unicode_literals, with_statement, division
3 |
4 | import types
5 | import inspect
6 |
7 | # preliminary: two utility functions
8 |
9 |
10 | def skip_redundant(iterable, skipset=None):
11 | """
12 | Redundant items are repeated items or items in the original skipset.
13 | """
14 | if skipset is None:
15 | skipset = set()
16 | for item in iterable:
17 | if item not in skipset:
18 | skipset.add(item)
19 | yield item
20 |
21 |
22 | def remove_redundant(metaclasses):
23 | skipset = set([types.ClassType])
24 | for meta in metaclasses: # determines the metaclasses to be skipped
25 | skipset.update(inspect.getmro(meta)[1:])
26 | return tuple(skip_redundant(metaclasses, skipset))
27 |
28 |
29 | # now the core of the module: two mutually recursive functions
30 |
31 | memoized_metaclasses_map = {}
32 |
33 |
34 | def get_noconflict_metaclass(bases, left_metas, right_metas):
35 | """
36 | Not intended to be used outside of this module, unless you know what you are doing.
37 | """
38 | # make tuple of needed metaclasses in specified priority order
39 | metas = left_metas + tuple(map(type, bases)) + right_metas
40 | needed_metas = remove_redundant(metas)
41 |
42 | # return existing confict-solving meta, if any
43 | if needed_metas in memoized_metaclasses_map:
44 | return memoized_metaclasses_map[needed_metas]
45 | # nope: compute, memoize and return needed conflict-solving meta
46 | elif not needed_metas: # wee, a trivial case, happy us
47 | meta = type
48 | elif len(needed_metas) == 1: # another trivial case
49 | meta = needed_metas[0]
50 | # check for recursion, can happen i.e. for Zope ExtensionClasses
51 | elif needed_metas == bases:
52 | raise TypeError("Incompatible root metatypes", needed_metas)
53 | else: # gotta work ...
54 | metaname = '_' + ''.join([m.__name__ for m in needed_metas])
55 | meta = classmaker()(metaname, needed_metas, {})
56 | memoized_metaclasses_map[needed_metas] = meta
57 | return meta
58 |
59 |
60 | def classmaker(left_metas=(), right_metas=()):
61 | def make_class(name, bases, adict):
62 | metaclass = get_noconflict_metaclass(bases, left_metas, right_metas)
63 | return metaclass(name, bases, adict)
64 | return make_class
65 |
--------------------------------------------------------------------------------
/django_common/compat.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | import sys
4 | from django import VERSION
5 | from django.db import transaction
6 | from django.utils import encoding
7 |
8 | PY2 = sys.version_info[0] == 2
9 |
10 | # commit_on_success was removed in 1.8, use atomic
11 | if hasattr(transaction, 'atomic'):
12 | atomic_decorator = getattr(transaction, 'atomic')
13 | else:
14 | atomic_decorator = getattr(transaction, 'commit_on_success')
15 |
16 | # ugly hack required for Python 2/3 compat
17 | if hasattr(encoding, 'force_unicode'):
18 | force_unicode = encoding.force_unicode
19 | elif hasattr(encoding, 'force_text'):
20 | force_unicode = encoding.force_text
21 | else:
22 | force_unicode = lambda x: x
23 |
24 |
25 | if (VERSION[0] == 1 and VERSION[1] >= 8) or VERSION[0] > 1:
26 | from django.contrib.admin.utils import unquote, flatten_fieldsets
27 | else:
28 | from django.contrib.admin.util import unquote, flatten_fieldsets
29 |
30 | if not PY2:
31 | string_types = (str,)
32 | else:
33 | string_types = (str, unicode)
34 |
--------------------------------------------------------------------------------
/django_common/context_processors.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | from django.conf import settings as django_settings
4 | from django_common.session import SessionManager
5 |
6 |
7 | def common_settings(request):
8 | return {
9 | 'domain_name': django_settings.DOMAIN_NAME,
10 | 'www_root': django_settings.WWW_ROOT,
11 | 'is_dev': django_settings.IS_DEV,
12 | 'is_prod': django_settings.IS_PROD,
13 | 'usertime': SessionManager(request).get_usertime()
14 | }
15 |
--------------------------------------------------------------------------------
/django_common/db_fields.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | import binascii
4 | import random
5 | import string
6 |
7 | from django.db.models import fields
8 | from django.template.defaultfilters import slugify
9 | from django.db import models
10 | from django.core.serializers.json import DjangoJSONEncoder
11 | try:
12 | import json
13 | except ImportError:
14 | from django.utils import simplejson as json
15 |
16 |
17 | from django import forms
18 | from django.conf import settings
19 |
20 | from django_common.compat import string_types
21 | from django_common.helper import md5_hash
22 |
23 |
24 | class JSONField(models.TextField):
25 | """
26 | JSONField is a generic textfield that neatly serializes/unserializes JSON objects seamlessly
27 | """
28 |
29 | def from_db_value(self, value, expression, connection, context):
30 | return self.to_python(value)
31 |
32 | def to_python(self, value):
33 | """Convert our string value to JSON after we load it from the DB"""
34 |
35 | if value == "":
36 | return None
37 |
38 | try:
39 | if isinstance(value, string_types):
40 | return json.loads(value)
41 | except ValueError:
42 | pass
43 |
44 | return value
45 |
46 | def get_prep_value(self, value):
47 | """Convert our JSON object to a string before we save"""
48 |
49 | if value == "":
50 | return None
51 |
52 | if isinstance(value, dict):
53 | value = json.dumps(value, cls=DjangoJSONEncoder)
54 |
55 | return value
56 |
57 |
58 | class UniqueSlugField(fields.SlugField):
59 | """
60 | Represents a self-managing sluf field, that makes sure that the slug value is unique on
61 | the db table. Slugs by default get a db_index on them. The "Unique" in the class name is
62 | a misnomer since it does support unique=False
63 |
64 | @requires "prepopulate_from" in the constructor. This could be a field or a function in the
65 | model class which is using this field
66 |
67 | Defaults update_on_save to False
68 |
69 | Taken and edited from: http://www.djangosnippets.org/snippets/728/
70 | """
71 | def __init__(self, prepopulate_from='id', *args, **kwargs):
72 | if kwargs.get('update_on_save'):
73 | self.__update_on_save = kwargs.pop('update_on_save')
74 | else:
75 | self.__update_on_save = False
76 | self.prepopulate_from = prepopulate_from
77 | super(UniqueSlugField, self).__init__(*args, **kwargs)
78 |
79 | def deconstruct(self):
80 | name, path, args, kwargs = super(UniqueSlugField, self).deconstruct()
81 | kwargs['prepopulate_from'] = self.prepopulate_from
82 | return name, path, args, kwargs
83 |
84 | def pre_save(self, model_instance, add):
85 | prepopulate_field = getattr(model_instance, self.prepopulate_from)
86 | if callable(prepopulate_field):
87 | prepopulate_value = prepopulate_field()
88 | else:
89 | prepopulate_value = prepopulate_field
90 |
91 | # if object has an id, and not to update on save,
92 | # then return existig model instance's slug value
93 | if getattr(model_instance, 'id') and not self.__update_on_save:
94 | return getattr(model_instance, self.name)
95 |
96 | # if this is a previously saved object, and current
97 | # instance's slug is same as one being proposed
98 | if getattr(model_instance, 'id') \
99 | and getattr(model_instance, self.name) == slugify(prepopulate_value):
100 | return getattr(model_instance, self.name)
101 |
102 | # if a unique slug is not required (not the default of course)
103 | if not self.unique:
104 | return self.__set_and_return(model_instance, self.name, slugify(prepopulate_value))
105 |
106 | return self.__unique_slug(model_instance.__class__, model_instance, self.name,
107 | prepopulate_value)
108 |
109 | def __unique_slug(self, model, model_instance, slug_field, slug_value):
110 | orig_slug = slug = slugify(slug_value)
111 | index = 1
112 | while True:
113 | try:
114 | model.objects.get(**{slug_field: slug})
115 | index += 1
116 | slug = orig_slug + '-' + str(index)
117 | except model.DoesNotExist:
118 | return self.__set_and_return(model_instance, slug_field, slug)
119 |
120 | def __set_and_return(self, model_instance, slug_field, slug):
121 | setattr(model_instance, slug_field, slug)
122 | return slug
123 |
124 | try:
125 | from south.modelsinspector import add_introspection_rules
126 | add_introspection_rules([
127 | (
128 | [UniqueSlugField], # Class(es) these apply to
129 | [], # Positional arguments (not used)
130 | { # Keyword argument
131 | "prepopulate_from": ["prepopulate_from", {"default": 'id'}],
132 | },
133 | ),
134 | ], ["^django_common\.db_fields\.UniqueSlugField"])
135 | except ImportError:
136 | pass
137 |
138 |
139 | class RandomHashField(fields.CharField):
140 | """
141 | Store a random hash for a certain model field.
142 |
143 | @param update_on_save optional field whether to update this hash or not,
144 | everytime the model instance is saved
145 | """
146 | def __init__(self, update_on_save=False, hash_length=None, *args, **kwargs):
147 | # TODO: args & kwargs serve no purpose but to make django evolution to work
148 | self.update_on_save = update_on_save
149 | self.hash_length = hash_length
150 | super(fields.CharField, self).__init__(
151 | max_length=128, unique=True, blank=False, null=False, db_index=True,
152 | default=md5_hash(max_length=self.hash_length))
153 |
154 | def pre_save(self, model_instance, add):
155 | if not add and not self.update_on_save:
156 | return getattr(model_instance, self.name)
157 |
158 | random_hash = md5_hash(max_length=self.hash_length)
159 | setattr(model_instance, self.name, random_hash)
160 | return random_hash
161 |
162 | try:
163 | from south.modelsinspector import add_introspection_rules
164 | add_introspection_rules([
165 | (
166 | [RandomHashField], # Class(es) these apply to
167 | [], # Positional arguments (not used)
168 | { # Keyword argument
169 | "update_on_save": ["update_on_save", {"default": False}],
170 | "hash_length": ["hash_length", {"default": None}],
171 | },
172 | ),
173 | ], ["^django_common\.db_fields\.RandomHashField"])
174 | except ImportError:
175 | pass
176 |
177 |
178 | class BaseEncryptedField(models.Field):
179 | """
180 | This code is based on the djangosnippet #1095
181 | You can find the original at http://www.djangosnippets.org/snippets/1095/
182 | """
183 | def __init__(self, *args, **kwargs):
184 | cipher = kwargs.pop('cipher', 'AES')
185 | imp = __import__('Crypto.Cipher', globals(), locals(), [bytes(cipher)], -1)
186 | self.cipher = getattr(imp, cipher).new(settings.SECRET_KEY[:32])
187 | self.prefix = '${0}$'.format(cipher)
188 |
189 | max_length = kwargs.get('max_length', 40)
190 | mod = max_length % self.cipher.block_size
191 | if mod > 0:
192 | max_length += self.cipher.block_size - mod
193 | kwargs['max_length'] = max_length * 2 + len(self.prefix)
194 |
195 | models.Field.__init__(self, *args, **kwargs)
196 |
197 | def _is_encrypted(self, value):
198 | return isinstance(value, string_types) and value.startswith(self.prefix)
199 |
200 | def _get_padding(self, value):
201 | mod = len(value) % self.cipher.block_size
202 | if mod > 0:
203 | return self.cipher.block_size - mod
204 | return 0
205 |
206 | def to_python(self, value):
207 | if self._is_encrypted(value):
208 | return self.cipher.decrypt(binascii.a2b_hex(value[len(self.prefix):])).split('\0')[0]
209 | return value
210 |
211 | def get_db_prep_value(self, value, connection=None, prepared=None):
212 | if value is not None and not self._is_encrypted(value):
213 | padding = self._get_padding(value)
214 | if padding > 0:
215 | suffix = [random.choice(string.printable) for _ in range(padding - 1)]
216 | value += "\0" + ''.join(suffix)
217 | value = self.prefix + binascii.b2a_hex(self.cipher.encrypt(value))
218 | return value
219 |
220 |
221 | class EncryptedTextField(BaseEncryptedField):
222 |
223 | def from_db_value(self, value, expression, connection, context):
224 | return self.to_python(value)
225 |
226 | def get_internal_type(self):
227 | return 'TextField'
228 |
229 | def formfield(self, **kwargs):
230 | defaults = {'widget': forms.Textarea}
231 | defaults.update(kwargs)
232 | return super(EncryptedTextField, self).formfield(**defaults)
233 |
234 | try:
235 | from south.modelsinspector import add_introspection_rules
236 | add_introspection_rules([
237 | (
238 | [EncryptedTextField], [], {},
239 | ),
240 | ], ["^django_common\.db_fields\.EncryptedTextField"])
241 | except ImportError:
242 | pass
243 |
244 |
245 | class EncryptedCharField(BaseEncryptedField):
246 |
247 | def from_db_value(self, value, expression, connection, context):
248 | return self.to_python(value)
249 |
250 | def get_internal_type(self):
251 | return "CharField"
252 |
253 | def formfield(self, **kwargs):
254 | defaults = {'max_length': self.max_length}
255 | defaults.update(kwargs)
256 | return super(EncryptedCharField, self).formfield(**defaults)
257 |
258 | try:
259 | from south.modelsinspector import add_introspection_rules
260 | add_introspection_rules([
261 | (
262 | [EncryptedCharField], [], {},
263 | ),
264 | ], ["^django_common\.db_fields\.EncryptedCharField"])
265 | except ImportError:
266 | pass
267 |
--------------------------------------------------------------------------------
/django_common/decorators.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | try:
4 | from functools import wraps
5 | except ImportError:
6 | from django.utils.functional import wraps
7 |
8 | import inspect
9 |
10 | from django.conf import settings
11 | from django.http import HttpResponseRedirect
12 |
13 |
14 | def ssl_required(allow_non_ssl=False):
15 | """
16 | Views decorated with this will always get redirected to https
17 | except when allow_non_ssl is set to true.
18 | """
19 | def wrapper(view_func):
20 | def _checkssl(request, *args, **kwargs):
21 | # allow_non_ssl=True lets non-https requests to come
22 | # through to this view (and hence not redirect)
23 | if hasattr(settings, 'SSL_ENABLED') and settings.SSL_ENABLED \
24 | and not request.is_secure() and not allow_non_ssl:
25 | return HttpResponseRedirect(
26 | request.build_absolute_uri().replace('http://', 'https://'))
27 | return view_func(request, *args, **kwargs)
28 |
29 | return _checkssl
30 | return wrapper
31 |
32 |
33 | def disable_for_loaddata(signal_handler):
34 | """
35 | See: https://code.djangoproject.com/ticket/8399
36 | Disables signal from firing if its caused because of loaddata
37 | """
38 | @wraps(signal_handler)
39 | def wrapper(*args, **kwargs):
40 | for fr in inspect.stack():
41 | if inspect.getmodulename(fr[1]) == 'loaddata':
42 | return
43 | signal_handler(*args, **kwargs)
44 | return wrapper
45 |
46 |
47 | def anonymous_required(view, redirect_to=None):
48 | """
49 | Only allow if user is NOT authenticated.
50 | """
51 | if redirect_to is None:
52 | redirect_to = settings.LOGIN_REDIRECT_URL
53 |
54 | @wraps(view)
55 | def wrapper(request, *a, **k):
56 | if request.user and request.user.is_authenticated():
57 | return HttpResponseRedirect(redirect_to)
58 | return view(request, *a, **k)
59 | return wrapper
60 |
--------------------------------------------------------------------------------
/django_common/email_backends.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | import os
4 |
5 | from django.conf import settings
6 |
7 | from django.core.mail.backends.smtp import EmailBackend
8 | from django.core.mail.backends.filebased import EmailBackend as FileEmailBackend
9 | from django.core.mail import message
10 |
11 |
12 | class TestEmailBackend(EmailBackend):
13 | """
14 | Email Backend to overwrite TO, CC and BCC in all outgoing emails to custom
15 | values.
16 |
17 | Sample values from setting.py:
18 | EMAIL_BACKEND = 'django_common.email_backends.TestEmailBackend'
19 | TEST_EMAIL_TO = ['dev@tivix.com'] # default are addresses form ADMINS
20 | TEST_EMAIL_CC = ['dev-cc@tivix.com'] # default is empty list
21 | TEST_EMAIL_BCC = ['dev-bcc@tivix.com'] # default is empty list
22 | """
23 |
24 | def _send(self, email_message):
25 | """A helper method that does the actual sending."""
26 | if not email_message.recipients():
27 | return False
28 | from_email = email_message.from_email
29 | if hasattr(message, 'sanitize_address'):
30 | from_email = message.sanitize_address(email_message.from_email,
31 | email_message.encoding)
32 | if hasattr(settings, 'TEST_EMAIL_TO'):
33 | email_message.to = settings.TEST_EMAIL_TO
34 | else:
35 | email_message.to = dict(getattr(settings, 'ADMINS', ())).values()
36 | email_message.cc = getattr(settings, 'TEST_EMAIL_CC', [])
37 | email_message.bcc = getattr(settings, 'TEST_EMAIL_BCC', [])
38 | if hasattr(message, 'sanitize_address'):
39 | recipients = [message.sanitize_address(addr, email_message.encoding)
40 | for addr in email_message.recipients()]
41 | else:
42 | recipients = email_message.recipients()
43 | try:
44 | self.connection.sendmail(from_email, recipients,
45 | email_message.message().as_string())
46 | except:
47 | if not self.fail_silently:
48 | raise
49 | return False
50 | return True
51 |
52 |
53 | class CustomFileEmailBackend(FileEmailBackend):
54 | """
55 | Email Backend to save emails as file with custom extension. It makes easier
56 | to open emails in email applications, f.e. with eml extension for mozilla
57 | thunderbird.
58 |
59 | Sample values from setting.py:
60 | EMAIL_BACKEND = 'django_common.email_backends.CustomFileEmailBackend'
61 | EMAIL_FILE_PATH = '/email/file/path/'
62 | EMAIL_FILE_EXT = 'eml'
63 | """
64 |
65 | def _get_filename(self):
66 | filename = super(CustomFileEmailBackend, self)._get_filename()
67 | if hasattr(settings, 'EMAIL_FILE_EXT'):
68 | filename = '{0}.{1}'.format(os.path.splitext(filename)[0],
69 | settings.EMAIL_FILE_EXT.strip('.'))
70 | return filename
71 |
--------------------------------------------------------------------------------
/django_common/helper.py:
--------------------------------------------------------------------------------
1 | "Some common routines that can be used throughout the code."
2 | from __future__ import print_function, unicode_literals, with_statement, division
3 |
4 | import hashlib
5 | import os
6 | import logging
7 | import datetime
8 | import threading
9 |
10 | try:
11 | import json
12 | except ImportError:
13 | from django.utils import simplejson as json
14 | from django.utils.encoding import force_text
15 | from django.template import Context
16 | from django.template.loader import get_template
17 | from django.core import exceptions
18 |
19 | from django_common.tzinfo import utc, Pacific
20 |
21 |
22 | class AppException(exceptions.ValidationError):
23 | """
24 | Base class for exceptions used in our system.
25 |
26 | A common base class permits application code to distinguish between exceptions raised in
27 | our code from ones raised in libraries.
28 | """
29 | pass
30 |
31 |
32 | class InvalidContentType(AppException):
33 | def __init__(self, file_types, msg=None):
34 | if not msg:
35 | msg = 'Only the following file ' \
36 | 'content types are permitted: {0}'.format(str(file_types))
37 | super(self.__class__, self).__init__(msg)
38 | self.file_types = file_types
39 |
40 |
41 | class FileTooLarge(AppException):
42 | def __init__(self, file_size_kb, msg=None):
43 | if not msg:
44 | msg = 'Files may not be larger than {0} KB'.format(file_size_kb)
45 | super(self.__class__, self).__init__(msg)
46 | self.file_size = file_size_kb
47 |
48 |
49 | def get_class(kls):
50 | """
51 | Converts a string to a class.
52 | Courtesy:
53 | http://stackoverflow.com/q/452969/#452981
54 | """
55 | parts = kls.split('.')
56 | module = ".".join(parts[:-1])
57 | m = __import__(module)
58 | for comp in parts[1:]:
59 | m = getattr(m, comp)
60 | return m
61 |
62 |
63 | def is_among(value, *possibilities):
64 | """
65 | Ensure that the method that has been used for the request is one
66 | of the expected ones (e.g., GET or POST).
67 | """
68 | for possibility in possibilities:
69 | if value == possibility:
70 | return True
71 | raise Exception('A different request value was encountered than expected: {0}'.format(value))
72 |
73 |
74 | def form_errors_serialize(form):
75 | errors = {}
76 | for field in form.fields.keys():
77 | if field in form.errors:
78 | if form.prefix:
79 | errors['{0}-{1}'.format(form.prefix, field)] = force_text(form.errors[field])
80 | else:
81 | errors[field] = force_text(form.errors[field])
82 |
83 | if form.non_field_errors():
84 | errors['non_field_errors'] = force_text(form.non_field_errors())
85 | return {'errors': errors}
86 |
87 |
88 | def json_response(data=None, errors=None, success=True):
89 | if not errors:
90 | errors = []
91 | if not data:
92 | data = {}
93 | data.update({
94 | 'errors': errors,
95 | 'success': len(errors) == 0 and success,
96 | })
97 | return json.dumps(data)
98 |
99 |
100 | def sha224_hash():
101 | return hashlib.sha224(os.urandom(224)).hexdigest()
102 |
103 |
104 | def sha1_hash():
105 | return hashlib.sha1(os.urandom(224)).hexdigest()
106 |
107 |
108 | def md5_hash(image=None, max_length=None):
109 | # TODO: Figure out how much entropy is actually needed, and reduce the current number
110 | # of bytes if possible if doing so will result in a performance improvement.
111 | if max_length:
112 | assert max_length > 0
113 |
114 | ret = hashlib.md5(image or os.urandom(224)).hexdigest()
115 | return ret if not max_length else ret[:max_length]
116 |
117 |
118 | def start_thread(target, *args):
119 | t = threading.Thread(target=target, args=args)
120 | t.setDaemon(True)
121 | t.start()
122 |
123 |
124 | def send_mail(subject, message, from_email, recipient_emails, files=None,
125 | html=False, reply_to=None, bcc=None, cc=None, files_manually=None):
126 | """
127 | Sends email with advanced optional parameters
128 |
129 | To attach non-file content (e.g. content not saved on disk), use
130 | files_manually parameter and provide list of 3 element tuples, e.g.
131 | [('design.png', img_data, 'image/png'),] which will be passed to
132 | email.attach().
133 | """
134 | import django.core.mail
135 | try:
136 | logging.debug('Sending mail to: {0}'.format(', '.join(r for r in recipient_emails)))
137 | logging.debug('Message: {0}'.format(message))
138 | email = django.core.mail.EmailMessage(subject, message, from_email, recipient_emails,
139 | bcc, cc=cc)
140 | if html:
141 | email.content_subtype = "html"
142 | if files:
143 | for file in files:
144 | email.attach_file(file)
145 | if files_manually:
146 | for filename, content, mimetype in files_manually:
147 | email.attach(filename, content, mimetype)
148 | if reply_to:
149 | email.extra_headers = {'Reply-To': reply_to}
150 | email.send()
151 | except Exception as e:
152 | # TODO: Raise error again so that more information is included in the logs?
153 | logging.error('Error sending message [{0}] from {1} to {2} {3}'.format(
154 | subject, from_email, recipient_emails, e))
155 |
156 |
157 | def send_mail_in_thread(subject, message, from_email, recipient_emails, files=None, html=False,
158 | reply_to=None, bcc=None, cc=None, files_manually=None):
159 | start_thread(send_mail, subject, message, from_email, recipient_emails, files, html,
160 | reply_to, bcc, cc, files_manually)
161 |
162 |
163 | def send_mail_using_template(subject, template_name, from_email, recipient_emails, context_map,
164 | in_thread=False, files=None, html=False, reply_to=None, bcc=None,
165 | cc=None, files_manually=None):
166 | t = get_template(template_name)
167 | message = t.render(context_map)
168 | if in_thread:
169 | return send_mail_in_thread(subject, message, from_email, recipient_emails, files, html,
170 | reply_to, bcc, cc, files_manually)
171 | else:
172 | return send_mail(subject, message, from_email, recipient_emails, files, html, reply_to,
173 | bcc, cc, files_manually)
174 |
175 |
176 | def utc_to_pacific(timestamp):
177 | return timestamp.replace(tzinfo=utc).astimezone(Pacific)
178 |
179 |
180 | def pacific_to_utc(timestamp):
181 | return timestamp.replace(tzinfo=Pacific).astimezone(utc)
182 |
183 |
184 | def humanize_time_since(timestamp=None):
185 | """
186 | Returns a fuzzy time since. Will only return the largest time. EX: 20 days, 14 min
187 | """
188 | timeDiff = datetime.datetime.now() - timestamp
189 | days = timeDiff.days
190 | hours = timeDiff.seconds / 3600
191 | minutes = timeDiff.seconds % 3600 / 60
192 | seconds = timeDiff.seconds % 3600 % 60
193 |
194 | str = ""
195 | if days > 0:
196 | if days == 1:
197 | t_str = "day"
198 | else:
199 | t_str = "days"
200 | str += "{0} {1}".format(days, t_str)
201 | return str
202 | elif hours > 0:
203 | if hours == 1:
204 | t_str = "hour"
205 | else:
206 | t_str = "hours"
207 | str += "{0} {1}".format(hours, t_str)
208 | return str
209 | elif minutes > 0:
210 | if minutes == 1:
211 | t_str = "min"
212 | else:
213 | t_str = "mins"
214 | str += "{0} {1}".format(minutes, t_str)
215 | return str
216 | elif seconds > 0:
217 | if seconds == 1:
218 | t_str = "sec"
219 | else:
220 | t_str = "secs"
221 | str += "{0} {1}".format(seconds, t_str)
222 | return str
223 | else:
224 | return str
225 |
226 |
227 | def chunks(l, n):
228 | """
229 | split successive n-sized chunks from a list.
230 | """
231 | for i in range(0, len(l), n):
232 | yield l[i:i + n]
233 |
--------------------------------------------------------------------------------
/django_common/http.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | from django.http import HttpResponse
4 |
5 | try:
6 | import json
7 | except ImportError:
8 | from django.utils import simplejson as json
9 |
10 |
11 | class JsonResponse(HttpResponse):
12 | def __init__(self, data=None, errors=None, success=True):
13 | """
14 | data is a map, errors a list
15 | """
16 | if not errors:
17 | errors = []
18 | if not data:
19 | data = {}
20 | json_resp = json_response(data=data, errors=errors, success=success)
21 | super(JsonResponse, self).__init__(json_resp, content_type='application/json')
22 |
23 |
24 | class JsonpResponse(HttpResponse):
25 | """
26 | Padded JSON response, used for widget XSS
27 | """
28 | def __init__(self, request, data=None, errors=None, success=True):
29 | """
30 | data is a map, errors a list
31 | """
32 | if not errors:
33 | errors = []
34 | if not data:
35 | data = {}
36 | json_resp = json_response(data=data, errors=errors, success=success)
37 | js = "{0}({1})".format(request.GET.get("jsonp", "jsonp_callback"), json_resp)
38 | super(JsonpResponse, self).__init__(js, mimetype='application/javascipt')
39 |
40 |
41 | def json_response(data=None, errors=None, success=True):
42 | if not errors:
43 | errors = []
44 | if not data:
45 | data = {}
46 | data.update({
47 | 'errors': errors,
48 | 'success': len(errors) == 0 and success,
49 | })
50 | return json.dumps(data)
51 |
52 |
53 | class XMLResponse(HttpResponse):
54 | def __init__(self, data):
55 | """
56 | data is the entire xml body/document
57 | """
58 | super(XMLResponse, self).__init__(data, mimetype='text/xml')
59 |
--------------------------------------------------------------------------------
/django_common/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tivix/django-common/407d208121011a8425139e541629554114d96c18/django_common/management/__init__.py
--------------------------------------------------------------------------------
/django_common/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tivix/django-common/407d208121011a8425139e541629554114d96c18/django_common/management/commands/__init__.py
--------------------------------------------------------------------------------
/django_common/management/commands/generate_secret_key.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | from django.core.management.base import BaseCommand
4 | from django.utils.crypto import get_random_string
5 | from django.utils.translation import ugettext as _
6 |
7 | import string
8 |
9 |
10 | class Command(BaseCommand):
11 | help = _('This command generates SECRET_KEY')
12 |
13 | # Default length is 50
14 | length = 50
15 |
16 | # Allowed characters
17 | allowed_chars = string.ascii_lowercase + string.ascii_uppercase + string.digits + string.punctuation
18 |
19 | def add_arguments(self, parser):
20 | """
21 | Define optional arguments with default values
22 | """
23 | parser.add_argument('--length', default=self.length,
24 | type=int, help=_('SECRET_KEY length default=%d' % self.length))
25 |
26 | parser.add_argument('--alphabet', default=self.allowed_chars,
27 | type=str, help=_('alphabet to use default=%s' % self.allowed_chars))
28 |
29 | def handle(self, *args, **options):
30 | length = options.get('length')
31 | alphabet = options.get('alphabet')
32 | secret_key = str(get_random_string(length=length, allowed_chars=alphabet))
33 |
34 | print('SECRET_KEY: %s' % secret_key)
35 |
--------------------------------------------------------------------------------
/django_common/management/commands/scaffold.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | from django.core.management.base import BaseCommand
4 |
5 | from django_common.scaffold import Scaffold
6 | from django_common import settings
7 |
8 |
9 | class Command(BaseCommand):
10 | def add_arguments(self, parser):
11 | parser.add_argument('app_name', nargs='*')
12 | parser.add_argument(
13 | '--model', default=None, dest='model', nargs='+', help="""
14 | model name - only one model name per run is allowed. \n
15 | It requires additional fields parameters:
16 |
17 | char - CharField \t\t\t\t
18 | text - TextField \t\t\t\t
19 | int - IntegerFIeld \t\t\t\t
20 | decimal -DecimalField \t\t\t\t
21 | datetime - DateTimeField \t\t\t\t
22 | foreign - ForeignKey \t\t\t\t
23 |
24 | Example usages: \t\t\t\t
25 |
26 | --model forum char:title text:body int:posts datetime:create_date \t\t
27 | --model blog foreign:blog:Blog, foreign:post:Post, foreign:added_by:User \t\t
28 | --model finance decimal:total_cost:10:2
29 |
30 | """
31 | )
32 |
33 | def handle(self, *args, **options):
34 | if len(options['app_name']) == 0:
35 | print("You must provide app name. For example:\n\npython manage.py scallfold my_app\n")
36 | return
37 |
38 | app_name = options['app_name'][0]
39 | model_data = options['model']
40 | if model_data:
41 | model_name = model_data[0]
42 | fields = model_data[1:]
43 | else:
44 | model_name = None
45 | fields = None
46 |
47 | scaffold = Scaffold(app_name, model_name, fields)
48 | scaffold.run()
49 |
50 | def get_version(self):
51 | return 'django-common version: {0}'.format(settings.VERSION)
52 |
--------------------------------------------------------------------------------
/django_common/middleware.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | from django.conf import settings
4 | from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect
5 | from django_common.session import SessionManager
6 |
7 | WWW = 'www'
8 |
9 |
10 | class WWWRedirectMiddleware(object):
11 | """
12 | Redirect requests for example from http://www.mysirw.com/* to http://mysite.com/*
13 | """
14 | def process_request(self, request):
15 | if settings.IS_PROD and request.get_host() != settings.DOMAIN_NAME:
16 | proto_suffix = 's' if request.is_secure() else ''
17 | url = 'http{0}://{1}{2}'.format(proto_suffix, settings.DOMAIN_NAME,
18 | request.get_full_path())
19 | return HttpResponsePermanentRedirect(url)
20 | return None
21 |
22 |
23 | class UserTimeTrackingMiddleware(object):
24 | """
25 | Tracking time user have been on site
26 | """
27 | def process_request(self, request):
28 | if request.user and request.user.is_authenticated():
29 | SessionManager(request).ping_usertime()
30 | else:
31 | SessionManager(request).clear_usertime()
32 |
33 |
34 | class SSLRedirectMiddleware(object):
35 | """
36 | Redirects all the requests that are non SSL to a SSL url
37 | """
38 | def process_request(self, request):
39 | if not request.is_secure():
40 | url = 'https://{0}{1}'.format(settings.DOMAIN_NAME, request.get_full_path())
41 | return HttpResponseRedirect(url)
42 | return None
43 |
44 |
45 | class NoSSLRedirectMiddleware(object):
46 | """
47 | Redirects if a non-SSL required view is hit. This middleware assumes a SSL protected view
48 | has been decorated by the 'ssl_required' decorator (see decorators.py)
49 |
50 | Redirects to https for admin though only for PROD
51 | """
52 | __DECORATOR_INNER_FUNC_NAME = '_checkssl'
53 |
54 | def __is_in_admin(self, request):
55 | return True if request.path.startswith('/admin/') else False
56 |
57 | def process_view(self, request, view_func, view_args, view_kwargs):
58 | if view_func.func_name != self.__DECORATOR_INNER_FUNC_NAME \
59 | and not (self.__is_in_admin(request) and settings.IS_PROD) \
60 | and request.is_secure(): # request is secure, but view is not decorated
61 | url = 'http://{0}{1}'.format(settings.DOMAIN_NAME, request.get_full_path())
62 | return HttpResponseRedirect(url)
63 | elif self.__is_in_admin(request) and not request.is_secure() and settings.IS_PROD:
64 | url = 'https://{0}{1}'.format(settings.DOMAIN_NAME, request.get_full_path())
65 | return HttpResponseRedirect(url)
66 |
--------------------------------------------------------------------------------
/django_common/mixin.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | from django.contrib.auth.decorators import login_required
4 | from django.utils.decorators import method_decorator
5 |
6 |
7 | class LoginRequiredMixin(object):
8 | @method_decorator(login_required)
9 | def dispatch(self, request, *args, **kwargs):
10 | return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)
11 |
--------------------------------------------------------------------------------
/django_common/scaffold.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | from os import path, system, listdir, sys, mkdir
4 | from django.conf import settings
5 | # VIEW CONSTS
6 |
7 | LIST_VIEW = """
8 | from %(app)s.forms import %(model)sForm
9 | def %(lower_model)s_list(request, template='%(lower_model)s/list.html'):
10 | d = {}
11 | d['form'] = %(model)sForm()
12 | if request.method == 'POST':
13 | form = %(model)sForm(request.POST)
14 | if form.is_valid():
15 | item = form.save()
16 | return JsonResponse(data={'id': item.id, 'name': str(item), 'form': %(model)sForm().as_p(), 'token': get_token(request)})
17 | else:
18 | d['form'] = form
19 | return JsonResponse(data={'form': d['form'].as_p(), 'token': get_token(request)}, success=False)
20 | d['%(lower_model)s_list'] = %(model)s.objects.all()
21 | return render(request, template, d)
22 | """
23 |
24 | DETAILS_VIEW = """
25 | from %(app)s.forms import %(model)sForm
26 | def %(lower_model)s_details(request, id, template='%(lower_model)s/details.html'):
27 | d = {}
28 | item = get_object_or_404(%(model)s, pk=id)
29 | d['form'] = %(model)sForm(instance=item)
30 | if request.method == 'POST':
31 | form = %(model)sForm(request.POST, instance=item)
32 | if form.is_valid():
33 | item = form.save()
34 | return JsonResponse(data={'form': %(model)sForm(instance=item).as_p(), 'token': get_token(request)})
35 | else:
36 | d['form'] = form
37 | return JsonResponse(data={'form': d['form'].as_p(), 'token': get_token(request)}, success=False)
38 | d['%(lower_model)s'] = %(model)s.objects.get(pk=id)
39 | return render(request, template, d)
40 | """
41 |
42 | DELETE_VIEW = """
43 | def %(lower_model)s_delete(request, id):
44 | item = %(model)s.objects.get(pk=id)
45 | item.delete()
46 | return JsonResponse()
47 | """
48 | # MODELS CONSTS
49 |
50 | MODEL_TEMPLATE = """
51 | class %s(models.Model):
52 | %s
53 | update_date = models.DateTimeField(auto_now=True)
54 | create_date = models.DateTimeField(auto_now_add=True)
55 |
56 | class Meta:
57 | ordering = ['-id']
58 | """
59 |
60 | IMPORT_MODEL_TEMPLATE = """from %(app)s.models import %(model)s"""
61 |
62 | CHARFIELD_TEMPLATE = """
63 | %(name)s = models.CharField(max_length=%(length)s, null=%(null)s, blank=%(null)s)
64 | """
65 |
66 | TEXTFIELD_TEMPLATE = """
67 | %(name)s = models.TextField(null=%(null)s, blank=%(null)s)
68 | """
69 |
70 | INTEGERFIELD_TEMPLATE = """
71 | %(name)s = models.IntegerField(null=%(null)s, default=%(default)s)
72 | """
73 |
74 | DECIMALFIELD_TEMPLATE = """
75 | %(name)s = models.DecimalField(max_digits=%(digits)s, decimal_places=%(places)s, null=%(null)s, default=%(default)s)
76 | """
77 |
78 | DATETIMEFIELD_TEMPLATE = """
79 | %(name)s = models.DateTimeField(null=%(null)s, default=%(default)s)
80 | """
81 |
82 | FOREIGNFIELD_TEMPLATE = """
83 | %(name)s = models.ForeignKey(%(foreign)s, null=%(null)s, blank=%(null)s)
84 | """
85 |
86 | TEMPLATE_LIST_CONTENT = """
87 | {%% extends "base.html" %%}
88 |
89 | {%% block page-title %%}%(title)s{%% endblock %%}
90 |
91 | {%% block content %%}
92 |
%(model)s list
93 |
94 |
95 | ID |
96 | Name |
97 | Action |
98 |
99 | {%% for item in %(model)s_list %%}
100 |
101 | {{ item.id }} |
102 | {{ item }} |
103 | show |
104 |
105 | {%% endfor %%}
106 |
107 |
108 |
109 |
118 |
119 |
136 | {%% endblock %%}
137 | """
138 |
139 | TEMPLATE_DETAILS_CONTENT = """
140 | {%% extends "base.html" %%}
141 |
142 | {%% block page-title %%}%(title)s - {{ %(model)s }} {%% endblock %%}
143 |
144 | {%% block content %%}
145 |
146 |
%(model)s - {{ %(model)s }}
147 |
159 |
160 |
161 |
162 |
163 |
172 |
173 |
174 |
196 | back to list
197 | {%% endblock %%}
198 | """
199 |
200 | URL_CONTENT = """
201 | from django.conf.urls import url
202 | from django.contrib.auth import views as auth_views
203 |
204 | from %(app)s import views
205 |
206 | urlpatterns = [
207 | url(r'^%(model)s/$', views.%(model)s_list, name='%(model)s-list'),
208 | url(r'^%(model)s/(?P\d+)/$', views.%(model)s_details, name='%(model)s-details'),
209 | url(r'^%(model)s/(?P\d+)/delete/$', views.%(model)s_delete, name='%(model)s-delete'),
210 | ]
211 | """
212 |
213 | URL_EXISTS_CONTENT = """
214 | url(r'^%(model)s/$', views.%(model)s_list, name='%(model)s-list'),
215 | url(r'^%(model)s/(?P\d+)/$', views.%(model)s_details, name='%(model)s-details'),
216 | url(r'^%(model)s/(?P\d+)/delete/$', views.%(model)s_delete, name='%(model)s-delete'),
217 | """
218 |
219 | ADMIN_CONTENT = """
220 | from %(app)s.models import %(model)s
221 | admin.site.register(%(model)s)
222 | """
223 |
224 | FORM_CONTENT = """
225 |
226 | from %(app)s.models import %(model)s
227 |
228 | class %(model)sForm(forms.ModelForm):
229 | class Meta:
230 | model = %(model)s
231 | """
232 |
233 | TESTS_CONTENT = """
234 |
235 | from %(app)s.models import %(model)s
236 |
237 |
238 | class %(model)sTest(TestCase):
239 |
240 | def setUp(self):
241 | self.user = User.objects.create(username='test_user')
242 |
243 | def tearDown(self):
244 | self.user.delete()
245 |
246 | def test_list(self):
247 | response = self.client.get(reverse('%(lower_model)s-list'))
248 | self.failUnlessEqual(response.status_code, 200)
249 |
250 | def test_crud(self):
251 | # Create new instance
252 | response = self.client.post(reverse('%(lower_model)s-list'), {})
253 | self.assertContains(response, '"success": true')
254 |
255 | # Read instance
256 | items = %(model)s.objects.all()
257 | self.failUnlessEqual(items.count(), 1)
258 | item = items[0]
259 | response = self.client.get(reverse('%(lower_model)s-details', kwargs={'id': item.id}))
260 | self.failUnlessEqual(response.status_code, 200)
261 |
262 | # Update instance
263 | response = self.client.post(reverse('%(lower_model)s-details', kwargs={'id': item.id}), {})
264 | self.assertContains(response, '"success": true')
265 |
266 | # Delete instance
267 | response = self.client.post(reverse('%(lower_model)s-delete', kwargs={'id': item.id}), {})
268 | self.assertContains(response, '"success": true')
269 | items = %(model)s.objects.all()
270 | self.failUnlessEqual(items.count(), 0)
271 |
272 | """
273 |
274 |
275 | class Scaffold(object):
276 | def _info(self, msg, indent=0):
277 | print("{0} {1}".format("\t" * int(indent), msg))
278 |
279 | def __init__(self, app, model, fields):
280 | self.app = app
281 | self.model = model
282 | self.fields = fields
283 |
284 | try:
285 | self.SCAFFOLD_APPS_DIR = settings.SCAFFOLD_APPS_DIR
286 | except:
287 | self.SCAFFOLD_APPS_DIR = './'
288 |
289 | def get_import(self, model):
290 | for dir in listdir(self.SCAFFOLD_APPS_DIR):
291 | if path.isdir('{0}{1}'.format(self.SCAFFOLD_APPS_DIR, dir)) \
292 | and path.exists('{0}{1}/models.py'.format(self.SCAFFOLD_APPS_DIR, dir)):
293 | with open('{0}{1}/models.py'.format(self.SCAFFOLD_APPS_DIR, dir), 'r') as fp:
294 | # Check if model exists
295 | for line in fp.readlines():
296 | if 'class {0}(models.Model)'.format(model) in line:
297 | # print "Foreign key '%s' was found in app %s..." % (model, dir)
298 | return IMPORT_MODEL_TEMPLATE % {'app': dir, 'model': model}
299 | return None
300 |
301 | def is_imported(self, path, model):
302 | with open(path, 'r') as import_file:
303 | for line in import_file.readlines():
304 | if 'import {0}'.format(model) in line:
305 | # print "Foreign key '%s' was found in models.py..." % (foreign)
306 | return True
307 | return False
308 |
309 | def add_global_view_imports(self, path):
310 | # from django.shortcuts import render, redirect, get_object_or_404, get_list_or_404
311 | import_list = list()
312 |
313 | with open(path, 'r') as import_file:
314 | need_import_shortcut = True
315 | need_import_urlresolvers = True
316 | need_import_users = True
317 | need_import_token = True
318 | need_import_JsonResponse = True
319 |
320 | for line in import_file.readlines():
321 | if 'from django.shortcuts import render, redirect, get_object_or_404' in line:
322 | need_import_shortcut = False
323 |
324 | if 'from django.core.urlresolvers import reverse' in line:
325 | need_import_urlresolvers = False
326 |
327 | if 'from django.contrib.auth.models import User, Group' in line:
328 | need_import_users = False
329 |
330 | if 'from django.middleware.csrf import get_token' in line:
331 | need_import_token = False
332 |
333 | if 'from django_common.http import JsonResponse' in line:
334 | need_import_JsonResponse = False
335 |
336 | if need_import_shortcut:
337 | import_list.append(
338 | 'from django.shortcuts import render, redirect, get_object_or_404')
339 | if need_import_urlresolvers:
340 | import_list.append('from django.core.urlresolvers import reverse')
341 | if need_import_users:
342 | import_list.append('from django.contrib.auth.models import User, Group')
343 | if need_import_token:
344 | import_list.append('from django.middleware.csrf import get_token')
345 | if need_import_JsonResponse:
346 | import_list.append('from django_common.http import JsonResponse')
347 |
348 | return import_list
349 |
350 | def view_exists(self, path, view):
351 | # Check if view already exists
352 | with open(path, 'r') as view_file:
353 | for line in view_file.readlines():
354 | if 'def {0}('.format(view) in line:
355 | return True
356 | return False
357 |
358 | def get_field(self, field):
359 | field = field.split(':')
360 | field_type = field[0]
361 |
362 | if field_type.lower() == 'char':
363 | try:
364 | length = field[2]
365 | except IndexError:
366 | length = 255
367 |
368 | try:
369 | null = field[3]
370 | null = 'False'
371 | except IndexError:
372 | null = 'True'
373 |
374 | return CHARFIELD_TEMPLATE % {'name': field[1], 'length': length, 'null': null}
375 | elif field_type.lower() == 'text':
376 | try:
377 | null = field[2]
378 | null = 'False'
379 | except IndexError:
380 | null = 'True'
381 |
382 | return TEXTFIELD_TEMPLATE % {'name': field[1], 'null': null}
383 | elif field_type.lower() == 'int':
384 | try:
385 | null = field[2]
386 | null = 'False'
387 | except IndexError:
388 | null = 'True'
389 |
390 | try:
391 | default = field[3]
392 | except IndexError:
393 | default = None
394 |
395 | return INTEGERFIELD_TEMPLATE % {'name': field[1], 'null': null, 'default': default}
396 | elif field_type.lower() == 'decimal':
397 | try:
398 | null = field[4]
399 | null = 'False'
400 | except IndexError:
401 | null = 'True'
402 |
403 | try:
404 | default = field[5]
405 | except IndexError:
406 | default = None
407 |
408 | return DECIMALFIELD_TEMPLATE % {
409 | 'name': field[1],
410 | 'digits': field[2],
411 | 'places': field[3],
412 | 'null': null,
413 | 'default': default,
414 | }
415 | elif field_type.lower() == 'datetime':
416 | try:
417 | null = field[2]
418 | null = 'False'
419 | except IndexError:
420 | null = 'True'
421 |
422 | try:
423 | default = field[3]
424 | except IndexError:
425 | default = None
426 |
427 | return DATETIMEFIELD_TEMPLATE % {'name': field[1], 'null': null, 'default': default}
428 | elif field_type.lower() == 'foreign':
429 | foreign = field[2]
430 | name = field[1]
431 | # Check if this foreign key is already in models.py
432 | if foreign in ('User', 'Group'):
433 | if not self.is_imported('{0}{1}/models.py'.format(self.SCAFFOLD_APPS_DIR,
434 | self.app), foreign):
435 | self.imports.append('\nfrom django.contrib.auth.models import User, Group\n')
436 | return FOREIGNFIELD_TEMPLATE % {'name': name, 'foreign': foreign, 'null': 'True'}
437 | if self.is_imported('{0}{1}/models.py'.format(
438 | self.SCAFFOLD_APPS_DIR, self.app), foreign):
439 | return FOREIGNFIELD_TEMPLATE % {'name': name, 'foreign': foreign, 'null': 'True'}
440 | # Check imports
441 | if self.get_import(foreign):
442 | self.imports.append(self.get_import(foreign))
443 | return FOREIGNFIELD_TEMPLATE % {'name': name, 'foreign': foreign, 'null': 'True'}
444 |
445 | self._info('error\t{0}{1}/models.py\t{2} class not found'.format(
446 | self.SCAFFOLD_APPS_DIR, self.app, foreign), 1)
447 | return None
448 |
449 | def create_app(self):
450 | self._info(" App ")
451 | self._info("===========")
452 | if self.SCAFFOLD_APPS_DIR and not path.exists('{0}'.format(self.SCAFFOLD_APPS_DIR)):
453 | raise Exception(
454 | "SCAFFOLD_APPS_DIR {0} does not exists".format(self.SCAFFOLD_APPS_DIR))
455 | if not path.exists('{0}{1}'.format(self.SCAFFOLD_APPS_DIR, self.app)):
456 | system('python manage.py startapp {0}'.format(self.app))
457 | system('mv {0} {1}{2}'.format(self.app, self.SCAFFOLD_APPS_DIR, self.app))
458 | self._info("create\t{0}{1}".format(self.SCAFFOLD_APPS_DIR, self.app), 1)
459 | else:
460 | self._info("exists\t{0}{1}".format(self.SCAFFOLD_APPS_DIR, self.app), 1)
461 |
462 | def create_views(self):
463 | self._info(" Views ")
464 | self._info("===========")
465 | # Open models.py to read
466 | view_path = '{0}{1}/views.py'.format(self.SCAFFOLD_APPS_DIR, self.app)
467 |
468 | # Check if urls.py exists
469 | if path.exists('{0}{1}/views.py'.format(self.SCAFFOLD_APPS_DIR, self.app)):
470 | self._info('exists\t{0}{1}/views.py'.format(self.SCAFFOLD_APPS_DIR, self.app), 1)
471 | else:
472 | with open("{0}{1}/views.py".format(self.SCAFFOLD_APPS_DIR, self.app), 'w'):
473 | self._info('create\t{0}{1}/views.py'.format(self.SCAFFOLD_APPS_DIR, self.app), 1)
474 |
475 | import_list = list()
476 | view_list = list()
477 |
478 | # Add global imports
479 | import_list.append('\n'.join(imp for imp in self.add_global_view_imports(view_path)))
480 |
481 | # Add model imports
482 | if not self.is_imported(view_path, self.model):
483 | import_list.append(self.get_import(self.model))
484 |
485 | lower_model = self.model.lower()
486 |
487 | # Check if view already exists
488 | if not self.view_exists(view_path, "{0}_list".format(lower_model)):
489 | view_list.append(LIST_VIEW % {
490 | 'lower_model': lower_model,
491 | 'model': self.model,
492 | 'app': self.app,
493 | })
494 | self._info("added \t{0}\t{1}_view".format(view_path, lower_model), 1)
495 | else:
496 | self._info("exists\t{0}\t{1}_view".format(view_path, lower_model), 1)
497 |
498 | if not self.view_exists(view_path, "{0}_details".format(lower_model)):
499 | view_list.append(DETAILS_VIEW % {
500 | 'lower_model': lower_model,
501 | 'model': self.model,
502 | 'app': self.app,
503 | })
504 | self._info("added \t{0}\t{1}_details".format(view_path, lower_model), 1)
505 | else:
506 | self._info("exists\t{0}\t{1}_details".format(view_path, lower_model), 1)
507 |
508 | if not self.view_exists(view_path, "{0}_delete".format(lower_model)):
509 | view_list.append(DELETE_VIEW % {
510 | 'lower_model': lower_model,
511 | 'model': self.model,
512 | })
513 | self._info("added \t{0}\t{1}_delete".format(view_path, lower_model), 1)
514 | else:
515 | self._info("exists\t{0}\t{1}_delete".format(view_path, lower_model), 1)
516 |
517 | # Open views.py to append
518 | with open(view_path, 'a') as view_file:
519 | view_file.write('\n'.join([import_line for import_line in import_list]))
520 | view_file.write(''.join([view for view in view_list]))
521 |
522 | def create_model(self):
523 | self._info(" Model ")
524 | self._info("===========")
525 |
526 | # Open models.py to read
527 | with open('{0}{1}/models.py'.format(self.SCAFFOLD_APPS_DIR, self.app), 'r') as fp:
528 | self.models_file = fp
529 |
530 | # Check if model already exists
531 | for line in self.models_file.readlines():
532 | if 'class {0}'.format(self.model) in line:
533 | self._info('exists\t{0}{1}/models.py'.format(self.SCAFFOLD_APPS_DIR, self.app), 1)
534 | return
535 |
536 | self._info('create\t{0}{1}/models.py'.format(self.SCAFFOLD_APPS_DIR, self.app), 1)
537 |
538 | # Prepare fields
539 | self.imports = []
540 | fields = []
541 |
542 | for field in self.fields:
543 | new_field = self.get_field(field)
544 |
545 | if new_field:
546 | fields.append(new_field)
547 | self._info('added\t{0}{1}/models.py\t{2} field'.format(
548 | self.SCAFFOLD_APPS_DIR, self.app, field.split(':')[1]), 1)
549 |
550 | # Open models.py to append
551 | with open('{0}{1}/models.py'.format(self.SCAFFOLD_APPS_DIR, self.app), 'a') as fp:
552 | fp.write(''.join([import_line for import_line in self.imports]))
553 | fp.write(MODEL_TEMPLATE % (self.model, ''.join(field for field in fields)))
554 |
555 | def create_templates(self):
556 | self._info(" Templates ")
557 | self._info("===========")
558 |
559 | # Check if template dir exists
560 |
561 | if path.exists('{0}{1}/templates/'.format(self.SCAFFOLD_APPS_DIR, self.app)):
562 | self._info('exists\t{0}{1}/templates/'.format(self.SCAFFOLD_APPS_DIR, self.app), 1)
563 | else:
564 | mkdir("{0}{1}/templates/".format(self.SCAFFOLD_APPS_DIR, self.app))
565 | self._info('create\t{0}{1}/templates/'.format(self.SCAFFOLD_APPS_DIR, self.app), 1)
566 |
567 | # Check if model template dir exists
568 |
569 | if path.exists('{0}{1}/templates/{2}/'.format(self.SCAFFOLD_APPS_DIR, self.app,
570 | self.model.lower())):
571 | self._info('exists\t{0}{1}/templates/{2}/'.format(self.SCAFFOLD_APPS_DIR, self.app,
572 | self.model.lower()), 1)
573 | else:
574 | mkdir("{0}{1}/templates/{2}/".format(self.SCAFFOLD_APPS_DIR, self.app,
575 | self.model.lower()))
576 | self._info('create\t{0}{1}/templates/{2}/'.format(
577 | self.SCAFFOLD_APPS_DIR, self.app, self.model.lower()), 1)
578 |
579 | # Check if list.html exists
580 |
581 | if path.exists('{0}{1}/templates/{2}/list.html'.format(self.SCAFFOLD_APPS_DIR, self.app,
582 | self.model.lower())):
583 | self._info('exists\t{0}{1}/templates/{2}/list.html'.format(
584 | self.SCAFFOLD_APPS_DIR, self.app, self.model.lower()), 1)
585 | else:
586 | with open("{0}{1}/templates/{2}/list.html".format(self.SCAFFOLD_APPS_DIR, self.app,
587 | self.model.lower()), 'w') as fp:
588 | fp.write(TEMPLATE_LIST_CONTENT % {
589 | 'model': self.model.lower(),
590 | 'title': self.model.lower(),
591 | })
592 | self._info('create\t{0}{1}/templates/{2}/list.html'.format(
593 | self.SCAFFOLD_APPS_DIR, self.app, self.model.lower()), 1)
594 |
595 | # Check if details.html exists
596 |
597 | if path.exists('{0}{1}/templates/{2}/details.html'.format(
598 | self.SCAFFOLD_APPS_DIR, self.app, self.model.lower())):
599 | self._info('exists\t{0}{1}/templates/{2}/details.html'.format(
600 | self.SCAFFOLD_APPS_DIR, self.app, self.model.lower()), 1)
601 | else:
602 | with open("{0}{1}/templates/{2}/details.html".format(
603 | self.SCAFFOLD_APPS_DIR, self.app, self.model.lower()), 'w') as fp:
604 | fp.write(TEMPLATE_DETAILS_CONTENT % {
605 | 'model': self.model.lower(),
606 | 'title': self.model.lower(),
607 | })
608 | self._info('create\t{0}{1}/templates/{2}/details.html'.format(
609 | self.SCAFFOLD_APPS_DIR, self.app, self.model.lower()), 1)
610 |
611 | def create_urls(self):
612 | self._info(" URLs ")
613 | self._info("===========")
614 |
615 | # Check if urls.py exists
616 |
617 | if path.exists('{0}{1}/urls.py'.format(self.SCAFFOLD_APPS_DIR, self.app)):
618 |
619 | # If does we need to add urls
620 | new_urls = ''
621 | with open("{0}{1}/urls.py".format(self.SCAFFOLD_APPS_DIR, self.app), 'r') as fp:
622 | for line in fp.readlines():
623 | new_urls += line
624 | if 'urlpatterns' in line:
625 | new_urls += URL_EXISTS_CONTENT % {
626 | 'app': self.app,
627 | 'model': self.model.lower(),
628 | }
629 | with open("{0}{1}/urls.py".format(self.SCAFFOLD_APPS_DIR, self.app), 'w') as fp:
630 | fp.write(new_urls)
631 | self._info('update\t{0}{1}/urls.py'.format(self.SCAFFOLD_APPS_DIR, self.app), 1)
632 | else:
633 | with open("{0}{1}/urls.py".format(self.SCAFFOLD_APPS_DIR, self.app), 'w') as fp:
634 | fp.write(URL_CONTENT % {
635 | 'app': self.app,
636 | 'model': self.model.lower(),
637 | })
638 |
639 | self._info('create\t{0}{1}/urls.py'.format(self.SCAFFOLD_APPS_DIR, self.app), 1)
640 |
641 | def create_admin(self):
642 | self._info(" Admin ")
643 | self._info("===========")
644 |
645 | # Check if admin.py exists
646 |
647 | if path.exists('{0}{1}/admin.py'.format(self.SCAFFOLD_APPS_DIR, self.app)):
648 | self._info('exists\t{0}{1}/admin.py'.format(self.SCAFFOLD_APPS_DIR, self.app), 1)
649 | else:
650 | with open("{0}{1}/admin.py".format(self.SCAFFOLD_APPS_DIR, self.app), 'w') as fp:
651 | fp.write("from django.contrib import admin\n")
652 | self._info('create\t{0}{1}/urls.py'.format(self.SCAFFOLD_APPS_DIR, self.app), 1)
653 |
654 | # Check if admin entry already exists
655 |
656 | with open("{0}{1}/admin.py".format(self.SCAFFOLD_APPS_DIR, self.app), 'r') as fp:
657 | content = fp.read()
658 | if "admin.site.register({0})".format(self.model) in content:
659 | self._info('exists\t{0}{1}/admin.py\t{2}'.format(self.SCAFFOLD_APPS_DIR, self.app,
660 | self.model.lower()), 1)
661 | else:
662 | with open("{0}{1}/admin.py".format(self.SCAFFOLD_APPS_DIR, self.app), 'a') as fp:
663 | fp.write(ADMIN_CONTENT % {'app': self.app, 'model': self.model})
664 | self._info('added\t{0}{1}/admin.py\t{2}'.format(self.SCAFFOLD_APPS_DIR, self.app,
665 | self.model.lower()), 1)
666 |
667 | def create_forms(self):
668 | self._info(" Forms ")
669 | self._info("===========")
670 |
671 | # Check if forms.py exists
672 | if path.exists('{0}{1}/forms.py'.format(self.SCAFFOLD_APPS_DIR, self.app)):
673 | self._info('exists\t{0}{1}/forms.py'.format(self.SCAFFOLD_APPS_DIR, self.app), 1)
674 | else:
675 | with open("{0}{1}/forms.py".format(self.SCAFFOLD_APPS_DIR, self.app), 'w') as fp:
676 | fp.write("from django import forms\n")
677 | self._info('create\t{0}{1}/forms.py'.format(self.SCAFFOLD_APPS_DIR, self.app), 1)
678 |
679 | # Check if form entry already exists
680 |
681 | with open("{0}{1}/forms.py".format(self.SCAFFOLD_APPS_DIR, self.app), 'r') as fp:
682 | content = fp.read()
683 | if "class {0}Form".format(self.model) in content:
684 | self._info('exists\t{0}{1}/forms.py\t{2}'.format(
685 | self.SCAFFOLD_APPS_DIR, self.app, self.model.lower()), 1)
686 | else:
687 | with open("{0}{1}/forms.py".format(self.SCAFFOLD_APPS_DIR, self.app), 'a') as fp:
688 | fp.write(FORM_CONTENT % {'app': self.app, 'model': self.model})
689 | self._info('added\t{0}{1}/forms.py\t{2}'.format(
690 | self.SCAFFOLD_APPS_DIR, self.app, self.model.lower()), 1)
691 |
692 | def create_tests(self):
693 | self._info(" Tests ")
694 | self._info("===========")
695 |
696 | # Check if tests.py exists
697 | if path.exists('{0}{1}/tests.py'.format(self.SCAFFOLD_APPS_DIR, self.app)):
698 | self._info('exists\t{0}{1}/tests.py'.format(self.SCAFFOLD_APPS_DIR, self.app), 1)
699 | # Check if imports exists:
700 | import_testcase = True
701 | import_user = True
702 | import_reverse = True
703 |
704 | with open("{0}{1}/tests.py".format(self.SCAFFOLD_APPS_DIR, self.app), 'r') as fp:
705 | for line in fp.readlines():
706 | if 'import TestCase' in line:
707 | import_testcase = False
708 | if 'import User' in line:
709 | import_user = False
710 | if 'import reverse' in line:
711 | import_reverse = False
712 |
713 | with open("{0}{1}/tests.py".format(self.SCAFFOLD_APPS_DIR, self.app), 'a') as fp:
714 | if import_testcase:
715 | fp.write("from django.test import TestCase\n")
716 | if import_user:
717 | fp.write("from django.contrib.auth.models import User\n")
718 | if import_reverse:
719 | fp.write("from django.core.urlresolvers import reverse\n")
720 | else:
721 | with open("{0}{1}/tests.py".format(self.SCAFFOLD_APPS_DIR, self.app), 'w') as fp:
722 | fp.write("from django.test import TestCase\n")
723 | fp.write("from django.contrib.auth.models import User\n")
724 | fp.write("from django.core.urlresolvers import reverse\n")
725 | self._info('create\t{0}{1}/tests.py'.format(self.SCAFFOLD_APPS_DIR, self.app), 1)
726 |
727 | # Check if test class already exists
728 | with open("{0}{1}/tests.py".format(self.SCAFFOLD_APPS_DIR, self.app), 'r') as fp:
729 | content = fp.read()
730 | if "class {0}Test".format(self.model) in content:
731 | self._info('exists\t{0}{1}/tests.py\t{2}'.format(
732 | self.SCAFFOLD_APPS_DIR, self.app, self.model.lower()), 1)
733 | else:
734 | with open("{0}{1}/tests.py".format(self.SCAFFOLD_APPS_DIR, self.app), 'a') as fp:
735 | fp.write(TESTS_CONTENT % {
736 | 'app': self.app,
737 | 'model': self.model,
738 | 'lower_model': self.model.lower(),
739 | })
740 |
741 | self._info('added\t{0}{1}/tests.py\t{2}'.format(self.SCAFFOLD_APPS_DIR, self.app,
742 | self.model.lower()), 1)
743 |
744 | def run(self):
745 | if not self.app:
746 | sys.exit("No application name found...")
747 | if not self.app.isalnum():
748 | sys.exit("Model name should be alphanumerical...")
749 | self.create_app()
750 | if self.model:
751 | self.create_model()
752 | self.create_views()
753 | self.create_admin()
754 | self.create_forms()
755 | self.create_urls()
756 | self.create_templates()
757 | self.create_tests()
758 |
--------------------------------------------------------------------------------
/django_common/session.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | from datetime import datetime, timedelta
4 | from django.conf import settings
5 |
6 |
7 | class SessionManagerBase(object):
8 | """
9 | Base class that a "SessionManager" concrete class should extend.
10 | It should have a list called _SESSION_KEYS that lists all the keys that class uses/depends on.
11 |
12 | Ideally each app has a session.py that has this class and is used in the apps views etc.
13 | """
14 | def __init__(self, request, prepend_key_with=''):
15 | self._session = request.session
16 | self._prepend_key_with = prepend_key_with
17 |
18 | def _get_or_set(self, key, value):
19 | key = '{0}{1}'.format(self._prepend_key_with, key)
20 |
21 | if value is not None:
22 | self._session[key] = value
23 | return value
24 | return self._session.get(key)
25 |
26 | def reset_keys(self):
27 | for key in self._SESSION_KEYS:
28 | key = '{0}{1}'.format(self._prepend_key_with, key)
29 |
30 | if key in self._session:
31 | del self._session[key]
32 |
33 |
34 | class SessionManager(SessionManagerBase):
35 | """Manages storing the cart"""
36 |
37 | USER_ONLINE_TIMEOUT = 180 # 3 min
38 |
39 | USERTIME = 'usertime'
40 | _GENERIC_VAR_KEY_PREFIX = 'lpvar_' # handles generic stuff being stored in the session
41 |
42 | _SESSION_KEYS = [
43 | USERTIME,
44 | ]
45 |
46 | def __init__(self, request):
47 | super(SessionManager, self).__init__(request, prepend_key_with=request.get_host())
48 | if not self._get_or_set(self.USERTIME, None):
49 | self._get_or_set(self.USERTIME, None)
50 |
51 | def get_usertime(self):
52 | usertime = self._get_or_set(self.USERTIME, None)
53 | try:
54 | return usertime['last'] - usertime['start']
55 | except:
56 | return 0
57 |
58 | def ping_usertime(self):
59 | # Override default user online timeout
60 | try:
61 | timeout = int(settings.USER_ONLINE_TIMEOUT)
62 | except:
63 | timeout = self.USER_ONLINE_TIMEOUT
64 | if not self._get_or_set(self.USERTIME, None):
65 | self._get_or_set(self.USERTIME, {'start': datetime.now(), 'last': datetime.now()})
66 | else:
67 | usertime = self._get_or_set(self.USERTIME, None)
68 | if usertime['last'] + timedelta(seconds=timeout) < datetime.now():
69 | # This mean user reached timeout - we start from begining
70 | self._get_or_set(self.USERTIME, {'start': datetime.now(), 'last': datetime.now()})
71 | else:
72 | # We just update last time
73 | usertime['last'] = datetime.now()
74 | return self._get_or_set(self.USERTIME, None)
75 |
76 | def clear_usertime(self):
77 | return self._get_or_set(self.USERTIME, {})
78 |
79 | def generic_var(self, key, value=None):
80 | """
81 | Stores generic variables in the session prepending it with _GENERIC_VAR_KEY_PREFIX.
82 | """
83 | return self._get_or_set('{0}{1}'.format(self._GENERIC_VAR_KEY_PREFIX, key), value)
84 |
--------------------------------------------------------------------------------
/django_common/settings.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | VERSION = '0.9.2'
4 |
--------------------------------------------------------------------------------
/django_common/static/django_common/js/ajax_form.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Helper class for forms, mostly helps with ajax form submits etc.
3 | *
4 | * + Assumes there is an image with class 'ajax-indicator' on the page somewhere.
5 | */
6 | function FormHelper(form_id) {
7 | if (form_id) {
8 | this.__form = $('#' + form_id);
9 | } else {
10 | this.__form = $('form');
11 | }
12 | }
13 |
14 | FormHelper.prototype.bind_for_ajax = function(success_handler, failure_handler) {
15 | var self=this;
16 | this.__form.submit(function() {
17 | self.ajax_submit(success_handler, failure_handler);
18 | return false;
19 | });
20 | }
21 |
22 | FormHelper.prototype.ajax_submit = function(success_handler, failure_handler) {
23 | this.__clear_errors();
24 | this.__form.find('img.ajax-indicator').show();
25 |
26 | var self=this;
27 | $.post(this.__form.attr('action'), this.__form.serialize(),
28 | function(data) {
29 | if (data.success) {
30 | success_handler(data);
31 | } else if (failure_handler != undefined) {
32 | failure_handler(data);
33 | } else {
34 | self.__fill_errors(data);
35 | }
36 | self.__form.find('img.ajax-indicator').hide();
37 | },
38 | "json");
39 |
40 | this.__toggle_inputs_disable_state(true);
41 | };
42 |
43 | FormHelper.prototype.__fill_errors = function(data) {
44 | if (data.form != undefined) {
45 | for (var field in data.form.errors) {
46 | if (field != 'non_field_errors') {
47 | this.__form.find('#id_error_container_' + field).html(data.form.errors[field]);
48 | this.__form.find('#id_' + field + '_container').addClass('errorRow').addClass('errRow');
49 | } else {
50 | this.__form.prepend('' +
51 | data.form.errors['non_field_errors'] + '
');
52 | }
53 | }
54 | }
55 | if (data.errors.length > 0) {
56 | this.__form.prepend('' +
57 | data.errors + '
');
58 | }
59 |
60 | this.__toggle_inputs_disable_state(false);
61 | };
62 |
63 | FormHelper.prototype.__toggle_inputs_disable_state = function(disable) {
64 | this.__form.find('input, select').attr('disabled', disable);
65 | }
66 |
67 | FormHelper.prototype.__clear_errors = function() {
68 | this.__form.find('div.error_container').empty();
69 | this.__form.find('div.formRow').removeClass('errorRow').removeClass('errRow');
70 | $('#id_non_field_errors').remove();
71 | };
72 |
--------------------------------------------------------------------------------
/django_common/static/django_common/js/common.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Common js functions
3 | *
4 | */
5 |
6 | function confirmModal(msg) {
7 | if (confirm(msg)) {
8 | return true;
9 | } else {
10 | return false;
11 | }
12 | };
--------------------------------------------------------------------------------
/django_common/templates/common/admin/nested.html:
--------------------------------------------------------------------------------
1 | {{ nested.formset.management_form }}
2 |
3 |
79 |
--------------------------------------------------------------------------------
/django_common/templates/common/admin/nested_tabular.html:
--------------------------------------------------------------------------------
1 | {% load i18n admin_modify admin_static %}
2 |
3 | {% block extrahead %}
4 |
29 | {% endblock %}
30 |
31 |
109 |
110 |
507 |
--------------------------------------------------------------------------------
/django_common/templates/common/fragments/checkbox_field.html:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
--------------------------------------------------------------------------------
/django_common/templates/common/fragments/form_field.html:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/django_common/templates/common/fragments/multi_checkbox_field.html:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/django_common/templates/common/fragments/radio_field.html:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/django_common/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tivix/django-common/407d208121011a8425139e541629554114d96c18/django_common/templatetags/__init__.py
--------------------------------------------------------------------------------
/django_common/templatetags/custom_tags.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | from django import template
4 | from django.forms import widgets
5 | from django.template.loader import get_template
6 |
7 | register = template.Library()
8 |
9 |
10 | class FormFieldNode(template.Node):
11 | """
12 | Helper class for the render_form_field below
13 | """
14 | def __init__(self, form_field, help_text=None, css_classes=None):
15 | self.form_field = template.Variable(form_field)
16 | self.help_text = help_text[1:-1] if help_text else help_text
17 | self.css_classes = css_classes[1:-1] if css_classes else css_classes
18 |
19 | def render(self, context):
20 |
21 | try:
22 | form_field = self.form_field.resolve(context)
23 | except template.VariableDoesNotExist:
24 | return ''
25 |
26 | widget = form_field.field.widget
27 |
28 | if isinstance(widget, widgets.HiddenInput):
29 | return form_field
30 | elif isinstance(widget, widgets.RadioSelect):
31 | t = get_template('common/fragments/radio_field.html')
32 | elif isinstance(widget, widgets.CheckboxInput):
33 | t = get_template('common/fragments/checkbox_field.html')
34 | elif isinstance(widget, widgets.CheckboxSelectMultiple):
35 | t = get_template('common/fragments/multi_checkbox_field.html')
36 | else:
37 | t = get_template('common/fragments/form_field.html')
38 |
39 | help_text = self.help_text
40 | if help_text is None:
41 | help_text = form_field.help_text
42 |
43 | return t.render({
44 | 'form_field': form_field,
45 | 'help_text': help_text,
46 | 'css_classes': self.css_classes
47 | })
48 |
49 |
50 | @register.tag
51 | def render_form_field(parser, token):
52 | """
53 | Usage is {% render_form_field form.field_name optional_help_text optional_css_classes %}
54 |
55 | - optional_help_text and optional_css_classes are strings
56 | - if optional_help_text is not given, then it is taken from form field object
57 | """
58 | try:
59 | help_text = None
60 | css_classes = None
61 |
62 | token_split = token.split_contents()
63 | if len(token_split) == 4:
64 | tag_name, form_field, help_text, css_classes = token.split_contents()
65 | elif len(token_split) == 3:
66 | tag_name, form_field, help_text = token.split_contents()
67 | else:
68 | tag_name, form_field = token.split_contents()
69 | except ValueError:
70 | raise template.TemplateSyntaxError(
71 | "Unable to parse arguments for {0}".format(repr(token.contents.split()[0])))
72 |
73 | return FormFieldNode(form_field, help_text=help_text, css_classes=css_classes)
74 |
75 |
76 | @register.simple_tag
77 | def active(request, pattern):
78 | """
79 | Returns the string 'active' if pattern matches.
80 | Used to assign a css class in navigation bars to active tab/section.
81 | """
82 | if request.path == pattern:
83 | return 'active'
84 | return ''
85 |
86 |
87 | @register.simple_tag
88 | def active_starts(request, pattern):
89 | """
90 | Returns the string 'active' if request url starts with pattern.
91 | Used to assign a css class in navigation bars to active tab/section.
92 | """
93 | if request.path.startswith(pattern):
94 | return 'active'
95 | return ''
96 |
--------------------------------------------------------------------------------
/django_common/tests.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | from django.utils import unittest
4 | from django.core.management import call_command
5 |
6 | try:
7 | from cStringIO import StringIO
8 | except ImportError:
9 | from StringIO import StringIO
10 |
11 | import sys
12 | import random
13 |
14 |
15 | class SimpleTestCase(unittest.TestCase):
16 | def setUp(self):
17 | pass
18 |
19 | def test_generate_secret_key(self):
20 | """ Test generation of a secret key """
21 | out = StringIO()
22 | sys.stdout = out
23 |
24 | for i in range(10):
25 | random_number = random.randrange(10, 100)
26 | call_command('generate_secret_key', length=random_number)
27 | secret_key = self._get_secret_key(out.getvalue())
28 |
29 | out.truncate(0)
30 | out.seek(0)
31 |
32 | assert len(secret_key) == random_number
33 |
34 | def _get_secret_key(self, result):
35 | """ Get only the value of a SECRET_KEY """
36 | for index, key in enumerate(result):
37 | if key == ':':
38 | return str(result[index + 1:]).strip()
39 |
--------------------------------------------------------------------------------
/django_common/tzinfo.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals, with_statement, division
2 |
3 | # From the python documentation
4 | # http://docs.python.org/library/datetime.html
5 | from datetime import tzinfo, timedelta, datetime
6 |
7 | ZERO = timedelta(0)
8 | HOUR = timedelta(hours=1)
9 |
10 | # A UTC class.
11 |
12 |
13 | class UTC(tzinfo):
14 | """
15 | UTC
16 | """
17 | def utcoffset(self, dt):
18 | return ZERO
19 |
20 | def tzname(self, dt):
21 | return "UTC"
22 |
23 | def dst(self, dt):
24 | return ZERO
25 |
26 | utc = UTC()
27 |
28 | # A class building tzinfo objects for fixed-offset time zones.
29 | # Note that FixedOffset(0, "UTC") is a different way to build a
30 | # UTC tzinfo object.
31 |
32 |
33 | class FixedOffset(tzinfo):
34 | """
35 | Fixed offset in minutes east from UTC.
36 | """
37 | def __init__(self, offset, name):
38 | self.__offset = timedelta(minutes=offset)
39 | self.__name = name
40 |
41 | def utcoffset(self, dt):
42 | return self.__offset
43 |
44 | def tzname(self, dt):
45 | return self.__name
46 |
47 | def dst(self, dt):
48 | return ZERO
49 |
50 | # A class capturing the platform's idea of local time.
51 |
52 | import time as _time
53 |
54 | STDOFFSET = timedelta(seconds=-_time.timezone)
55 | if _time.daylight:
56 | DSTOFFSET = timedelta(seconds=-_time.altzone)
57 | else:
58 | DSTOFFSET = STDOFFSET
59 |
60 | DSTDIFF = DSTOFFSET - STDOFFSET
61 |
62 |
63 | class LocalTimezone(tzinfo):
64 | def utcoffset(self, dt):
65 | if self._isdst(dt):
66 | return DSTOFFSET
67 | else:
68 | return STDOFFSET
69 |
70 | def dst(self, dt):
71 | if self._isdst(dt):
72 | return DSTDIFF
73 | else:
74 | return ZERO
75 |
76 | def tzname(self, dt):
77 | return _time.tzname[self._isdst(dt)]
78 |
79 | def _isdst(self, dt):
80 | tt = (dt.year, dt.month, dt.day,
81 | dt.hour, dt.minute, dt.second,
82 | dt.weekday(), 0, -1)
83 | stamp = _time.mktime(tt)
84 | tt = _time.localtime(stamp)
85 | return tt.tm_isdst > 0
86 |
87 | Local = LocalTimezone()
88 |
89 |
90 | # A complete implementation of current DST rules for major US time zones.
91 |
92 | def first_sunday_on_or_after(dt):
93 | days_to_go = 6 - dt.weekday()
94 | if days_to_go:
95 | dt += timedelta(days_to_go)
96 | return dt
97 |
98 |
99 | # US DST Rules
100 | #
101 | # This is a simplified (i.e., wrong for a few cases) set of rules for US
102 | # DST start and end times. For a complete and up-to-date set of DST rules
103 | # and timezone definitions, visit the Olson Database (or try pytz):
104 | # http://www.twinsun.com/tz/tz-link.htm
105 | # http://sourceforge.net/projects/pytz/ (might not be up-to-date)
106 | #
107 | # In the US, since 2007, DST starts at 2am (standard time) on the second
108 | # Sunday in March, which is the first Sunday on or after Mar 8.
109 | DSTSTART_2007 = datetime(1, 3, 8, 2)
110 | # and ends at 2am (DST time; 1am standard time) on the first Sunday of Nov.
111 | DSTEND_2007 = datetime(1, 11, 1, 1)
112 | # From 1987 to 2006, DST used to start at 2am (standard time) on the first
113 | # Sunday in April and to end at 2am (DST time; 1am standard time) on the last
114 | # Sunday of October, which is the first Sunday on or after Oct 25.
115 | DSTSTART_1987_2006 = datetime(1, 4, 1, 2)
116 | DSTEND_1987_2006 = datetime(1, 10, 25, 1)
117 | # From 1967 to 1986, DST used to start at 2am (standard time) on the last
118 | # Sunday in April (the one on or after April 24) and to end at 2am (DST time;
119 | # 1am standard time) on the last Sunday of October, which is the first Sunday
120 | # on or after Oct 25.
121 | DSTSTART_1967_1986 = datetime(1, 4, 24, 2)
122 | DSTEND_1967_1986 = DSTEND_1987_2006
123 |
124 |
125 | class USTimeZone(tzinfo):
126 | def __init__(self, hours, reprname, stdname, dstname):
127 | self.stdoffset = timedelta(hours=hours)
128 | self.reprname = reprname
129 | self.stdname = stdname
130 | self.dstname = dstname
131 |
132 | def __repr__(self):
133 | return self.reprname
134 |
135 | def tzname(self, dt):
136 | if self.dst(dt):
137 | return self.dstname
138 | else:
139 | return self.stdname
140 |
141 | def utcoffset(self, dt):
142 | return self.stdoffset + self.dst(dt)
143 |
144 | def dst(self, dt):
145 | if dt is None or dt.tzinfo is None:
146 | # An exception may be sensible here, in one or both cases.
147 | # It depends on how you want to treat them. The default
148 | # fromutc() implementation (called by the default astimezone()
149 | # implementation) passes a datetime with dt.tzinfo is self.
150 | return ZERO
151 | assert dt.tzinfo is self
152 |
153 | # Find start and end times for US DST. For years before 1967, return
154 | # ZERO for no DST.
155 | if 2006 < dt.year:
156 | dststart, dstend = DSTSTART_2007, DSTEND_2007
157 | elif 1986 < dt.year < 2007:
158 | dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006
159 | elif 1966 < dt.year < 1987:
160 | dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986
161 | else:
162 | return ZERO
163 |
164 | start = first_sunday_on_or_after(dststart.replace(year=dt.year))
165 | end = first_sunday_on_or_after(dstend.replace(year=dt.year))
166 |
167 | # Can't compare naive to aware objects, so strip the timezone from
168 | # dt first.
169 | if start <= dt.replace(tzinfo=None) < end:
170 | return HOUR
171 | else:
172 | return ZERO
173 |
174 | Eastern = USTimeZone(-5, "Eastern", "EST", "EDT")
175 | Central = USTimeZone(-6, "Central", "CST", "CDT")
176 | Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
177 | Pacific = USTimeZone(-8, "Pacific", "PST", "PDT")
178 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | try:
4 | from setuptools import setup, find_packages
5 | except ImportError:
6 | from ez_setup import use_setuptools
7 | use_setuptools()
8 | from setuptools import setup, find_packages
9 |
10 | from django_common import settings
11 |
12 | import os
13 |
14 | here = os.path.dirname(os.path.abspath(__file__))
15 | f = open(os.path.join(here, 'README.rst'))
16 | long_description = f.read().strip()
17 | f.close()
18 |
19 | setup(
20 | name='django-common-helpers',
21 | version=settings.VERSION,
22 | author='Tivix',
23 | author_email='dev@tivix.com',
24 | url='http://github.com/tivix/django-common',
25 | description='Common things every Django app needs!',
26 | packages=find_packages(),
27 | long_description=long_description,
28 | keywords='django',
29 | zip_safe=False,
30 | install_requires=[
31 | 'Django>=1.8.0'
32 | ],
33 | test_suite='django_common.tests',
34 | include_package_data=True,
35 | classifiers=[
36 | 'Framework :: Django',
37 | 'Intended Audience :: Developers',
38 | 'Intended Audience :: System Administrators',
39 | 'Operating System :: OS Independent',
40 | 'Topic :: Software Development'
41 | ],
42 | )
43 |
--------------------------------------------------------------------------------