Skip to content

Commit f05cd97

Browse files
authored
Merge pull request #19 from edx/foreign-key-key-fields
Allow the use of ForeignKey fields as ConfigurationModel KEY_FIELDS
2 parents a63ec94 + acfcca4 commit f05cd97

File tree

2 files changed

+109
-64
lines changed

2 files changed

+109
-64
lines changed

config_models/models.py

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44
from __future__ import absolute_import, unicode_literals
55

6-
from django.db import connection, models
6+
from django.db import models
77
from django.conf import settings
88
from django.core.cache import caches, InvalidCacheBackendError
99
from django.utils.translation import ugettext_lazy as _
@@ -36,12 +36,7 @@ def _current_ids_subquery(self):
3636
all the current entries (i.e. the most recent entry for each unique set
3737
of key values). Only useful if KEY_FIELDS is set.
3838
"""
39-
key_fields_escaped = [connection.ops.quote_name(name) for name in self.model.KEY_FIELDS]
40-
# The following assumes that the rows with the most recent date also have the highest IDs
41-
return "SELECT MAX(id) FROM {table_name} GROUP BY {key_fields}".format(
42-
key_fields=', '.join(key_fields_escaped),
43-
table_name=self.model._meta.db_table # pylint: disable=protected-access
44-
)
39+
return self.values(*self.model.KEY_FIELDS).annotate(max=models.Max('pk')).values('max')
4540

4641
def current_set(self):
4742
"""
@@ -51,12 +46,10 @@ def current_set(self):
5146
necessaryily mean enbled.
5247
"""
5348
assert self.model.KEY_FIELDS != (), "Just use model.current() if there are no KEY_FIELDS"
54-
return self.get_queryset().extra( # pylint: disable=no-member
55-
where=["{table_name}.id IN ({subquery})".format(
56-
table_name=self.model._meta.db_table, # pylint: disable=protected-access, no-member
57-
subquery=self._current_ids_subquery(),
58-
)],
59-
select={'is_active': 1}, # This annotation is used by the admin changelist. sqlite requires '1', not 'True'
49+
return self.get_queryset().filter(
50+
pk__in=self._current_ids_subquery()
51+
).annotate(
52+
is_active=models.Value(1, output_field=models.IntegerField())
6053
)
6154

6255
def with_active_flag(self):
@@ -65,18 +58,17 @@ def with_active_flag(self):
6558
if it's the most recent entry for that combination of keys.
6659
"""
6760
if self.model.KEY_FIELDS:
68-
subquery = self._current_ids_subquery()
69-
return self.get_queryset().extra( # pylint: disable=no-member
70-
select={'is_active': "{table_name}.id IN ({subquery})".format(
71-
table_name=self.model._meta.db_table, # pylint: disable=protected-access, no-member
72-
subquery=subquery,
73-
)}
61+
return self.get_queryset().annotate(
62+
is_active=models.ExpressionWrapper(
63+
models.Q(pk__in=self._current_ids_subquery()),
64+
output_field=models.IntegerField(),
65+
)
66+
)
67+
return self.get_queryset().annotate(
68+
is_active=models.ExpressionWrapper(
69+
models.Q(pk=self.model.current().pk),
70+
output_field=models.IntegerField(),
7471
)
75-
return self.get_queryset().extra( # pylint: disable=no-member
76-
select={'is_active': "{table_name}.id = {pk}".format(
77-
table_name=self.model._meta.db_table, # pylint: disable=protected-access, no-member
78-
pk=self.model.current().pk, # pylint: disable=no-member
79-
)}
8072
)
8173

8274

config_models/tests/test_config_models.py

Lines changed: 93 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# -*- coding: utf-8 -*-
21
"""
32
Tests of ConfigurationModel
43
"""
@@ -119,6 +118,21 @@ def test_active_annotation(self, mock_cache):
119118
self.assertEqual(rows[1].string_field, 'first')
120119
self.assertEqual(rows[1].is_active, False)
121120

121+
def test_keyed_active_annotation(self, mock_cache):
122+
mock_cache.get.return_value = None
123+
124+
with freeze_time('2012-01-01'):
125+
ExampleKeyedConfig.objects.create(left='left', right='right', user=self.user, string_field='first')
126+
127+
ExampleKeyedConfig.objects.create(left='left', right='right', user=self.user, string_field='second')
128+
129+
rows = ExampleKeyedConfig.objects.with_active_flag().order_by('-change_date')
130+
self.assertEqual(len(rows), 2)
131+
self.assertEqual(rows[0].string_field, 'second')
132+
self.assertEqual(rows[0].is_active, True)
133+
self.assertEqual(rows[1].string_field, 'first')
134+
self.assertEqual(rows[1].is_active, False)
135+
122136
def test_always_insert(self, __):
123137
config = ExampleConfig(changed_by=self.user, string_field='first')
124138
config.save()
@@ -196,17 +210,18 @@ class ExampleKeyedConfig(ConfigurationModel):
196210
"""
197211
cache_timeout = 300
198212

199-
KEY_FIELDS = ('left', 'right')
213+
KEY_FIELDS = ('left', 'right', 'user')
200214

201215
left = models.CharField(max_length=30)
202216
right = models.CharField(max_length=30)
217+
user = models.ForeignKey('auth.User')
203218

204219
string_field = models.TextField()
205220
int_field = models.IntegerField(default=10)
206221

207222
def __str__(self):
208-
return "ExampleKeyedConfig(enabled={}, left={}, right={}, string_field={}, int_field={})".format(
209-
self.enabled, self.left, self.right, self.string_field, self.int_field
223+
return "ExampleKeyedConfig(enabled={}, left={}, right={}, user={}, string_field={}, int_field={})".format(
224+
self.enabled, self.left, self.right, self.user, self.string_field, self.int_field
210225
)
211226

212227

@@ -225,12 +240,12 @@ def setUp(self):
225240
@ddt.unpack
226241
def test_cache_key_name(self, left, right, _mock_cache):
227242
self.assertEqual(
228-
ExampleKeyedConfig.cache_key_name(left, right),
229-
'configuration/ExampleKeyedConfig/current/{},{}'.format(left, right)
243+
ExampleKeyedConfig.cache_key_name(left, right, self.user),
244+
'configuration/ExampleKeyedConfig/current/{},{},{}'.format(left, right, self.user)
230245
)
231246

232247
@ddt.data(
233-
((), 'left,right'),
248+
((), 'left,right,user'),
234249
(('left', 'right'), 'left,right'),
235250
(('left', ), 'left')
236251
)
@@ -245,15 +260,15 @@ def test_key_values_cache_key_name(self, args, expected_key, _mock_cache):
245260
def test_no_config_empty_cache(self, left, right, mock_cache):
246261
mock_cache.get.return_value = None
247262

248-
current = ExampleKeyedConfig.current(left, right)
263+
current = ExampleKeyedConfig.current(left, right, self.user)
249264
self.assertEqual(current.int_field, 10)
250265
self.assertEqual(current.string_field, '')
251-
mock_cache.set.assert_called_with(ExampleKeyedConfig.cache_key_name(left, right), current, 300)
266+
mock_cache.set.assert_called_with(ExampleKeyedConfig.cache_key_name(left, right, self.user), current, 300)
252267

253268
@ddt.data(('a', 'b'), ('c', 'd'))
254269
@ddt.unpack
255270
def test_no_config_full_cache(self, left, right, mock_cache):
256-
current = ExampleKeyedConfig.current(left, right)
271+
current = ExampleKeyedConfig.current(left, right, self.user)
257272
self.assertEqual(current, mock_cache.get.return_value)
258273

259274
def test_config_ordering(self, mock_cache):
@@ -265,30 +280,34 @@ def test_config_ordering(self, mock_cache):
265280
left='left_a',
266281
right='right_a',
267282
string_field='first_a',
283+
user=self.user,
268284
).save()
269285

270286
ExampleKeyedConfig(
271287
changed_by=self.user,
272288
left='left_b',
273289
right='right_b',
274290
string_field='first_b',
291+
user=self.user,
275292
).save()
276293

277294
ExampleKeyedConfig(
278295
changed_by=self.user,
279296
left='left_a',
280297
right='right_a',
281298
string_field='second_a',
299+
user=self.user,
282300
).save()
283301
ExampleKeyedConfig(
284302
changed_by=self.user,
285303
left='left_b',
286304
right='right_b',
287305
string_field='second_b',
306+
user=self.user,
288307
).save()
289308

290-
self.assertEqual(ExampleKeyedConfig.current('left_a', 'right_a').string_field, 'second_a')
291-
self.assertEqual(ExampleKeyedConfig.current('left_b', 'right_b').string_field, 'second_b')
309+
self.assertEqual(ExampleKeyedConfig.current('left_a', 'right_a', self.user).string_field, 'second_a')
310+
self.assertEqual(ExampleKeyedConfig.current('left_b', 'right_b', self.user).string_field, 'second_b')
292311

293312
def test_cache_set(self, mock_cache):
294313
mock_cache.get.return_value = None
@@ -297,56 +316,67 @@ def test_cache_set(self, mock_cache):
297316
changed_by=self.user,
298317
left='left',
299318
right='right',
319+
user=self.user,
300320
string_field='first',
301321
)
302322
first.save()
303323

304-
ExampleKeyedConfig.current('left', 'right')
324+
ExampleKeyedConfig.current('left', 'right', self.user)
305325

306-
mock_cache.set.assert_called_with(ExampleKeyedConfig.cache_key_name('left', 'right'), first, 300)
326+
mock_cache.set.assert_called_with(ExampleKeyedConfig.cache_key_name('left', 'right', self.user), first, 300)
307327

308328
def test_key_values(self, mock_cache):
309329
mock_cache.get.return_value = None
310330

311331
with freeze_time('2012-01-01'):
312-
ExampleKeyedConfig(left='left_a', right='right_a', changed_by=self.user).save()
313-
ExampleKeyedConfig(left='left_b', right='right_b', changed_by=self.user).save()
332+
ExampleKeyedConfig(left='left_a', right='right_a', user=self.user, changed_by=self.user).save()
333+
ExampleKeyedConfig(left='left_b', right='right_b', user=self.user, changed_by=self.user).save()
314334

315-
ExampleKeyedConfig(left='left_a', right='right_a', changed_by=self.user).save()
316-
ExampleKeyedConfig(left='left_b', right='right_b', changed_by=self.user).save()
335+
ExampleKeyedConfig(left='left_a', right='right_a', user=self.user, changed_by=self.user).save()
336+
ExampleKeyedConfig(left='left_b', right='right_b', user=self.user, changed_by=self.user).save()
317337

318338
unique_key_pairs = ExampleKeyedConfig.key_values()
319339
self.assertEqual(len(unique_key_pairs), 2)
320-
self.assertEqual(set(unique_key_pairs), set([('left_a', 'right_a'), ('left_b', 'right_b')]))
340+
self.assertEqual(
341+
set(unique_key_pairs),
342+
set([('left_a', 'right_a', self.user.id), ('left_b', 'right_b', self.user.id)])
343+
)
321344
unique_left_keys = ExampleKeyedConfig.key_values('left', flat=True)
322345
self.assertEqual(len(unique_left_keys), 2)
323346
self.assertEqual(set(unique_left_keys), set(['left_a', 'left_b']))
324347

325348
def test_key_string_values(self, mock_cache):
326349
""" Ensure str() vs unicode() doesn't cause duplicate cache entries """
327-
ExampleKeyedConfig(left='left', right=u'〉☃', enabled=True, int_field=10, changed_by=self.user).save()
350+
ExampleKeyedConfig(
351+
left='left',
352+
right=u'\N{RIGHT ANGLE BRACKET}\N{SNOWMAN}',
353+
enabled=True,
354+
int_field=10,
355+
user=self.user,
356+
changed_by=self.user
357+
).save()
328358
mock_cache.get.return_value = None
329359

330-
entry = ExampleKeyedConfig.current('left', u'〉☃')
360+
entry = ExampleKeyedConfig.current('left', u'\N{RIGHT ANGLE BRACKET}\N{SNOWMAN}', self.user)
331361
key = mock_cache.get.call_args[0][0]
332362
self.assertEqual(entry.int_field, 10)
333363
mock_cache.get.assert_called_with(key)
334364
self.assertEqual(mock_cache.set.call_args[0][0], key)
335365

336366
mock_cache.get.reset_mock()
337-
entry = ExampleKeyedConfig.current(u'left', u'〉☃')
367+
entry = ExampleKeyedConfig.current(u'left', u'\N{RIGHT ANGLE BRACKET}\N{SNOWMAN}', self.user)
338368
self.assertEqual(entry.int_field, 10)
339369
mock_cache.get.assert_called_with(key)
340370

341371
def test_current_set(self, mock_cache):
342372
mock_cache.get.return_value = None
343373

344374
with freeze_time('2012-01-01'):
345-
ExampleKeyedConfig(left='left_a', right='right_a', int_field=0, changed_by=self.user).save()
346-
ExampleKeyedConfig(left='left_b', right='right_b', int_field=0, changed_by=self.user).save()
375+
ExampleKeyedConfig(left='left_a', right='right_a', int_field=0, user=self.user, changed_by=self.user).save()
376+
ExampleKeyedConfig(left='left_b', right='right_b', int_field=0, user=self.user, changed_by=self.user).save()
347377

348-
ExampleKeyedConfig(left='left_a', right='right_a', int_field=1, changed_by=self.user).save()
349-
ExampleKeyedConfig(left='left_b', right='right_b', int_field=2, changed_by=self.user).save()
378+
ExampleKeyedConfig(left='left_a', right='right_a', int_field=1, user=self.user, changed_by=self.user).save()
379+
ExampleKeyedConfig(left='left_b', right='right_b', int_field=2, user=self.user, changed_by=self.user).save()
350380

351381
queryset = ExampleKeyedConfig.objects.current_set()
352382
self.assertEqual(len(queryset.all()), 2)
@@ -359,10 +389,10 @@ def test_active_annotation(self, mock_cache):
359389
mock_cache.get.return_value = None
360390

361391
with freeze_time('2012-01-01'):
362-
ExampleKeyedConfig.objects.create(left='left_a', right='right_a', string_field='first')
363-
ExampleKeyedConfig.objects.create(left='left_b', right='right_b', string_field='first')
392+
ExampleKeyedConfig.objects.create(left='left_a', right='right_a', user=self.user, string_field='first')
393+
ExampleKeyedConfig.objects.create(left='left_b', right='right_b', user=self.user, string_field='first')
364394

365-
ExampleKeyedConfig.objects.create(left='left_a', right='right_a', string_field='second')
395+
ExampleKeyedConfig.objects.create(left='left_a', right='right_a', user=self.user, string_field='second')
366396

367397
rows = ExampleKeyedConfig.objects.with_active_flag()
368398
self.assertEqual(len(rows), 3)
@@ -386,41 +416,64 @@ def test_key_values_cache(self, mock_cache):
386416
def test_equality(self, mock_cache):
387417
mock_cache.get.return_value = None
388418

389-
config1 = ExampleKeyedConfig(left='left_a', right='right_a', int_field=1, changed_by=self.user)
419+
config1 = ExampleKeyedConfig(left='left_a', right='right_a', int_field=1, user=self.user, changed_by=self.user)
390420
config1.save()
391421

392-
config2 = ExampleKeyedConfig(left='left_b', right='right_b', int_field=2, changed_by=self.user, enabled=True)
422+
config2 = ExampleKeyedConfig(
423+
left='left_b',
424+
right='right_b',
425+
int_field=2,
426+
user=self.user,
427+
changed_by=self.user,
428+
enabled=True,
429+
)
393430
config2.save()
394431

395-
config3 = ExampleKeyedConfig(left='left_c', changed_by=self.user)
432+
config3 = ExampleKeyedConfig(left='left_c', user=self.user, changed_by=self.user)
396433
config3.save()
397434

398435
self.assertTrue(
399-
ExampleKeyedConfig.equal_to_current({"left": "left_a", "right": "right_a", "int_field": 1})
436+
ExampleKeyedConfig.equal_to_current({
437+
"left": "left_a",
438+
"right": "right_a",
439+
"int_field": 1,
440+
"user": self.user
441+
})
400442
)
401443
self.assertTrue(
402-
ExampleKeyedConfig.equal_to_current({"left": "left_b", "right": "right_b", "int_field": 2, "enabled": True})
444+
ExampleKeyedConfig.equal_to_current({
445+
"left": "left_b",
446+
"right": "right_b",
447+
"int_field": 2,
448+
"user": self.user,
449+
"enabled": True
450+
})
403451
)
404452
self.assertTrue(
405-
ExampleKeyedConfig.equal_to_current({"left": "left_c"})
453+
ExampleKeyedConfig.equal_to_current({"left": "left_c", "user": self.user})
406454
)
407455

408456
self.assertFalse(
409457
ExampleKeyedConfig.equal_to_current(
410-
{"left": "left_a", "right": "right_a", "int_field": 1, "string_field": "foo"}
458+
{"left": "left_a", "right": "right_a", "user": self.user, "int_field": 1, "string_field": "foo"}
411459
)
412460
)
413461
self.assertFalse(
414-
ExampleKeyedConfig.equal_to_current({"left": "left_a", "int_field": 1})
462+
ExampleKeyedConfig.equal_to_current({"left": "left_a", "user": self.user, "int_field": 1})
415463
)
416464
self.assertFalse(
417-
ExampleKeyedConfig.equal_to_current({"left": "left_b", "right": "right_b", "int_field": 2})
465+
ExampleKeyedConfig.equal_to_current({
466+
"left": "left_b",
467+
"right": "right_b",
468+
"user": self.user,
469+
"int_field": 2,
470+
})
418471
)
419472
self.assertFalse(
420-
ExampleKeyedConfig.equal_to_current({"left": "left_c", "int_field": 11})
473+
ExampleKeyedConfig.equal_to_current({"left": "left_c", "user": self.user, "int_field": 11})
421474
)
422475

423-
self.assertFalse(ExampleKeyedConfig.equal_to_current({}))
476+
self.assertFalse(ExampleKeyedConfig.equal_to_current({"user": self.user}))
424477

425478

426479
@ddt.ddt

0 commit comments

Comments
 (0)