Code

Ticket #3591: app_labels.3.diff

File app_labels.3.diff, 28.1 KB (added by Vinay Sajip <vinay_sajip@…>, 7 years ago)

Minor tweaks (tidy-ups) to the last patch.

Line 
1=== modified file 'django/bin/make-messages.py'
2--- django/bin/make-messages.py
3+++ django/bin/make-messages.py
4@@ -129,6 +129,22 @@
5                 print "errors happened while running msguniq"
6                 print errors
7                 sys.exit(8)
8+            # Try importing the local settings. This will work if you're
9+            # in the project directory
10+            sys.path.insert(0, '.')
11+            try:
12+                import settings
13+                apps = settings.INSTALLED_APPS
14+            except (ImportError, AttributeError):
15+                apps = []
16+            del sys.path[0]
17+            # Now look for all applications which have a verbose name
18+            # which is different from the default. If different, add
19+            # an internationalization string.
20+            for app in apps:
21+                if (not isinstance(app, basestring)) and (app.verbose_name != app.path.split('.')[-1]):
22+                    s = '\nmsgid "%s"\nmsgstr ""\n' % app.verbose_name
23+                    msgs += s
24             open(potfile, 'w').write(msgs)
25             if os.path.exists(pofile):
26                 (stdin, stdout, stderr) = os.popen3('msgmerge -q "%s" "%s"' % (pofile, potfile), 'b')
27
28=== modified file 'django/conf/__init__.py'
29--- django/conf/__init__.py     
30+++ django/conf/__init__.py     
31@@ -97,13 +97,13 @@
32         # of all those apps.
33         new_installed_apps = []
34         for app in self.INSTALLED_APPS:
35-            if app.endswith('.*'):
36+            if not isinstance(app, basestring) or not app.endswith('.*'):
37+                new_installed_apps.append(app)
38+            else:
39                 appdir = os.path.dirname(__import__(app[:-2], {}, {}, ['']).__file__)
40                 for d in os.listdir(appdir):
41                     if d.isalpha() and os.path.isdir(os.path.join(appdir, d)):
42                         new_installed_apps.append('%s.%s' % (app[:-2], d))
43-            else:
44-                new_installed_apps.append(app)
45         self.INSTALLED_APPS = new_installed_apps
46 
47         if hasattr(time, 'tzset'):
48@@ -147,3 +147,26 @@
49     return gettext(*args)
50 
51 __builtins__['_'] = first_time_gettext
52+
53+class app(object):
54+    """Configuration directive for specifying an app."""
55+    def __init__(self, path, app_label=None, verbose_name=None):
56+        self.path = path
57+        # if name isn't specified, get the last part of the Python dotted path
58+        self.label = app_label or path.split('.')[-1]
59+        self.verbose_name = verbose_name or self.label
60+        self.app_module = None    # will be filled in by loading.py
61+        self.models_module = None # will be filled in by loading.py
62+
63+    def __repr__(self):
64+        return "<app: %s>" % self.path
65+
66+def get_installed_app_paths():
67+    "Return the paths of all entries in settings.INSTALLED_APPS."
68+    rv = []
69+    for a in settings.INSTALLED_APPS:
70+        if isinstance(a, basestring):
71+            rv.append(a)
72+        else:
73+            rv.append(a.path)
74+    return rv
75
76=== modified file 'django/contrib/admin/templatetags/adminapplist.py'
77--- django/contrib/admin/templatetags/adminapplist.py   
78+++ django/contrib/admin/templatetags/adminapplist.py   
79@@ -18,7 +18,7 @@
80             app_models = get_models(app)
81             if not app_models:
82                 continue
83-            app_label = app_models[0]._meta.app_label
84+            app_label = app.label
85 
86             has_module_perms = user.has_module_perms(app_label)
87 
88@@ -49,7 +49,7 @@
89                     model_list = [x for key, x in decorated]
90 
91                     app_list.append({
92-                        'name': app_label.title(),
93+                        'name': _(app.verbose_name).title(),
94                         'has_module_perms': has_module_perms,
95                         'models': model_list,
96                     })
97
98=== modified file 'django/contrib/admin/views/doc.py'
99--- django/contrib/admin/views/doc.py   
100+++ django/contrib/admin/views/doc.py   
101@@ -159,11 +159,11 @@
102 
103     # Get the model class.
104     try:
105-        app_mod = models.get_app(app_label)
106+        app = models.get_app(app_label)
107     except ImproperlyConfigured:
108         raise Http404, _("App %r not found") % app_label
109     model = None
110-    for m in models.get_models(app_mod):
111+    for m in models.get_models(app):
112         if m._meta.object_name.lower() == model_name:
113             model = m
114             break
115
116=== modified file 'django/contrib/auth/management.py'
117--- django/contrib/auth/management.py   
118+++ django/contrib/auth/management.py   
119@@ -3,8 +3,7 @@
120 """
121 
122 from django.dispatch import dispatcher
123-from django.db.models import get_models, signals
124-from django.contrib.auth import models as auth_app
125+from django.db.models import get_models, signals, find_app
126 
127 def _get_permission_codename(action, opts):
128     return '%s_%s' % (action, opts.object_name.lower())
129@@ -46,4 +45,4 @@
130             break
131 
132 dispatcher.connect(create_permissions, signal=signals.post_syncdb)
133-dispatcher.connect(create_superuser, sender=auth_app, signal=signals.post_syncdb)
134+dispatcher.connect(create_superuser, sender=find_app('django.contrib.auth'), signal=signals.post_syncdb)
135
136=== modified file 'django/contrib/sites/management.py'
137--- django/contrib/sites/management.py 
138+++ django/contrib/sites/management.py 
139@@ -3,9 +3,8 @@
140 """
141 
142 from django.dispatch import dispatcher
143-from django.db.models import signals
144+from django.db.models import signals, find_app
145 from django.contrib.sites.models import Site
146-from django.contrib.sites import models as site_app
147 
148 def create_default_site(app, created_models, verbosity):
149     if Site in created_models:
150@@ -14,4 +13,4 @@
151         s = Site(domain="example.com", name="example.com")
152         s.save()
153 
154-dispatcher.connect(create_default_site, sender=site_app, signal=signals.post_syncdb)
155+dispatcher.connect(create_default_site, sender=find_app('django.contrib.sites'), signal=signals.post_syncdb)
156
157=== modified file 'django/core/management.py'
158--- django/core/management.py   
159+++ django/core/management.py   
160@@ -368,7 +368,7 @@
161     from django.conf import settings
162 
163     opts = model._meta
164-    app_dir = os.path.normpath(os.path.join(os.path.dirname(models.get_app(model._meta.app_label).__file__), 'sql'))
165+    app_dir = os.path.normpath(os.path.join(os.path.dirname(models.get_app(model._meta.app_label).models_module.__file__), 'sql'))
166     output = []
167 
168     # Some backends can't execute more than one SQL statement at a time,
169@@ -396,7 +396,7 @@
170     output = []
171 
172     app_models = get_models(app)
173-    app_dir = os.path.normpath(os.path.join(os.path.dirname(app.__file__), 'sql'))
174+    app_dir = os.path.normpath(os.path.join(os.path.dirname(app.models_module.__file__), 'sql'))
175 
176     for model in app_models:
177         output.extend(get_custom_sql_for_model(model))
178@@ -456,7 +456,7 @@
179     from django.dispatch import dispatcher
180     # Emit the post_sync signal for every application.
181     for app in models.get_apps():
182-        app_name = app.__name__.split('.')[-2]
183+        app_name = app.label
184         if verbosity >= 2:
185             print "Running post-sync handlers for application", app_name
186         dispatcher.send(signal=models.signals.post_syncdb, sender=app,
187@@ -466,7 +466,7 @@
188 def syncdb(verbosity=1, interactive=True):
189     "Creates the database tables for all apps in INSTALLED_APPS whose tables haven't already been created."
190     from django.db import connection, transaction, models, get_creation_module
191-    from django.conf import settings
192+    from django.conf import settings, get_installed_app_paths
193 
194     disable_termcolors()
195 
196@@ -475,7 +475,7 @@
197 
198     # Import the 'management' module within each installed app, to register
199     # dispatcher events.
200-    for app_name in settings.INSTALLED_APPS:
201+    for app_name in get_installed_app_paths():
202         try:
203             __import__(app_name + '.management', {}, {}, [''])
204         except ImportError:
205@@ -496,7 +496,7 @@
206 
207     # Create the tables for each model
208     for app in models.get_apps():
209-        app_name = app.__name__.split('.')[-2]
210+        app_name = app.label
211         model_list = models.get_models(app)
212         for model in model_list:
213             # Create the model's database table, if it doesn't already exist.
214@@ -519,7 +519,7 @@
215     # Create the m2m tables. This must be done after all tables have been created
216     # to ensure that all referred tables will exist.
217     for app in models.get_apps():
218-        app_name = app.__name__.split('.')[-2]
219+        app_name = app.label
220         model_list = models.get_models(app)
221         for model in model_list:
222             if model in created_models:
223@@ -539,7 +539,7 @@
224     # Install custom SQL for the app (but only if this
225     # is a model we've just created)
226     for app in models.get_apps():
227-        app_name = app.__name__.split('.')[-2]
228+        app_name = app.label
229         for model in models.get_models(app):
230             if model in created_models:
231                 custom_sql = get_custom_sql_for_model(model)
232@@ -556,9 +556,9 @@
233                     else:
234                         transaction.commit_unless_managed()
235 
236-    # Install SQL indicies for all newly created models
237+    # Install SQL indices for all newly created models
238     for app in models.get_apps():
239-        app_name = app.__name__.split('.')[-2]
240+        app_name = app.label
241         for model in models.get_models(app):
242             if model in created_models:
243                 index_sql = get_sql_indexes_for_model(model)
244@@ -676,7 +676,7 @@
245 
246 def flush(verbosity=1, interactive=True):
247     "Returns all tables in the database to the same state they were in immediately after syncdb."
248-    from django.conf import settings
249+    from django.conf import settings, get_installed_app_paths
250     from django.db import connection, transaction, models
251     from django.dispatch import dispatcher
252 
253@@ -687,7 +687,7 @@
254 
255     # Import the 'management' module within each installed app, to register
256     # dispatcher events.
257-    for app_name in settings.INSTALLED_APPS:
258+    for app_name in get_installed_app_paths():
259         try:
260             __import__(app_name + '.management', {}, {}, [''])
261         except ImportError:
262@@ -1293,9 +1293,9 @@
263     from django.db.models import get_app, get_apps
264 
265     if len(app_labels) == 0:
266-        app_list = get_apps()
267+        app_list = [app.models_module for app in get_apps()]
268     else:
269-        app_list = [get_app(app_label) for app_label in app_labels]
270+        app_list = [get_app(app_label).models_module for app_label in app_labels]
271 
272     test_path = settings.TEST_RUNNER.split('.')
273     # Allow for Python 2.5 relative paths
274@@ -1340,7 +1340,11 @@
275     transaction.enter_transaction_management()
276     transaction.managed(True)
277 
278-    app_fixtures = [os.path.join(os.path.dirname(app.__file__),'fixtures') for app in get_apps()]
279+    app_fixtures = []
280+    for app in get_apps():
281+        if app.models_module:
282+            app_fixtures.append(os.path.join(os.path.dirname(
283+                                app.models_module.__file__),'fixtures'))
284     for fixture_label in fixture_labels:
285         parts = fixture_label.split('.')
286         if len(parts) == 1:
287@@ -1619,19 +1623,19 @@
288         from django.db import models
289         validate(silent_success=True)
290         try:
291-            mod_list = [models.get_app(app_label) for app_label in args[1:]]
292+            app_list = [models.get_app(app_label) for app_label in args[1:]]
293         except ImportError, e:
294             sys.stderr.write(style.ERROR("Error: %s. Are you sure your INSTALLED_APPS setting is correct?\n" % e))
295             sys.exit(1)
296-        if not mod_list:
297+        if not app_list:
298             parser.print_usage_and_exit()
299         if action not in NO_SQL_TRANSACTION:
300             print style.SQL_KEYWORD("BEGIN;")
301-        for mod in mod_list:
302+        for app in app_list:
303             if action == 'reset':
304-                output = action_mapping[action](mod, options.interactive)
305+                output = action_mapping[action](app, options.interactive)
306             else:
307-                output = action_mapping[action](mod)
308+                output = action_mapping[action](app)
309             if output:
310                 print '\n'.join(output)
311         if action not in NO_SQL_TRANSACTION:
312
313=== modified file 'django/db/models/__init__.py'
314--- django/db/models/__init__.py       
315+++ django/db/models/__init__.py       
316@@ -2,7 +2,7 @@
317 from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
318 from django.core import validators
319 from django.db import backend, connection
320-from django.db.models.loading import get_apps, get_app, get_models, get_model, register_models
321+from django.db.models.loading import get_apps, get_app, get_models, get_model, register_models, find_app
322 from django.db.models.query import Q
323 from django.db.models.manager import Manager
324 from django.db.models.base import Model, AdminOptions
325
326=== modified file 'django/db/models/base.py'
327--- django/db/models/base.py   
328+++ django/db/models/base.py   
329@@ -8,7 +8,7 @@
330 from django.db.models.options import Options, AdminOptions
331 from django.db import connection, backend, transaction
332 from django.db.models import signals
333-from django.db.models.loading import register_models, get_model
334+from django.db.models.loading import register_models, get_model, get_app_label
335 from django.dispatch import dispatcher
336 from django.utils.datastructures import SortedDict
337 from django.utils.functional import curry
338@@ -42,12 +42,9 @@
339                 new_class._meta.parents.append(base)
340                 new_class._meta.parents.extend(base._meta.parents)
341 
342-        model_module = sys.modules[new_class.__module__]
343-
344         if getattr(new_class._meta, 'app_label', None) is None:
345-            # Figure out the app_label by looking one level up.
346-            # For 'django.contrib.sites.models', this would be 'sites'.
347-            new_class._meta.app_label = model_module.__name__.split('.')[-2]
348+            # Figure out the app_label.
349+            new_class._meta.app_label = get_app_label(new_class)
350 
351         # Bail out early if we have already created this class.
352         m = get_model(new_class._meta.app_label, name, False)
353
354=== modified file 'django/db/models/loading.py'
355--- django/db/models/loading.py
356+++ django/db/models/loading.py
357@@ -1,6 +1,6 @@
358 "Utilities for loading models and the modules that contain them."
359 
360-from django.conf import settings
361+from django.conf import settings, app
362 from django.core.exceptions import ImproperlyConfigured
363 import sys
364 import os
365@@ -18,42 +18,118 @@
366 _loaded = False  # Has the contents of settings.INSTALLED_APPS been loaded?
367                  # i.e., has get_apps() been called?
368 
369+_app_map = {}    # Map of app instances keyed by app_label.
370+_mod_map = {}    # Map of app instances keyed by app module names.
371+_apps = []       # List of app instances. Shadows _app_list.
372+
373+_set_apps = None # what INSTALLED_APPS was when _loaded was set to True
374+
375 def get_apps():
376-    "Returns a list of all installed modules that contain models."
377+    "Returns a list of all loaded applications."
378     global _app_list
379     global _loaded
380-    if not _loaded:
381+    global _app_map
382+    global _mod_map
383+    global _set_apps
384+    global _apps
385+
386+    changed = (_set_apps != settings.INSTALLED_APPS)
387+    if changed or not _loaded:
388+        if changed:
389+            _apps = []
390+            _app_map = {}
391+            _mod_map = {}
392         _loaded = True
393-        for app_name in settings.INSTALLED_APPS:
394+        _set_apps = settings.INSTALLED_APPS[:]
395+        # We first loop through, setting up the app instances and
396+        # the relevant maps so that we can find the instances by app_label
397+        # or app module name. These need to be ready because we need to be
398+        # able to get the app_label e.g. for applying to model classes.
399+        # If a model class is loaded indirectly without being in an
400+        # installed app, the app_label used will be what it is now - the
401+        # last part of the app module name.
402+        # Note that _app_map and _mod_map will contain entries even
403+        # when an app cannot be loaded by load_app.
404+        for app_entry in settings.INSTALLED_APPS:
405+            if isinstance(app_entry, basestring):
406+                the_app = app(app_entry)
407+            else:
408+                the_app = app_entry
409+            _app_map[the_app.label] = the_app
410+            _mod_map[the_app.path] = the_app
411+
412+        for app_entry in settings.INSTALLED_APPS:
413+            if isinstance(app_entry, basestring):
414+                the_app = _mod_map[app_entry]
415+            else:
416+                the_app = app_entry
417+            app_name = the_app.path
418             try:
419                 load_app(app_name)
420+                _apps.append(the_app)
421             except Exception, e:
422                 # Problem importing the app
423                 _app_errors[app_name] = e
424-    return _app_list
425+    return _apps
426+
427+def get_app_label(model_class):
428+    "Returns the app label to be used for a model class."
429+    global _mod_map
430+    key = model_class.__module__
431+    i = key.rfind('.')
432+    assert i > 0, "Model class must be defined in a package sub-module"
433+    key = key[:i]
434+    if key in _mod_map:
435+        rv = _mod_map[key].label
436+    else:
437+        i = key.rfind('.')
438+        if i < 0:
439+            rv = key
440+        else:
441+            rv = key[i + 1:]
442+    return rv
443+
444+def find_app(app_path):
445+    "Find an app instance, given the application path."
446+    get_apps()
447+    return _mod_map[app_path] # _mod_map.get(app_path, None)
448 
449 def get_app(app_label, emptyOK=False):
450-    "Returns the module containing the models for the given app_label. If the app has no models in it and 'emptyOK' is True, returns None."
451+    "Returns the app instance for the given app_label. If the app has no models in it and 'emptyOK' is True, returns None."
452     get_apps() # Run get_apps() to populate the _app_list cache. Slightly hackish.
453-    for app_name in settings.INSTALLED_APPS:
454-        if app_label == app_name.split('.')[-1]:
455-            mod = load_app(app_name)
456-            if mod is None:
457-                if emptyOK:
458-                    return None
459-            else:
460-                return mod
461+    if app_label not in _app_map:
462+        rv = None
463+    else:
464+        rv = _app_map[app_label]
465+    if emptyOK or rv is not None:
466+        return rv
467     raise ImproperlyConfigured, "App with label %s could not be found" % app_label
468 
469 def load_app(app_name):
470-    "Loads the app with the provided fully qualified name, and returns the model module."
471+    "Loads the app with the provided fully qualified name, and returns the models module."
472     global _app_list
473+    global _app_map
474+    global _mod_map
475+    global _apps
476     mod = __import__(app_name, {}, {}, ['models'])
477+    if app_name in _mod_map:
478+        the_app = _mod_map[app_name]
479+    else:
480+        # An app can be loaded by load_app even before get_apps is
481+        # called. In this case, we just make a new app and add it to the maps.
482+        the_app = app(app_name)
483+        _app_map[the_app.label] = the_app
484+        _mod_map[app_name] = the_app
485+        _apps.append(the_app)
486+    the_app.app_module = mod
487     if not hasattr(mod, 'models'):
488-        return None
489-    if mod.models not in _app_list:
490-        _app_list.append(mod.models)
491-    return mod.models
492+        rv = None
493+    else:
494+        rv = mod.models
495+        if rv not in _app_list:
496+            _app_list.append(rv)
497+    the_app.models_module = rv
498+    return rv
499 
500 def get_app_errors():
501     "Returns the map of known problems with the INSTALLED_APPS"
502@@ -61,18 +137,18 @@
503     get_apps() # Run get_apps() to populate the _app_list cache. Slightly hackish.
504     return _app_errors
505 
506-def get_models(app_mod=None):
507+def get_models(the_app=None):
508     """
509-    Given a module containing models, returns a list of the models. Otherwise
510-    returns a list of all installed models.
511+    Given an app instance, returns a list of its models. Otherwise
512+    returns a list of all loaded models.
513     """
514-    app_list = get_apps() # Run get_apps() to populate the _app_list cache. Slightly hackish.
515-    if app_mod:
516-        return _app_models.get(app_mod.__name__.split('.')[-2], {}).values()
517+    get_apps() # Run get_apps() to populate the _app_list cache. Slightly hackish.
518+    if the_app:
519+        return _app_models.get(the_app.label, {}).values()
520     else:
521         model_list = []
522-        for app_mod in app_list:
523-            model_list.extend(get_models(app_mod))
524+        for the_app in _apps:
525+            model_list.extend(get_models(the_app))
526         return model_list
527 
528 def get_model(app_label, model_name, seed_cache=True):
529
530=== modified file 'django/db/models/options.py'
531--- django/db/models/options.py
532+++ django/db/models/options.py
533@@ -1,4 +1,4 @@
534-from django.conf import settings
535+from django.conf import settings, get_installed_app_paths
536 from django.db.models.related import RelatedObject
537 from django.db.models.fields.related import ManyToManyRel
538 from django.db.models.fields import AutoField, FieldDoesNotExist
539@@ -36,7 +36,7 @@
540 
541     def contribute_to_class(self, cls, name):
542         cls._meta = self
543-        self.installed = re.sub('\.models$', '', cls.__module__) in settings.INSTALLED_APPS
544+        self.installed = re.sub('\.models$', '', cls.__module__) in get_installed_app_paths()
545         # First, construct the default values for these options.
546         self.object_name = cls.__name__
547         self.module_name = self.object_name.lower()
548
549=== modified file 'django/template/loaders/app_directories.py'
550--- django/template/loaders/app_directories.py 
551+++ django/template/loaders/app_directories.py 
552@@ -1,13 +1,13 @@
553 # Wrapper for loading templates from "template" directories in installed app packages.
554 
555-from django.conf import settings
556+from django.conf import settings, get_installed_app_paths
557 from django.core.exceptions import ImproperlyConfigured
558 from django.template import TemplateDoesNotExist
559 import os
560 
561 # At compile time, cache the directories to search.
562 app_template_dirs = []
563-for app in settings.INSTALLED_APPS:
564+for app in get_installed_app_paths():
565     i = app.rfind('.')
566     if i == -1:
567         m, a = app, None
568
569=== modified file 'django/template/loaders/eggs.py'
570--- django/template/loaders/eggs.py     
571+++ django/template/loaders/eggs.py     
572@@ -6,7 +6,7 @@
573     resource_string = None
574 
575 from django.template import TemplateDoesNotExist
576-from django.conf import settings
577+from django.conf import settings, get_installed_app_paths
578 
579 def load_template_source(template_name, template_dirs=None):
580     """
581@@ -16,7 +16,7 @@
582     """
583     if resource_string is not None:
584         pkg_name = 'templates/' + template_name
585-        for app in settings.INSTALLED_APPS:
586+        for app in get_installed_app_paths():
587             try:
588                 return (resource_string(app, pkg_name), 'egg:%s:%s ' % (app, pkg_name))
589             except:
590
591=== modified file 'django/templatetags/__init__.py'
592--- django/templatetags/__init__.py     
593+++ django/templatetags/__init__.py     
594@@ -1,6 +1,6 @@
595-from django.conf import settings
596+from django.conf import settings, get_installed_app_paths
597 
598-for a in settings.INSTALLED_APPS:
599+for a in get_installed_app_paths():
600     try:
601         __path__.extend(__import__(a + '.templatetags', {}, {}, ['']).__path__)
602     except ImportError:
603
604=== modified file 'django/test/client.py'
605--- django/test/client.py       
606+++ django/test/client.py       
607@@ -1,7 +1,7 @@
608 import sys
609 from cStringIO import StringIO
610 from urlparse import urlparse
611-from django.conf import settings
612+from django.conf import settings, get_installed_app_paths
613 from django.core.handlers.base import BaseHandler
614 from django.core.handlers.wsgi import WSGIRequest
615 from django.core.signals import got_request_exception
616@@ -175,7 +175,7 @@
617         if response.cookies:
618             self.cookies.update(response.cookies)
619 
620-        if 'django.contrib.sessions' in settings.INSTALLED_APPS:
621+        if 'django.contrib.sessions' in get_installed_app_paths():
622             from django.contrib.sessions.middleware import SessionWrapper
623             cookie = self.cookies.get(settings.SESSION_COOKIE_NAME, None)
624             if cookie:
625
626=== modified file 'django/utils/translation/trans_real.py'
627--- django/utils/translation/trans_real.py     
628+++ django/utils/translation/trans_real.py     
629@@ -107,7 +107,7 @@
630     if t is not None:
631         return t
632 
633-    from django.conf import settings
634+    from django.conf import settings, get_installed_app_paths
635 
636     # set up the right translation class
637     klass = DjangoTranslation
638@@ -160,7 +160,7 @@
639         if projectpath and os.path.isdir(projectpath):
640             res = _merge(projectpath)
641 
642-        for appname in settings.INSTALLED_APPS:
643+        for appname in get_installed_app_paths():
644             p = appname.rfind('.')
645             if p >= 0:
646                 app = getattr(__import__(appname[:p], {}, {}, [appname[p+1:]]), appname[p+1:])
647
648=== modified file 'django/views/i18n.py'
649--- django/views/i18n.py       
650+++ django/views/i18n.py       
651@@ -1,7 +1,7 @@
652 from django import http
653 from django.utils.translation import check_for_language, activate, to_locale, get_language
654 from django.utils.text import javascript_quote
655-from django.conf import settings
656+from django.conf import settings, get_installed_app_paths
657 import os
658 import gettext as gettext_module
659 
660@@ -104,7 +104,7 @@
661         packages = ['django.conf']
662     if type(packages) in (str, unicode):
663         packages = packages.split('+')
664-    packages = [p for p in packages if p == 'django.conf' or p in settings.INSTALLED_APPS]
665+    packages = [p for p in packages if p == 'django.conf' or p in get_installed_app_paths()]
666     default_locale = to_locale(settings.LANGUAGE_CODE)
667     locale = to_locale(get_language())
668     t = {}
669
670=== modified file 'docs/settings.txt'
671--- docs/settings.txt   
672+++ docs/settings.txt   
673@@ -467,9 +467,29 @@
674 
675 Default: ``()`` (Empty tuple)
676 
677-A tuple of strings designating all applications that are enabled in this Django
678-installation. Each string should be a full Python path to a Python package that
679-contains a Django application, as created by `django-admin.py startapp`_.
680+A tuple of entries designating all applications that are enabled in this Django
681+installation. Each entry should be one of the following:
682+
683+    * A string which is a full Python path to a Python package that contains a
684+      Django application, as created by `django-admin.py startapp`_.
685+    * A string which is a full Python path to a Python package which contains
686+      multiple applications, with an appended ``.*``. This entry is replaced
687+      by entries for the contained applications. For example, a wildcard
688+      entry of the form ``django.contrib.*`` would be replaced by
689+      multiple entries for ``django.contrib.admin``, ``django.contrib.auth``
690+      etc.
691+    * An ``app`` configuration directive (to use it, you need to add
692+      ``from django.conf import app`` to your ``settings`` file). This takes
693+      the form ``app(path, label=None, verbose_name=None)``. The ``path``
694+      argument must specify the full Python path to a Python package that
695+      contains a Django application. The ``label`` argument, if specified,
696+      provides a unique application label (used to disambiguate different
697+      third-party applications with conflicting default label values). If this
698+      value is not specified, the last portion of the application path is used
699+      as the default label. The ``verbose_name`` argument, if specified, is
700+      used to provide a descriptive name for the application in the admin
701+      screens. If this value is not specified, the last portion of the
702+      application path is used as the default value.
703 
704 .. _django-admin.py startapp: ../django-admin/#startapp-appname
705 
706
707=== modified file 'tests/runtests.py'
708--- tests/runtests.py   
709+++ tests/runtests.py   
710@@ -48,16 +48,16 @@
711 
712     def runTest(self):
713         from django.core import management
714-        from django.db.models.loading import load_app
715+        from django.db.models.loading import load_app, find_app
716         from cStringIO import StringIO
717 
718         try:
719             module = load_app(self.model_label)
720         except Exception, e:
721-            self.fail('Unable to load invalid model module')
722+            self.fail('Unable to load invalid application %s: %s' % (self.model_label, e))
723 
724         s = StringIO()
725-        count = management.get_validation_errors(s, module)
726+        count = management.get_validation_errors(s, find_app(self.model_label))
727         s.seek(0)
728         error_log = s.read()
729         actual = error_log.split('\n')
730