Skip to content

Commit 8ee1edd

Browse files
committed
Add a BinaryField model field
Thanks Michael Jung, Charl Botha and Florian Apolloner for review and help on the patch.
1 parent 0f306ca commit 8ee1edd

File tree

10 files changed

+85
-2
lines changed

10 files changed

+85
-2
lines changed

django/db/backends/mysql/creation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class DatabaseCreation(BaseDatabaseCreation):
77
# If a column type is set to None, it won't be included in the output.
88
data_types = {
99
'AutoField': 'integer AUTO_INCREMENT',
10+
'BinaryField': 'longblob',
1011
'BooleanField': 'bool',
1112
'CharField': 'varchar(%(max_length)s)',
1213
'CommaSeparatedIntegerField': 'varchar(%(max_length)s)',

django/db/backends/oracle/creation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class DatabaseCreation(BaseDatabaseCreation):
1717

1818
data_types = {
1919
'AutoField': 'NUMBER(11)',
20+
'BinaryField': 'BLOB',
2021
'BooleanField': 'NUMBER(1) CHECK (%(qn_column)s IN (0,1))',
2122
'CharField': 'NVARCHAR2(%(max_length)s)',
2223
'CommaSeparatedIntegerField': 'VARCHAR2(%(max_length)s)',

django/db/backends/postgresql_psycopg2/creation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class DatabaseCreation(BaseDatabaseCreation):
1111
# If a column type is set to None, it won't be included in the output.
1212
data_types = {
1313
'AutoField': 'serial',
14+
'BinaryField': 'bytea',
1415
'BooleanField': 'boolean',
1516
'CharField': 'varchar(%(max_length)s)',
1617
'CommaSeparatedIntegerField': 'varchar(%(max_length)s)',

django/db/backends/sqlite3/creation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class DatabaseCreation(BaseDatabaseCreation):
99
# schema inspection is more useful.
1010
data_types = {
1111
'AutoField': 'integer',
12+
'BinaryField': 'BLOB',
1213
'BooleanField': 'bool',
1314
'CharField': 'varchar(%(max_length)s)',
1415
'CommaSeparatedIntegerField': 'varchar(%(max_length)s)',

django/db/models/fields/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,3 +1291,30 @@ def formfield(self, **kwargs):
12911291
}
12921292
defaults.update(kwargs)
12931293
return super(URLField, self).formfield(**defaults)
1294+
1295+
class BinaryField(Field):
1296+
description = _("Raw binary data")
1297+
1298+
def __init__(self, *args, **kwargs):
1299+
kwargs['editable'] = False
1300+
super(BinaryField, self).__init__(*args, **kwargs)
1301+
if self.max_length is not None:
1302+
self.validators.append(validators.MaxLengthValidator(self.max_length))
1303+
1304+
def get_internal_type(self):
1305+
return "BinaryField"
1306+
1307+
def get_default(self):
1308+
if self.has_default() and not callable(self.default):
1309+
return self.default
1310+
default = super(BinaryField, self).get_default()
1311+
if default == '':
1312+
return b''
1313+
return default
1314+
1315+
def get_db_prep_value(self, value, connection, prepared=False):
1316+
value = super(BinaryField, self
1317+
).get_db_prep_value(value, connection, prepared)
1318+
if value is not None:
1319+
return connection.Database.Binary(value)
1320+
return value

django/utils/six.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,10 +394,14 @@ def with_metaclass(meta, base=object):
394394
_iterlists = "lists"
395395
_assertRaisesRegex = "assertRaisesRegex"
396396
_assertRegex = "assertRegex"
397+
memoryview = memoryview
397398
else:
398399
_iterlists = "iterlists"
399400
_assertRaisesRegex = "assertRaisesRegexp"
400401
_assertRegex = "assertRegexpMatches"
402+
# memoryview and buffer are not stricly equivalent, but should be fine for
403+
# django core usage (mainly BinaryField)
404+
memoryview = buffer
401405

402406

403407
def iterlists(d):

docs/ref/models/fields.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,22 @@ A 64 bit integer, much like an :class:`IntegerField` except that it is
347347
guaranteed to fit numbers from -9223372036854775808 to 9223372036854775807. The
348348
default form widget for this field is a :class:`~django.forms.TextInput`.
349349

350+
``BinaryField``
351+
-------------------
352+
353+
.. class:: BinaryField([**options])
354+
355+
.. versionadded:: 1.6
356+
357+
A field to store raw binary data. It only supports ``bytes`` assignment. Be
358+
aware that this field has limited functionality. For example, it is not possible
359+
to filter a queryset on a ``BinaryField`` value.
360+
361+
.. admonition:: Abusing ``BinaryField``
362+
363+
Although you might think about storing files in the database, consider that
364+
it is bad design in 99% of the cases. This field is *not* a replacement for
365+
proper :ref.`static files <static-files> handling.
350366

351367
``BooleanField``
352368
----------------

docs/releases/1.6.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ UTC. This limitation was lifted in Django 1.6. Use :meth:`QuerySet.datetimes()
5353
<django.db.models.query.QuerySet.datetimes>` to perform time zone aware
5454
aggregation on a :class:`~django.db.models.DateTimeField`.
5555

56+
``BinaryField`` model field
57+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
58+
59+
A new :class:`django.db.models.BinaryField` model field allows to store raw
60+
binary data in the database.
61+
5662
Minor features
5763
~~~~~~~~~~~~~~
5864

tests/model_fields/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ class VerboseNameField(models.Model):
106106
class DecimalLessThanOne(models.Model):
107107
d = models.DecimalField(max_digits=3, decimal_places=3)
108108

109+
class DataModel(models.Model):
110+
short_data = models.BinaryField(max_length=10, default=b'\x08')
111+
data = models.BinaryField()
112+
109113
###############################################################################
110114
# FileField
111115

tests/model_fields/tests.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
from django.utils import unittest
1313

1414
from .models import (Foo, Bar, Whiz, BigD, BigS, Image, BigInt, Post,
15-
NullBooleanModel, BooleanModel, Document, RenamedField, VerboseNameField,
16-
FksToBooleans)
15+
NullBooleanModel, BooleanModel, DataModel, Document, RenamedField,
16+
VerboseNameField, FksToBooleans)
1717

1818
from .imagefield import (ImageFieldTests, ImageFieldTwoDimensionsTests,
1919
TwoImageFieldTests, ImageFieldNoDimensionsTests,
@@ -424,3 +424,25 @@ def test_changed(self):
424424
field = d._meta.get_field('myfile')
425425
field.save_form_data(d, 'else.txt')
426426
self.assertEqual(d.myfile, 'else.txt')
427+
428+
429+
class BinaryFieldTests(test.TestCase):
430+
binary_data = b'\x00\x46\xFE'
431+
432+
def test_set_and_retrieve(self):
433+
data_set = (self.binary_data, six.memoryview(self.binary_data))
434+
for bdata in data_set:
435+
dm = DataModel(data=bdata)
436+
dm.save()
437+
dm = DataModel.objects.get(pk=dm.pk)
438+
self.assertEqual(bytes(dm.data), bytes(bdata))
439+
# Resave (=update)
440+
dm.save()
441+
dm = DataModel.objects.get(pk=dm.pk)
442+
self.assertEqual(bytes(dm.data), bytes(bdata))
443+
# Test default value
444+
self.assertEqual(bytes(dm.short_data), b'\x08')
445+
446+
def test_max_length(self):
447+
dm = DataModel(short_data=self.binary_data*4)
448+
self.assertRaises(ValidationError, dm.full_clean)

0 commit comments

Comments
 (0)