diff --git a/django/db/backends/mysql/creation.py b/django/db/backends/mysql/creation.py index 01efe8d35b..3a57c29479 100644 --- a/django/db/backends/mysql/creation.py +++ b/django/db/backends/mysql/creation.py @@ -7,6 +7,7 @@ class DatabaseCreation(BaseDatabaseCreation): # If a column type is set to None, it won't be included in the output. data_types = { 'AutoField': 'integer AUTO_INCREMENT', + 'BinaryField': 'longblob', 'BooleanField': 'bool', 'CharField': 'varchar(%(max_length)s)', 'CommaSeparatedIntegerField': 'varchar(%(max_length)s)', diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py index 1cc3957232..aaca74e8d1 100644 --- a/django/db/backends/oracle/creation.py +++ b/django/db/backends/oracle/creation.py @@ -17,6 +17,7 @@ class DatabaseCreation(BaseDatabaseCreation): data_types = { 'AutoField': 'NUMBER(11)', + 'BinaryField': 'BLOB', 'BooleanField': 'NUMBER(1) CHECK (%(qn_column)s IN (0,1))', 'CharField': 'NVARCHAR2(%(max_length)s)', 'CommaSeparatedIntegerField': 'VARCHAR2(%(max_length)s)', diff --git a/django/db/backends/postgresql_psycopg2/creation.py b/django/db/backends/postgresql_psycopg2/creation.py index d977939f41..b19926b440 100644 --- a/django/db/backends/postgresql_psycopg2/creation.py +++ b/django/db/backends/postgresql_psycopg2/creation.py @@ -11,6 +11,7 @@ class DatabaseCreation(BaseDatabaseCreation): # If a column type is set to None, it won't be included in the output. data_types = { 'AutoField': 'serial', + 'BinaryField': 'bytea', 'BooleanField': 'boolean', 'CharField': 'varchar(%(max_length)s)', 'CommaSeparatedIntegerField': 'varchar(%(max_length)s)', diff --git a/django/db/backends/sqlite3/creation.py b/django/db/backends/sqlite3/creation.py index 9dacac72e1..c90a697e35 100644 --- a/django/db/backends/sqlite3/creation.py +++ b/django/db/backends/sqlite3/creation.py @@ -9,6 +9,7 @@ class DatabaseCreation(BaseDatabaseCreation): # schema inspection is more useful. data_types = { 'AutoField': 'integer', + 'BinaryField': 'BLOB', 'BooleanField': 'bool', 'CharField': 'varchar(%(max_length)s)', 'CommaSeparatedIntegerField': 'varchar(%(max_length)s)', diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 2702876397..8302aaceaa 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -1291,3 +1291,30 @@ class URLField(CharField): } defaults.update(kwargs) return super(URLField, self).formfield(**defaults) + +class BinaryField(Field): + description = _("Raw binary data") + + def __init__(self, *args, **kwargs): + kwargs['editable'] = False + super(BinaryField, self).__init__(*args, **kwargs) + if self.max_length is not None: + self.validators.append(validators.MaxLengthValidator(self.max_length)) + + def get_internal_type(self): + return "BinaryField" + + def get_default(self): + if self.has_default() and not callable(self.default): + return self.default + default = super(BinaryField, self).get_default() + if default == '': + return b'' + return default + + def get_db_prep_value(self, value, connection, prepared=False): + value = super(BinaryField, self + ).get_db_prep_value(value, connection, prepared) + if value is not None: + return connection.Database.Binary(value) + return value diff --git a/django/utils/six.py b/django/utils/six.py index b93dc5b164..208c5c1112 100644 --- a/django/utils/six.py +++ b/django/utils/six.py @@ -394,10 +394,14 @@ if PY3: _iterlists = "lists" _assertRaisesRegex = "assertRaisesRegex" _assertRegex = "assertRegex" + memoryview = memoryview else: _iterlists = "iterlists" _assertRaisesRegex = "assertRaisesRegexp" _assertRegex = "assertRegexpMatches" + # memoryview and buffer are not stricly equivalent, but should be fine for + # django core usage (mainly BinaryField) + memoryview = buffer def iterlists(d): diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 1a0f93cf9d..1dbc8c3998 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -347,6 +347,22 @@ A 64 bit integer, much like an :class:`IntegerField` except that it is guaranteed to fit numbers from -9223372036854775808 to 9223372036854775807. The default form widget for this field is a :class:`~django.forms.TextInput`. +``BinaryField`` +------------------- + +.. class:: BinaryField([**options]) + +.. versionadded:: 1.6 + +A field to store raw binary data. It only supports ``bytes`` assignment. Be +aware that this field has limited functionality. For example, it is not possible +to filter a queryset on a ``BinaryField`` value. + +.. admonition:: Abusing ``BinaryField`` + + Although you might think about storing files in the database, consider that + it is bad design in 99% of the cases. This field is *not* a replacement for + proper :ref.`static files handling. ``BooleanField`` ---------------- diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 34fa687290..89e7ff17ee 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -53,6 +53,12 @@ UTC. This limitation was lifted in Django 1.6. Use :meth:`QuerySet.datetimes() ` to perform time zone aware aggregation on a :class:`~django.db.models.DateTimeField`. +``BinaryField`` model field +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A new :class:`django.db.models.BinaryField` model field allows to store raw +binary data in the database. + Minor features ~~~~~~~~~~~~~~ diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py index 1d20f44fae..c3b2f7fccb 100644 --- a/tests/model_fields/models.py +++ b/tests/model_fields/models.py @@ -106,6 +106,10 @@ class VerboseNameField(models.Model): class DecimalLessThanOne(models.Model): d = models.DecimalField(max_digits=3, decimal_places=3) +class DataModel(models.Model): + short_data = models.BinaryField(max_length=10, default=b'\x08') + data = models.BinaryField() + ############################################################################### # FileField diff --git a/tests/model_fields/tests.py b/tests/model_fields/tests.py index 8c596ed4f5..eaf84773e3 100644 --- a/tests/model_fields/tests.py +++ b/tests/model_fields/tests.py @@ -12,8 +12,8 @@ from django.utils import six from django.utils import unittest from .models import (Foo, Bar, Whiz, BigD, BigS, Image, BigInt, Post, - NullBooleanModel, BooleanModel, Document, RenamedField, VerboseNameField, - FksToBooleans) + NullBooleanModel, BooleanModel, DataModel, Document, RenamedField, + VerboseNameField, FksToBooleans) from .imagefield import (ImageFieldTests, ImageFieldTwoDimensionsTests, TwoImageFieldTests, ImageFieldNoDimensionsTests, @@ -424,3 +424,25 @@ class FileFieldTests(unittest.TestCase): field = d._meta.get_field('myfile') field.save_form_data(d, 'else.txt') self.assertEqual(d.myfile, 'else.txt') + + +class BinaryFieldTests(test.TestCase): + binary_data = b'\x00\x46\xFE' + + def test_set_and_retrieve(self): + data_set = (self.binary_data, six.memoryview(self.binary_data)) + for bdata in data_set: + dm = DataModel(data=bdata) + dm.save() + dm = DataModel.objects.get(pk=dm.pk) + self.assertEqual(bytes(dm.data), bytes(bdata)) + # Resave (=update) + dm.save() + dm = DataModel.objects.get(pk=dm.pk) + self.assertEqual(bytes(dm.data), bytes(bdata)) + # Test default value + self.assertEqual(bytes(dm.short_data), b'\x08') + + def test_max_length(self): + dm = DataModel(short_data=self.binary_data*4) + self.assertRaises(ValidationError, dm.full_clean)