Skip to content
This repository was archived by the owner on Apr 16, 2020. It is now read-only.

Commit dc92d91

Browse files
author
primal100
committed
Fresh Start
0 parents  commit dc92d91

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

92 files changed

+7511
-0
lines changed

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
*.pyc
2+
*.log
3+
.idea/*
4+
build/*
5+
dist/*
6+
docs/_*
7+
django_postgres_extensions.egg-info/*

LICENSE

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Copyright (c) Paul Martin and all contributors.
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without modification,
5+
are permitted provided that the following conditions are met:
6+
7+
1. Redistributions of source code must retain the above copyright notice,
8+
this list of conditions and the following disclaimer.
9+
10+
2. Redistributions in binary form must reproduce the above copyright
11+
notice, this list of conditions and the following disclaimer in the
12+
documentation and/or other materials provided with the distribution.
13+
14+
3. My name may not be used to endorse or promote products
15+
derived from this software without specific prior written permission.
16+
17+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
21+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
24+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

MANIFEST.in

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
include LICENSE
2+
include readme.rst
3+
include description.rst

description.rst

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Django Postgres Extensions!
2+
3+
Django Postgres Extensions adds a lot of functionality to Django.contrib.postgres, specifically in relation to ArrayField, HStoreField and JSONField, including much better form fields for dealing with these field types. The app also includes an Array Many To Many Field, so you can store the relationship in an array column instead of requiring an extra database table.
4+
5+
Check out http://django-postgres-extensions.readthedocs.io/en/latest/ to get started.
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
default_app_config = 'django_postgres_extensions.apps.PSQLExtensionsConfig'
2+
__version__ = "0.9.2"

django_postgres_extensions/admin/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from django.contrib.admin.options import ModelAdmin
2+
from django.utils.translation import string_concat, ugettext as _
3+
from django.forms.widgets import CheckboxSelectMultiple, SelectMultiple
4+
from django_postgres_extensions.models import ArrayManyToManyField
5+
from django.contrib.admin import widgets
6+
7+
class PostgresAdmin(ModelAdmin):
8+
9+
def formfield_for_manytomany(self, db_field, request=None, **kwargs):
10+
"""
11+
Get a form Field for a ManyToManyField.
12+
"""
13+
# If it uses an intermediary model that isn't auto created, don't show
14+
# a field in admin.
15+
if hasattr(db_field, 'through') and not db_field.remote_field.through._meta.auto_created:
16+
return None
17+
db = kwargs.get('using')
18+
19+
if db_field.name in self.raw_id_fields:
20+
kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.remote_field,
21+
self.admin_site, using=db)
22+
kwargs['help_text'] = ''
23+
elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
24+
kwargs['widget'] = widgets.FilteredSelectMultiple(
25+
db_field.verbose_name,
26+
db_field.name in self.filter_vertical
27+
)
28+
29+
if 'queryset' not in kwargs:
30+
queryset = self.get_field_queryset(db, db_field, request)
31+
if queryset is not None:
32+
kwargs['queryset'] = queryset
33+
34+
form_field = db_field.formfield(**kwargs)
35+
if isinstance(form_field.widget, SelectMultiple) and not isinstance(form_field.widget, CheckboxSelectMultiple):
36+
msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.')
37+
help_text = form_field.help_text
38+
form_field.help_text = string_concat(help_text, ' ', msg) if help_text else msg
39+
return form_field
40+
41+
def formfield_for_dbfield(self, db_field, request, **kwargs):
42+
43+
# ForeignKey or ManyToManyFields
44+
if isinstance(db_field, ArrayManyToManyField):
45+
# Combine the field kwargs with any options for formfield_overrides.
46+
# Make sure the passed in **kwargs override anything in
47+
# formfield_overrides because **kwargs is more specific, and should
48+
# always win.
49+
if db_field.__class__ in self.formfield_overrides:
50+
kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs)
51+
52+
formfield = self.formfield_for_manytomany(db_field, request, **kwargs)
53+
54+
# For non-raw_id fields, wrap the widget with a wrapper that adds
55+
# extra HTML -- the "add other" interface -- to the end of the
56+
# rendered output. formfield can be None if it came from a
57+
# OneToOneField with parent_link=True or a M2M intermediary.
58+
if formfield and db_field.name not in self.raw_id_fields:
59+
related_modeladmin = self.admin_site._registry.get(db_field.remote_field.model)
60+
wrapper_kwargs = {}
61+
if related_modeladmin:
62+
wrapper_kwargs.update(
63+
can_add_related=related_modeladmin.has_add_permission(request),
64+
can_change_related=related_modeladmin.has_change_permission(request),
65+
can_delete_related=related_modeladmin.has_delete_permission(request),
66+
)
67+
formfield.widget = widgets.RelatedFieldWidgetWrapper(
68+
formfield.widget, db_field.remote_field, self.admin_site, **wrapper_kwargs
69+
)
70+
71+
return formfield
72+
else:
73+
return super(PostgresAdmin, self).formfield_for_dbfield(db_field, request, **kwargs)

django_postgres_extensions/apps.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from django.apps import AppConfig
2+
from django.utils.translation import ugettext_lazy as _
3+
from django.db.models import query
4+
from django.db.models.sql import datastructures
5+
from .models.query import update, _update, format, prefetch_one_level
6+
from .models.sql.datastructures import as_sql
7+
from django.db.models.signals import pre_delete
8+
from .signals import delete_reverse_related
9+
from django.conf import settings
10+
11+
12+
class PSQLExtensionsConfig(AppConfig):
13+
name = 'django_postgres_extensions'
14+
verbose_name = _('Extra features for PostgreSQL fields')
15+
16+
def ready(self):
17+
query.QuerySet.format = format
18+
query.QuerySet.update = update
19+
query.QuerySet._update = _update
20+
if getattr(settings, 'ENABLE_ARRAY_M2M', False):
21+
datastructures.Join.as_sql = as_sql
22+
query.prefetch_one_level = prefetch_one_level
23+
pre_delete.connect(delete_reverse_related)

django_postgres_extensions/backends/__init__.py

Whitespace-only changes.

django_postgres_extensions/backends/postgresql/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from django.db.backends.postgresql.base import DatabaseWrapper as BaseDatabaseWrapper
2+
from .schema import DatabaseSchemaEditor
3+
from .creation import DatabaseCreation
4+
from .operations import DatabaseOperations
5+
6+
class DatabaseWrapper(BaseDatabaseWrapper):
7+
8+
SchemaEditorClass = DatabaseSchemaEditor
9+
10+
def __init__(self, *args, **kwargs):
11+
super(DatabaseWrapper, self).__init__(*args, **kwargs)
12+
self.creation = DatabaseCreation(self)
13+
self.ops = DatabaseOperations(self)
14+
15+
self.any_operators = {
16+
'exact': '= ANY(%s)',
17+
'in': 'LIKE ANY(%s)',
18+
'gt': '< ANY(%s)',
19+
'gte': '<= ANY(%s)',
20+
'lt': '> ANY(%s)',
21+
'lte': '>= ANY(%s)',
22+
'startof': 'LIKE ANY(%s)',
23+
'endof': 'LIKE ANY(%s)',
24+
'contains': '<@ ANY(%s)'
25+
}
26+
27+
28+
self.all_operators = {
29+
'exact': '= ALL(%s)',
30+
'in': 'LIKE ALL(%s)',
31+
'gt': '< ALL(%s)',
32+
'gte': '<= ALL(%s)',
33+
'lt': '> ALL(%s)',
34+
'lte': '>= ALL(%s)',
35+
'startof': 'LIKE ALL(%s)',
36+
'endof': 'LIKE ALL(%s)',
37+
'contains': '<@ ALL(%s)'
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from django.contrib.postgres.signals import register_hstore_handler
2+
from django.db.backends.postgresql.creation import DatabaseCreation as BaseDatabaseCreation
3+
from django.conf import settings
4+
5+
class DatabaseCreation(BaseDatabaseCreation):
6+
def create_test_db(self, verbosity=1, autoclobber=False, serialize=True, keepdb=False):
7+
"""
8+
Creates a test database, prompting the user for confirmation if the
9+
database already exists. Returns the name of the test database created.
10+
"""
11+
# Don't import django.core.management if it isn't needed.
12+
from django.core.management import call_command
13+
14+
test_database_name = self._get_test_db_name()
15+
16+
if verbosity >= 1:
17+
action = 'Creating'
18+
if keepdb:
19+
action = "Using existing"
20+
21+
print("%s test database for alias %s..." % (
22+
action,
23+
self._get_database_display_str(verbosity, test_database_name),
24+
))
25+
26+
# We could skip this call if keepdb is True, but we instead
27+
# give it the keepdb param. This is to handle the case
28+
# where the test DB doesn't exist, in which case we need to
29+
# create it, then just not destroy it. If we instead skip
30+
# this, we will get an exception.
31+
self._create_test_db(verbosity, autoclobber, keepdb)
32+
33+
self.connection.close()
34+
settings.DATABASES[self.connection.alias]["NAME"] = test_database_name
35+
self.connection.settings_dict["NAME"] = test_database_name
36+
37+
with self.connection.cursor() as cursor:
38+
for extension in ('hstore',):
39+
cursor.execute("CREATE EXTENSION IF NOT EXISTS %s" % extension)
40+
register_hstore_handler(self.connection)
41+
42+
# We report migrate messages at one level lower than that requested.
43+
# This ensures we don't get flooded with messages during testing
44+
# (unless you really ask to be flooded).
45+
call_command(
46+
'migrate',
47+
verbosity=max(verbosity - 1, 0),
48+
interactive=False,
49+
database=self.connection.alias,
50+
run_syncdb=True,
51+
)
52+
53+
# We then serialize the current state of the database into a string
54+
# and store it on the connection. This slightly horrific process is so people
55+
# who are testing on databases without transactions or who are using
56+
# a TransactionTestCase still get a clean database on every test run.
57+
if serialize:
58+
self.connection._test_serialized_contents = self.serialize_db_to_string()
59+
60+
call_command('createcachetable', database=self.connection.alias)
61+
62+
# Ensure a connection for the side effect of initializing the test database.
63+
self.connection.ensure_connection()
64+
65+
return test_database_name
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from django.db.backends.postgresql.operations import DatabaseOperations as BaseDatabaseOperations
2+
3+
class DatabaseOperations(BaseDatabaseOperations):
4+
compiler_module = "django_postgres_extensions.models.sql.compiler"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from django.db.backends.postgresql import schema
2+
3+
class DatabaseSchemaEditor(schema.DatabaseSchemaEditor):
4+
sql_create_array_index = "CREATE INDEX %(name)s ON %(table)s USING GIN (%(columns)s)%(extra)s"
5+
6+
def _model_indexes_sql(self, model):
7+
output = super(DatabaseSchemaEditor, self)._model_indexes_sql(model)
8+
if not model._meta.managed or model._meta.proxy or model._meta.swapped:
9+
return output
10+
11+
for field in model._meta.local_fields:
12+
array_index_statement = self._create_array_index_sql(model, field)
13+
if array_index_statement is not None:
14+
output.append(array_index_statement)
15+
return output
16+
17+
def _create_array_index_sql(self, model, field):
18+
db_type = field.db_type(connection=self.connection)
19+
if db_type is not None and '[' in db_type and db_type.endswith(']') and (field.db_index or field.unique):
20+
return self._create_index_sql(model, [field], suffix='_gin', sql=self.sql_create_array_index)
21+
return None
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .fields import NestedFormField
2+
from .widgets import NestedFormWidget
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from .widgets import NestedFormWidget
2+
from django.forms.fields import MultiValueField, CharField
3+
from django.core.exceptions import ValidationError
4+
5+
6+
class NestedFormField(MultiValueField):
7+
"""
8+
A Field that aggregates the logic of multiple Fields to create a nested form within a form.
9+
10+
The compress method returns a dictionary of field names and values.
11+
12+
Requires either a ``fields`` or ``keys`` argument but not both.
13+
14+
The ``fields`` argument is a list of tuples; each tuple consisting of a field name and a field instance.
15+
If given a nested form will be created consisting of these fields.
16+
17+
The ``keys`` argument is a list/tuple of field names. If given, a nested form will be created consisting of
18+
django.forms.CharField instances, with the given key names. This is primarily for use with
19+
django.contrib.postgres.HStoreField. By default, all fields are not required.
20+
21+
To make all fields required set the ``require_all_fields`` argument to True.
22+
23+
The ``max_value_length`` is ignored if the ``fields`` argument is given. If the ``keys`` argument is given, the max
24+
length for each CharField instance will be set to this value.
25+
26+
Uses the NestedFormWidget.
27+
"""
28+
def __init__(self, fields=(), keys=(), require_all_fields=False, max_value_length=25, *args, **kwargs):
29+
if (fields and keys) or (not fields and not keys):
30+
raise ValueError("NestedFormField requires either a tuple of fields or keys but not both")
31+
32+
if keys:
33+
fields = []
34+
for key in keys:
35+
field = CharField(max_length=max_value_length, required=False)
36+
fields.append((key, field))
37+
form_fields = []
38+
widgets = []
39+
self.labels = []
40+
self.names = {}
41+
for field in fields:
42+
label = field[1].label or field[0]
43+
self.names[label] = field[0]
44+
self.labels.append(label)
45+
form_fields.append(field[1])
46+
widgets.append(field[1].widget)
47+
widget = NestedFormWidget(self.labels, widgets, self.names)
48+
super(NestedFormField, self).__init__(*args, fields=form_fields, widget=widget,
49+
require_all_fields=require_all_fields, **kwargs)
50+
51+
def compress(self, data_list):
52+
result = {}
53+
for i, label in enumerate(self.labels):
54+
name = self.names[label]
55+
result[name] = data_list[i]
56+
return result
57+
58+
def to_python(self, value):
59+
if not value:
60+
return {}
61+
if isinstance(value, dict):
62+
return value
63+
else:
64+
raise ValidationError(
65+
self.error_messages['invalid_json'],
66+
code='invalid_json',
67+
)

0 commit comments

Comments
 (0)