├── .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 | 96 | 97 | 98 | 99 | {%% for item in %(model)s_list %%} 100 | 101 | 102 | 103 | 104 | 105 | {%% endfor %%} 106 |
IDNameAction
{{ item.id }}{{ item }}show
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 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 |
IDNameAction
{{ %(model)s.id }}{{ %(model)s }}
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 |
4 |

{{ nested.opts.verbose_name_plural|capfirst }}

5 |
6 | 7 | {% for field in nested.fields %} 8 | 9 | {% endfor %} 10 | {% if nested.formset.can_delete %} 11 | 12 | {% endif %} 13 | 14 | 15 | 16 | 17 | {% for formset in nested %} 18 | {% if formset.form.non_field_errors %} 19 | 22 | {% endif %} 23 | {% if forloop.last %} 24 | 25 | 33 | {% for fieldset in formset %} 34 | {% for line in fieldset %} 35 | {% for field in line %} 36 | 40 | {% endfor %} 41 | {% endfor %} 42 | {% endfor %} 43 | {% if formset.original and nested.formset.can_delete %} 44 | 45 | {% endif %} 46 | 47 | {% else %} 48 | 49 | 57 | {% for fieldset in formset %} 58 | {% for line in fieldset %} 59 | {% for field in line %} 60 | 64 | {% endfor %} 65 | {% endfor %} 66 | {% endfor %} 67 | {% if formset.original and nested.formset.can_delete %} 68 | 69 | {% else %} 70 | 71 | {% endif %} 72 | 73 | {% endif %} 74 | 75 | 76 | {% endfor %} 77 | 78 | 79 |
{{ field.label|capfirst }}Delete?
20 | {{ form.form.non_field_errors }} 21 |
26 | {% if formset.original %}

27 | {{ formset.original }} 28 |

{% endif %} 29 | {% if formset.has_auto_field %} 30 | {{ formset.pk_field.field }} 31 | {% endif %}{{ formset.fk_field.field }} 32 |
37 | {{ field.field.errors.as_ul}} 38 | {{ field.field }} 39 | {{ formset.deletion_field.field }}
50 | {% if formset.original %}

51 | {{ formset.original }} 52 |

{% endif %} 53 | {% if formset.has_auto_field %} 54 | {{ formset.pk_field.field }} 55 | {% endif %}{{ formset.fk_field.field }} 56 |
61 | {{ field.field.errors.as_ul}} 62 | {{ field.field }} 63 | {{ formset.deletion_field.field }}
-------------------------------------------------------------------------------- /django_common/templates/common/admin/nested_tabular.html: -------------------------------------------------------------------------------- 1 | {% load i18n admin_modify admin_static %} 2 | 3 | {% block extrahead %} 4 | 29 | {% endblock %} 30 | 31 |
32 | 108 |
109 | 110 | 507 | -------------------------------------------------------------------------------- /django_common/templates/common/fragments/checkbox_field.html: -------------------------------------------------------------------------------- 1 |
2 | 12 |
13 | -------------------------------------------------------------------------------- /django_common/templates/common/fragments/form_field.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | {{ form_field }} 7 | {% if form_field.help_text %} 8 | {{ form_field.help_text|safe }} 9 | {% endif %} 10 | {% if form_field.errors %} 11 | {{ form_field.errors|safe }} 12 | {% endif %} 13 |
14 |
15 | -------------------------------------------------------------------------------- /django_common/templates/common/fragments/multi_checkbox_field.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 12 | {{ form_field }} 13 |
14 |
15 | -------------------------------------------------------------------------------- /django_common/templates/common/fragments/radio_field.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 12 | {{ form_field }} 13 | 14 |
15 |
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 | --------------------------------------------------------------------------------