summaryrefslogtreecommitdiff
path: root/horizon/base.py
blob: ee0bf2d09fe33047d8ad1c6ef08af6a1c392ba52 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
# Copyright 2012 Nebula, Inc.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""
Contains the core classes and functionality that makes Horizon what it is.
This module is considered internal, and should not be relied on directly.

Public APIs are made available through the :mod:`horizon` module and
the classes contained therein.
"""

import collections
import copy
import inspect
import logging
import os

from django.conf import settings
from django.conf.urls import include
from django.conf.urls import patterns
from django.conf.urls import url
from django.core.exceptions import ImproperlyConfigured  # noqa
from django.core.urlresolvers import reverse
from django.utils.datastructures import SortedDict
from django.utils.functional import SimpleLazyObject  # noqa
from django.utils.importlib import import_module  # noqa
from django.utils.module_loading import module_has_submodule  # noqa
from django.utils.translation import ugettext_lazy as _

from horizon import conf
from horizon.decorators import _current_component  # noqa
from horizon.decorators import require_auth  # noqa
from horizon.decorators import require_perms  # noqa
from horizon import loaders


LOG = logging.getLogger(__name__)


def _decorate_urlconf(urlpatterns, decorator, *args, **kwargs):
    for pattern in urlpatterns:
        if getattr(pattern, 'callback', None):
            pattern._callback = decorator(pattern.callback, *args, **kwargs)
        if getattr(pattern, 'url_patterns', []):
            _decorate_urlconf(pattern.url_patterns, decorator, *args, **kwargs)


def access_cached(func):
    def inner(self, context):
        session = context['request'].session
        try:
            if session['allowed']['valid_for'] != session.get('token'):
                raise KeyError()
        except KeyError:
            session['allowed'] = {"valid_for": session.get('token')}

        key = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
        if key not in session['allowed']:
            session['allowed'][key] = func(self, context)
            session.modified = True
        return session['allowed'][key]
    return inner


class NotRegistered(Exception):
    pass


class HorizonComponent(object):
    policy_rules = None

    def __init__(self):
        super(HorizonComponent, self).__init__()
        if not self.slug:
            raise ImproperlyConfigured('Every %s must have a slug.'
                                       % self.__class__)

    def __unicode__(self):
        name = getattr(self, 'name', u"Unnamed %s" % self.__class__.__name__)
        return unicode(name)

    def _get_default_urlpatterns(self):
        package_string = '.'.join(self.__module__.split('.')[:-1])
        if getattr(self, 'urls', None):
            try:
                mod = import_module('.%s' % self.urls, package_string)
            except ImportError:
                mod = import_module(self.urls)
            urlpatterns = mod.urlpatterns
        else:
            # Try importing a urls.py from the dashboard package
            if module_has_submodule(import_module(package_string), 'urls'):
                urls_mod = import_module('.urls', package_string)
                urlpatterns = urls_mod.urlpatterns
            else:
                urlpatterns = patterns('')
        return urlpatterns

    @access_cached
    def can_access(self, context):
        """Return whether the user has role based access to this component.

        This method is not intended to be overridden.
        The result of the method is stored in per-session cache.
        """
        return self.allowed(context)

    def allowed(self, context):
        """Checks if the user is allowed to access this component.

        This method should be overridden to return the result of
        any policy checks required for the user to access this component
        when more complex checks are required.
        """
        return self._can_access(context['request'])

    def _can_access(self, request):
        policy_check = getattr(settings, "POLICY_CHECK_FUNCTION", None)

        # this check is an OR check rather than an AND check that is the
        # default in the policy engine, so calling each rule individually
        if policy_check and self.policy_rules:
            for rule in self.policy_rules:
                if policy_check((rule,), request):
                    return True
            return False

        # default to allowed
        return True


class Registry(object):
    def __init__(self):
        self._registry = {}
        if not getattr(self, '_registerable_class', None):
            raise ImproperlyConfigured('Subclasses of Registry must set a '
                                       '"_registerable_class" property.')

    def _register(self, cls):
        """Registers the given class.

        If the specified class is already registered then it is ignored.
        """
        if not inspect.isclass(cls):
            raise ValueError('Only classes may be registered.')
        elif not issubclass(cls, self._registerable_class):
            raise ValueError('Only %s classes or subclasses may be registered.'
                             % self._registerable_class.__name__)

        if cls not in self._registry:
            cls._registered_with = self
            self._registry[cls] = cls()

        return self._registry[cls]

    def _unregister(self, cls):
        """Unregisters the given class.

        If the specified class isn't registered, ``NotRegistered`` will
        be raised.
        """
        if not issubclass(cls, self._registerable_class):
            raise ValueError('Only %s classes or subclasses may be '
                             'unregistered.' % self._registerable_class)

        if cls not in self._registry.keys():
            raise NotRegistered('%s is not registered' % cls)

        del self._registry[cls]

        return True

    def _registered(self, cls):
        if inspect.isclass(cls) and issubclass(cls, self._registerable_class):
            found = self._registry.get(cls, None)
            if found:
                return found
        else:
            # Allow for fetching by slugs as well.
            for registered in self._registry.values():
                if registered.slug == cls:
                    return registered
        class_name = self._registerable_class.__name__
        if hasattr(self, "_registered_with"):
            parent = self._registered_with._registerable_class.__name__
            raise NotRegistered('%(type)s with slug "%(slug)s" is not '
                                'registered with %(parent)s "%(name)s".'
                                % {"type": class_name,
                                   "slug": cls,
                                   "parent": parent,
                                   "name": self.slug})
        else:
            slug = getattr(cls, "slug", cls)
            raise NotRegistered('%(type)s with slug "%(slug)s" is not '
                                'registered.' % {"type": class_name,
                                                 "slug": slug})


class Panel(HorizonComponent):
    """A base class for defining Horizon dashboard panels.

    All Horizon dashboard panels should extend from this class. It provides
    the appropriate hooks for automatically constructing URLconfs, and
    providing permission-based access control.

    .. attribute:: name

        The name of the panel. This will be displayed in the
        auto-generated navigation and various other places.
        Default: ``''``.

    .. attribute:: slug

        A unique "short name" for the panel. The slug is used as
        a component of the URL path for the panel. Default: ``''``.

    .. attribute:: permissions

        A list of permission names, all of which a user must possess in order
        to access any view associated with this panel. This attribute
        is combined cumulatively with any permissions required on the
        ``Dashboard`` class with which it is registered.

    .. attribute:: urls

        Path to a URLconf of views for this panel using dotted Python
        notation. If no value is specified, a file called ``urls.py``
        living in the same package as the ``panel.py`` file is used.
        Default: ``None``.

    .. attribute:: nav
    .. method:: nav(context)

        The ``nav`` attribute can be either boolean value or a callable
        which accepts a ``RequestContext`` object as a single argument
        to control whether or not this panel should appear in
        automatically-generated navigation. Default: ``True``.

    .. attribute:: index_url_name

        The ``name`` argument for the URL pattern which corresponds to
        the index view for this ``Panel``. This is the view that
        :meth:`.Panel.get_absolute_url` will attempt to reverse.
    """
    name = ''
    slug = ''
    urls = None
    nav = True
    index_url_name = "index"

    def __repr__(self):
        return "<Panel: %s>" % self.slug

    def get_absolute_url(self):
        """Returns the default URL for this panel.

        The default URL is defined as the URL pattern with ``name="index"`` in
        the URLconf for this panel.
        """
        try:
            return reverse('horizon:%s:%s:%s' % (self._registered_with.slug,
                                                 self.slug,
                                                 self.index_url_name))
        except Exception as exc:
            # Logging here since this will often be called in a template
            # where the exception would be hidden.
            LOG.info("Error reversing absolute URL for %s: %s" % (self, exc))
            raise

    @property
    def _decorated_urls(self):
        urlpatterns = self._get_default_urlpatterns()

        # Apply access controls to all views in the patterns
        permissions = getattr(self, 'permissions', [])
        _decorate_urlconf(urlpatterns, require_perms, permissions)
        _decorate_urlconf(urlpatterns, _current_component, panel=self)

        # Return the three arguments to django.conf.urls.include
        return urlpatterns, self.slug, self.slug


class PanelGroup(object):
    """A container for a set of :class:`~horizon.Panel` classes.

    When iterated, it will yield each of the ``Panel`` instances it
    contains.

    .. attribute:: slug

        A unique string to identify this panel group. Required.

    .. attribute:: name

        A user-friendly name which will be used as the group heading in
        places such as the navigation. Default: ``None``.

    .. attribute:: panels

        A list of panel module names which should be contained within this
        grouping.
    """
    def __init__(self, dashboard, slug=None, name=None, panels=None):
        self.dashboard = dashboard
        self.slug = slug or getattr(self, "slug", "default")
        self.name = name or getattr(self, "name", None)
        # Our panels must be mutable so it can be extended by others.
        self.panels = list(panels or getattr(self, "panels", []))

    def __repr__(self):
        return "<%s: %s>" % (self.__class__.__name__, self.slug)

    def __unicode__(self):
        return self.name

    def __iter__(self):
        panel_instances = []
        for name in self.panels:
            try:
                panel_instances.append(self.dashboard.get_panel(name))
            except NotRegistered as e:
                LOG.debug(e)
        return iter(panel_instances)


class Dashboard(Registry, HorizonComponent):
    """A base class for defining Horizon dashboards.

    All Horizon dashboards should extend from this base class. It provides the
    appropriate hooks for automatic discovery of :class:`~horizon.Panel`
    modules, automatically constructing URLconfs, and providing
    permission-based access control.

    .. attribute:: name

        The name of the dashboard. This will be displayed in the
        auto-generated navigation and various other places.
        Default: ``''``.

    .. attribute:: slug

        A unique "short name" for the dashboard. The slug is used as
        a component of the URL path for the dashboard. Default: ``''``.

    .. attribute:: panels

        The ``panels`` attribute can be either a flat list containing the name
        of each panel **module**  which should be loaded as part of this
        dashboard, or a list of :class:`~horizon.PanelGroup` classes which
        define groups of panels as in the following example::

            class SystemPanels(horizon.PanelGroup):
                slug = "syspanel"
                name = _("System")
                panels = ('overview', 'instances', ...)

            class Syspanel(horizon.Dashboard):
                panels = (SystemPanels,)

        Automatically generated navigation will use the order of the
        modules in this attribute.

        Default: ``[]``.

        .. warning::

            The values for this attribute should not correspond to the
            :attr:`~.Panel.name` attributes of the ``Panel`` classes.
            They should be the names of the Python modules in which the
            ``panel.py`` files live. This is used for the automatic
            loading and registration of ``Panel`` classes much like
            Django's ``ModelAdmin`` machinery.

            Panel modules must be listed in ``panels`` in order to be
            discovered by the automatic registration mechanism.

    .. attribute:: default_panel

        The name of the panel which should be treated as the default
        panel for the dashboard, i.e. when you visit the root URL
        for this dashboard, that's the panel that is displayed.
        Default: ``None``.

    .. attribute:: permissions

        A list of permission names, all of which a user must possess in order
        to access any panel registered with this dashboard. This attribute
        is combined cumulatively with any permissions required on individual
        :class:`~horizon.Panel` classes.

    .. attribute:: urls

        Optional path to a URLconf of additional views for this dashboard
        which are not connected to specific panels. Default: ``None``.

    .. attribute:: nav
    .. method:: nav(context)

        The ``nav`` attribute can be either boolean value or a callable
        which accepts a ``RequestContext`` object as a single argument
        to control whether or not this dashboard should appear in
        automatically-generated navigation. Default: ``True``.

    .. attribute:: public

        Boolean value to determine whether this dashboard can be viewed
        without being logged in. Defaults to ``False``.

    """
    _registerable_class = Panel
    name = ''
    slug = ''
    urls = None
    panels = []
    default_panel = None
    nav = True
    public = False

    def __repr__(self):
        return "<Dashboard: %s>" % self.slug

    def __init__(self, *args, **kwargs):
        super(Dashboard, self).__init__(*args, **kwargs)
        self._panel_groups = None

    def get_panel(self, panel):
        """Returns the specified :class:`~horizon.Panel` instance registered
        with this dashboard.
        """
        return self._registered(panel)

    def get_panels(self):
        """Returns the :class:`~horizon.Panel` instances registered with this
        dashboard in order, without any panel groupings.
        """
        all_panels = []
        panel_groups = self.get_panel_groups()
        for panel_group in panel_groups.values():
            all_panels.extend(panel_group)
        return all_panels

    def get_panel_group(self, slug):
        """Returns the specified :class:~horizon.PanelGroup
        or None if not registered
        """
        return self._panel_groups.get(slug)

    def get_panel_groups(self):
        registered = copy.copy(self._registry)
        panel_groups = []

        # Gather our known panels
        if self._panel_groups is not None:
            for panel_group in self._panel_groups.values():
                for panel in panel_group:
                    registered.pop(panel.__class__)
                panel_groups.append((panel_group.slug, panel_group))

        # Deal with leftovers (such as add-on registrations)
        if len(registered):
            slugs = [panel.slug for panel in registered.values()]
            new_group = PanelGroup(self,
                                   slug="other",
                                   name=_("Other"),
                                   panels=slugs)
            panel_groups.append((new_group.slug, new_group))
        return SortedDict(panel_groups)

    def get_absolute_url(self):
        """Returns the default URL for this dashboard.

        The default URL is defined as the URL pattern with ``name="index"``
        in the URLconf for the :class:`~horizon.Panel` specified by
        :attr:`~horizon.Dashboard.default_panel`.
        """
        try:
            return self._registered(self.default_panel).get_absolute_url()
        except Exception:
            # Logging here since this will often be called in a template
            # where the exception would be hidden.
            LOG.exception("Error reversing absolute URL for %s." % self)
            raise

    @property
    def _decorated_urls(self):
        urlpatterns = self._get_default_urlpatterns()

        default_panel = None

        # Add in each panel's views except for the default view.
        for panel in self._registry.values():
            if panel.slug == self.default_panel:
                default_panel = panel
                continue
            url_slug = panel.slug.replace('.', '/')
            urlpatterns += patterns('',
                                    url(r'^%s/' % url_slug,
                                        include(panel._decorated_urls)))
        # Now the default view, which should come last
        if not default_panel:
            raise NotRegistered('The default panel "%s" is not registered.'
                                % self.default_panel)
        urlpatterns += patterns('',
                                url(r'',
                                    include(default_panel._decorated_urls)))

        # Require login if not public.
        if not self.public:
            _decorate_urlconf(urlpatterns, require_auth)
        # Apply access controls to all views in the patterns
        permissions = getattr(self, 'permissions', [])
        _decorate_urlconf(urlpatterns, require_perms, permissions)
        _decorate_urlconf(urlpatterns, _current_component, dashboard=self)

        # Return the three arguments to django.conf.urls.include
        return urlpatterns, self.slug, self.slug

    def _autodiscover(self):
        """Discovers panels to register from the current dashboard module."""
        if getattr(self, "_autodiscover_complete", False):
            return

        panels_to_discover = []
        panel_groups = []
        # If we have a flat iterable of panel names, wrap it again so
        # we have a consistent structure for the next step.
        if all([isinstance(i, basestring) for i in self.panels]):
            self.panels = [self.panels]

        # Now iterate our panel sets.
        for panel_set in self.panels:
            # Instantiate PanelGroup classes.
            if not isinstance(panel_set, collections.Iterable) and \
                    issubclass(panel_set, PanelGroup):
                panel_group = panel_set(self)
            # Check for nested tuples, and convert them to PanelGroups
            elif not isinstance(panel_set, PanelGroup):
                panel_group = PanelGroup(self, panels=panel_set)

            # Put our results into their appropriate places
            panels_to_discover.extend(panel_group.panels)
            panel_groups.append((panel_group.slug, panel_group))

        self._panel_groups = SortedDict(panel_groups)

        # Do the actual discovery
        package = '.'.join(self.__module__.split('.')[:-1])
        mod = import_module(package)
        for panel in panels_to_discover:
            try:
                before_import_registry = copy.copy(self._registry)
                import_module('.%s.panel' % panel, package)
            except Exception:
                self._registry = before_import_registry
                if module_has_submodule(mod, panel):
                    raise
        self._autodiscover_complete = True

    @classmethod
    def register(cls, panel):
        """Registers a :class:`~horizon.Panel` with this dashboard."""
        panel_class = Horizon.register_panel(cls, panel)
        # Support template loading from panel template directories.
        panel_mod = import_module(panel.__module__)
        panel_dir = os.path.dirname(panel_mod.__file__)
        template_dir = os.path.join(panel_dir, "templates")
        if os.path.exists(template_dir):
            key = os.path.join(cls.slug, panel.slug)
            loaders.panel_template_dirs[key] = template_dir
        return panel_class

    @classmethod
    def unregister(cls, panel):
        """Unregisters a :class:`~horizon.Panel` from this dashboard."""
        success = Horizon.unregister_panel(cls, panel)
        if success:
            # Remove the panel's template directory.
            key = os.path.join(cls.slug, panel.slug)
            if key in loaders.panel_template_dirs:
                del loaders.panel_template_dirs[key]
        return success

    def allowed(self, context):
        """Checks for role based access for this dashboard.

        Checks for access to any panels in the dashboard and of the the
        dashboard itself.

        This method should be overridden to return the result of
        any policy checks required for the user to access this dashboard
        when more complex checks are required.
        """

        # if the dashboard has policy rules, honor those above individual
        # panels
        if not self._can_access(context['request']):
            return False

        # check if access is allowed to a single panel,
        # the default for each panel is True
        for panel in self.get_panels():
            if panel.can_access(context):
                return True

        return False


class Workflow(object):
    pass

try:
    from django.utils.functional import empty  # noqa
except ImportError:
    # Django 1.3 fallback
    empty = None


class LazyURLPattern(SimpleLazyObject):
    def __iter__(self):
        if self._wrapped is empty:
            self._setup()
        return iter(self._wrapped)

    def __reversed__(self):
        if self._wrapped is empty:
            self._setup()
        return reversed(self._wrapped)

    def __len__(self):
        if self._wrapped is empty:
            self._setup()
        return len(self._wrapped)

    def __getitem__(self, idx):
        if self._wrapped is empty:
            self._setup()
        return self._wrapped[idx]


class Site(Registry, HorizonComponent):
    """The overarching class which encompasses all dashboards and panels."""

    # Required for registry
    _registerable_class = Dashboard

    name = "Horizon"
    namespace = 'horizon'
    slug = 'horizon'
    urls = 'horizon.site_urls'

    def __repr__(self):
        return u"<Site: %s>" % self.slug

    @property
    def _conf(self):
        return conf.HORIZON_CONFIG

    @property
    def dashboards(self):
        return self._conf['dashboards']

    @property
    def default_dashboard(self):
        return self._conf['default_dashboard']

    def register(self, dashboard):
        """Registers a :class:`~horizon.Dashboard` with Horizon."""
        return self._register(dashboard)

    def unregister(self, dashboard):
        """Unregisters a :class:`~horizon.Dashboard` from Horizon."""
        return self._unregister(dashboard)

    def registered(self, dashboard):
        return self._registered(dashboard)

    def register_panel(self, dashboard, panel):
        dash_instance = self.registered(dashboard)
        return dash_instance._register(panel)

    def unregister_panel(self, dashboard, panel):
        dash_instance = self.registered(dashboard)
        if not dash_instance:
            raise NotRegistered("The dashboard %s is not registered."
                                % dashboard)
        return dash_instance._unregister(panel)

    def get_dashboard(self, dashboard):
        """Returns the specified :class:`~horizon.Dashboard` instance."""
        return self._registered(dashboard)

    def get_dashboards(self):
        """Returns an ordered tuple of :class:`~horizon.Dashboard` modules.

        Orders dashboards according to the ``"dashboards"`` key in
        ``HORIZON_CONFIG`` or else returns all registered dashboards
        in alphabetical order.

        Any remaining :class:`~horizon.Dashboard` classes registered with
        Horizon but not listed in ``HORIZON_CONFIG['dashboards']``
        will be appended to the end of the list alphabetically.
        """
        if self.dashboards:
            registered = copy.copy(self._registry)
            dashboards = []
            for item in self.dashboards:
                dashboard = self._registered(item)
                dashboards.append(dashboard)
                registered.pop(dashboard.__class__)
            if len(registered):
                extra = registered.values()
                extra.sort()
                dashboards.extend(extra)
            return dashboards
        else:
            dashboards = self._registry.values()
            dashboards.sort()
            return dashboards

    def get_default_dashboard(self):
        """Returns the default :class:`~horizon.Dashboard` instance.

        If ``"default_dashboard"`` is specified in ``HORIZON_CONFIG``
        then that dashboard will be returned. If not, the first dashboard
        returned by :func:`~horizon.get_dashboards` will be returned.
        """
        if self.default_dashboard:
            return self._registered(self.default_dashboard)
        elif len(self._registry):
            return self.get_dashboards()[0]
        else:
            raise NotRegistered("No dashboard modules have been registered.")

    def get_user_home(self, user):
        """Returns the default URL for a particular user.

        This method can be used to customize where a user is sent when
        they log in, etc. By default it returns the value of
        :meth:`get_absolute_url`.

        An alternative function can be supplied to customize this behavior
        by specifying a either a URL or a function which returns a URL via
        the ``"user_home"`` key in ``HORIZON_CONFIG``. Each of these
        would be valid::

            {"user_home": "/home",}  # A URL
            {"user_home": "my_module.get_user_home",}  # Path to a function
            {"user_home": lambda user: "/" + user.name,}  # A function
            {"user_home": None,}  # Will always return the default dashboard

        This can be useful if the default dashboard may not be accessible
        to all users. When user_home is missing from HORIZON_CONFIG,
        it will default to the settings.LOGIN_REDIRECT_URL value.
        """
        user_home = self._conf['user_home']
        if user_home:
            if callable(user_home):
                return user_home(user)
            elif isinstance(user_home, basestring):
                # Assume we've got a URL if there's a slash in it
                if '/' in user_home:
                    return user_home
                else:
                    mod, func = user_home.rsplit(".", 1)
                    return getattr(import_module(mod), func)(user)
            # If it's not callable and not a string, it's wrong.
            raise ValueError('The user_home setting must be either a string '
                             'or a callable object (e.g. a function).')
        else:
            return self.get_absolute_url()

    def get_absolute_url(self):
        """Returns the default URL for Horizon's URLconf.

        The default URL is determined by calling
        :meth:`~horizon.Dashboard.get_absolute_url`
        on the :class:`~horizon.Dashboard` instance returned by
        :meth:`~horizon.get_default_dashboard`.
        """
        return self.get_default_dashboard().get_absolute_url()

    @property
    def _lazy_urls(self):
        """Lazy loading for URL patterns.

        This method avoids problems associated with attempting to evaluate
        the URLconf before the settings module has been loaded.
        """
        def url_patterns():
            return self._urls()[0]

        return LazyURLPattern(url_patterns), self.namespace, self.slug

    def _urls(self):
        """Constructs the URLconf for Horizon from registered Dashboards."""
        urlpatterns = self._get_default_urlpatterns()
        self._autodiscover()

        # Discover each dashboard's panels.
        for dash in self._registry.values():
            dash._autodiscover()

        # Load the plugin-based panel configuration
        self._load_panel_customization()

        # Allow for override modules
        if self._conf.get("customization_module", None):
            customization_module = self._conf["customization_module"]
            bits = customization_module.split('.')
            mod_name = bits.pop()
            package = '.'.join(bits)
            mod = import_module(package)
            try:
                before_import_registry = copy.copy(self._registry)
                import_module('%s.%s' % (package, mod_name))
            except Exception:
                self._registry = before_import_registry
                if module_has_submodule(mod, mod_name):
                    raise

        # Compile the dynamic urlconf.
        for dash in self._registry.values():
            urlpatterns += patterns('',
                                    url(r'^%s/' % dash.slug,
                                        include(dash._decorated_urls)))

        # Return the three arguments to django.conf.urls.include
        return urlpatterns, self.namespace, self.slug

    def _autodiscover(self):
        """Discovers modules to register from ``settings.INSTALLED_APPS``.

        This makes sure that the appropriate modules get imported to register
        themselves with Horizon.
        """
        if not getattr(self, '_registerable_class', None):
            raise ImproperlyConfigured('You must set a '
                                       '"_registerable_class" property '
                                       'in order to use autodiscovery.')
        # Discover both dashboards and panels, in that order
        for mod_name in ('dashboard', 'panel'):
            for app in settings.INSTALLED_APPS:
                mod = import_module(app)
                try:
                    before_import_registry = copy.copy(self._registry)
                    import_module('%s.%s' % (app, mod_name))
                except Exception:
                    self._registry = before_import_registry
                    if module_has_submodule(mod, mod_name):
                        raise

    def _load_panel_customization(self):
        """Applies the plugin-based panel configurations.

        This method parses the panel customization from the ``HORIZON_CONFIG``
        and make changes to the dashboard accordingly.

        It supports adding, removing and setting default panels on the
        dashboard. It also support registering a panel group.
        """
        panel_customization = self._conf.get("panel_customization", [])

        for config in panel_customization:
            if config.get('PANEL'):
                self._process_panel_configuration(config)
            elif config.get('PANEL_GROUP'):
                self._process_panel_group_configuration(config)
            else:
                LOG.warning("Skipping %s because it doesn't have PANEL or "
                            "PANEL_GROUP defined.", config.__name__)

    def _process_panel_configuration(self, config):
        """Add, remove and set default panels on the dashboard."""
        try:
            dashboard = config.get('PANEL_DASHBOARD')
            if not dashboard:
                LOG.warning("Skipping %s because it doesn't have "
                            "PANEL_DASHBOARD defined.", config.__name__)
                return
            panel_slug = config.get('PANEL')
            dashboard_cls = self.get_dashboard(dashboard)
            panel_group = config.get('PANEL_GROUP')
            default_panel = config.get('DEFAULT_PANEL')

            # Set the default panel
            if default_panel:
                dashboard_cls.default_panel = default_panel

            # Remove the panel
            if config.get('REMOVE_PANEL', False):
                for panel in dashboard_cls.get_panels():
                    if panel_slug == panel.slug:
                        dashboard_cls.unregister(panel.__class__)
            elif config.get('ADD_PANEL', None):
                # Add the panel to the dashboard
                panel_path = config['ADD_PANEL']
                mod_path, panel_cls = panel_path.rsplit(".", 1)
                try:
                    mod = import_module(mod_path)
                except ImportError:
                    LOG.warning("Could not load panel: %s", mod_path)
                    return
                panel = getattr(mod, panel_cls)
                dashboard_cls.register(panel)
                if panel_group:
                    dashboard_cls.get_panel_group(panel_group).__class__.\
                        panels.append(panel.slug)
                    dashboard_cls.get_panel_group(panel_group).\
                        panels.append(panel.slug)
                else:
                    panels = list(dashboard_cls.panels)
                    panels.append(panel)
                    dashboard_cls.panels = tuple(panels)
        except Exception as e:
            LOG.warning('Could not process panel %(panel)s: %(exc)s',
                        {'panel': panel_slug, 'exc': e})

    def _process_panel_group_configuration(self, config):
        """Adds a panel group to the dashboard."""
        panel_group_slug = config.get('PANEL_GROUP')
        try:
            dashboard = config.get('PANEL_GROUP_DASHBOARD')
            if not dashboard:
                LOG.warning("Skipping %s because it doesn't have "
                            "PANEL_GROUP_DASHBOARD defined.", config.__name__)
                return
            dashboard_cls = self.get_dashboard(dashboard)

            panel_group_name = config.get('PANEL_GROUP_NAME')
            if not panel_group_name:
                LOG.warning("Skipping %s because it doesn't have "
                            "PANEL_GROUP_NAME defined.", config.__name__)
                return
            # Create the panel group class
            panel_group = type(panel_group_slug,
                               (PanelGroup, ),
                               {'slug': panel_group_slug,
                                'name': panel_group_name,
                                'panels': []},)
            # Add the panel group to dashboard
            panels = list(dashboard_cls.panels)
            panels.append(panel_group)
            dashboard_cls.panels = tuple(panels)
            # Trigger the autodiscovery to completely load the new panel group
            dashboard_cls._autodiscover_complete = False
            dashboard_cls._autodiscover()
        except Exception as e:
            LOG.warning('Could not process panel group %(panel_group)s: '
                        '%(exc)s',
                        {'panel_group': panel_group_slug, 'exc': e})


class HorizonSite(Site):
    """A singleton implementation of Site such that all dealings with horizon
    get the same instance no matter what. There can be only one.
    """
    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(Site, cls).__new__(cls, *args, **kwargs)
        return cls._instance


# The one true Horizon
Horizon = HorizonSite()