From 06db4cd346261cdb4344018b9d512e9c5a6975a2 Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 24 Jul 2014 09:53:34 +0000 Subject: [PATCH] Refactor admin_fields --- TODO.md | 2 + orchestra/admin/decorators.py | 30 +++-- orchestra/admin/menu.py | 20 +-- orchestra/admin/utils.py | 115 ++++++------------ orchestra/apps/issues/admin.py | 27 +--- orchestra/apps/orchestration/admin.py | 4 +- orchestra/apps/payments/admin.py | 25 +++- orchestra/apps/payments/models.py | 9 +- orchestra/apps/resources/admin.py | 4 +- orchestra/conf/base_settings.py | 2 +- orchestra/static/orchestra/icons/Pack.png | Bin 2857 -> 2630 bytes orchestra/static/orchestra/icons/Pack.svg | 21 +--- .../static/orchestra/icons/contact_alt.png | Bin 0 -> 4286 bytes .../static/orchestra/icons/contact_book.png | Bin 0 -> 5771 bytes 14 files changed, 119 insertions(+), 140 deletions(-) create mode 100644 orchestra/static/orchestra/icons/contact_alt.png create mode 100644 orchestra/static/orchestra/icons/contact_book.png diff --git a/TODO.md b/TODO.md index a32ec73c..39218e4e 100644 --- a/TODO.md +++ b/TODO.md @@ -65,3 +65,5 @@ Remember that, as always with QuerySets, any subsequent chained methods which im * Be consistent with dates: name_on, created ? + +* backend logs with hal logo diff --git a/orchestra/admin/decorators.py b/orchestra/admin/decorators.py index 898da675..141fccf8 100644 --- a/orchestra/admin/decorators.py +++ b/orchestra/admin/decorators.py @@ -1,4 +1,4 @@ -from functools import wraps +from functools import wraps, partial from django.contrib import messages from django.contrib.admin import helpers @@ -7,6 +7,22 @@ from django.utils.decorators import available_attrs from django.utils.encoding import force_text +def admin_field(method): + def admin_field_wrapper(*args, **kwargs): + """ utility function for creating admin links """ + kwargs['field'] = args[0] if args else '' + kwargs['order'] = kwargs.get('order', kwargs['field']) + kwargs['popup'] = kwargs.get('popup', False) + kwargs['description'] = kwargs.get('description', + kwargs['field'].split('__')[-1].replace('_', ' ').capitalize()) + admin_method = partial(method, **kwargs) + admin_method.short_description = kwargs['description'] + admin_method.allow_tags = True + admin_method.admin_order_field = kwargs['order'] + return admin_method + return admin_field_wrapper + + def action_with_confirmation(action_name, extra_context={}, template='admin/orchestra/generic_confirmation.html'): """ @@ -14,7 +30,6 @@ def action_with_confirmation(action_name, extra_context={}, If custom template is provided the form must contain: """ - def decorator(func, extra_context=extra_context, template=template): @wraps(func, assigned=available_attrs(func)) def inner(modeladmin, request, queryset): @@ -23,16 +38,16 @@ def action_with_confirmation(action_name, extra_context={}, stay = func(modeladmin, request, queryset) if not stay: return - + opts = modeladmin.model._meta app_label = opts.app_label action_value = func.__name__ - + if len(queryset) == 1: objects_name = force_text(opts.verbose_name) else: objects_name = force_text(opts.verbose_name_plural) - + context = { "title": "Are you sure?", "content_message": "Are you sure you want to %s the selected %s?" % @@ -45,12 +60,11 @@ def action_with_confirmation(action_name, extra_context={}, "app_label": app_label, 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, } - + context.update(extra_context) - + # Display the confirmation page return TemplateResponse(request, template, context, current_app=modeladmin.admin_site.name) return inner return decorator - diff --git a/orchestra/admin/menu.py b/orchestra/admin/menu.py index c3e7b4f1..cb9c1927 100644 --- a/orchestra/admin/menu.py +++ b/orchestra/admin/menu.py @@ -32,7 +32,8 @@ def get_services(): for model, options in services.get().iteritems(): if options.get('menu', True): opts = model._meta - url = reverse('admin:%s_%s_changelist' % (opts.app_label, opts.model_name)) + url = reverse('admin:{}_{}_changelist'.format( + opts.app_label, opts.model_name)) name = capfirst(options.get('verbose_name_plural')) result.append(items.MenuItem(name, url)) return sorted(result, key=lambda i: i.title) @@ -40,24 +41,27 @@ def get_services(): def get_account_items(): childrens = [ - items.MenuItem(_("Accounts"), reverse('admin:accounts_account_changelist')) + items.MenuItem(_("Accounts"), + reverse('admin:accounts_account_changelist')) ] if isinstalled('orchestra.apps.contacts'): url = reverse('admin:contacts_contact_changelist') childrens.append(items.MenuItem(_("Contacts"), url)) if isinstalled('orchestra.apps.users'): url = reverse('admin:users_user_changelist') - users = [items.MenuItem(_("Users"), url)] - if isinstalled('rest_framework.authtoken'): - tokens = reverse('admin:authtoken_token_changelist') - users.append(items.MenuItem(_("Tokens"), tokens)) - childrens.append(items.MenuItem(_("Users"), url, children=users)) + childrens.append(items.MenuItem(_("Users"), url)) if isinstalled('orchestra.apps.prices'): url = reverse('admin:prices_pack_changelist') childrens.append(items.MenuItem(_("Packs"), url)) if isinstalled('orchestra.apps.orders'): url = reverse('admin:orders_order_changelist') childrens.append(items.MenuItem(_("Orders"), url)) + if isinstalled('orchestra.apps.bills'): + url = reverse('admin:bills_bill_changelist') + childrens.append(items.MenuItem(_("Bills"), url)) + if isinstalled('orchestra.apps.payments'): + url = reverse('admin:payments_transaction_changelist') + childrens.append(items.MenuItem(_("Transactions"), url)) if isinstalled('orchestra.apps.issues'): url = reverse('admin:issues_ticket_changelist') childrens.append(items.MenuItem(_("Tickets"), url)) @@ -92,7 +96,7 @@ def get_administration_items(): childrens.append(items.MenuItem(_("Miscellaneous"), url)) if isinstalled('orchestra.apps.issues'): url = reverse('admin:issues_queue_changelist') - childrens.append(items.MenuItem(_("Issue queues"), url)) + childrens.append(items.MenuItem(_("Ticket queues"), url)) if isinstalled('djcelery'): task = reverse('admin:djcelery_taskstate_changelist') periodic = reverse('admin:djcelery_periodictask_changelist') diff --git a/orchestra/admin/utils.py b/orchestra/admin/utils.py index 70a0c77d..2de55a03 100644 --- a/orchestra/admin/utils.py +++ b/orchestra/admin/utils.py @@ -12,6 +12,8 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.models.utils import get_field_value from orchestra.utils.humanize import naturaldate +from .decorators import admin_field + def get_modeladmin(model, import_module=True): """ returns the modeladmin registred for model """ @@ -44,7 +46,9 @@ def insertattr(model, name, value, weight=0): weights = {} if hasattr(modeladmin, 'weights') and name in modeladmin.weights: weights = modeladmin.weights.get(name) - inserted_attrs[name] = [ (attr, weights.get(attr, 0)) for attr in getattr(modeladmin, name) ] + inserted_attrs[name] = [ + (attr, weights.get(attr, 0)) for attr in getattr(modeladmin, name) + ] inserted_attrs[name].append((value, weight)) inserted_attrs[name].sort(key=lambda a: a[1]) @@ -70,85 +74,40 @@ def set_default_filter(queryarg, request, value): request.META['QUERY_STRING'] = request.GET.urlencode() +@admin_field def admin_link(*args, **kwargs): - """ utility function for creating admin links """ - field = args[0] if args else '' - order = kwargs.pop('order', field) - popup = kwargs.pop('popup', False) - - def display_link(*args): - instance = args[-1] - obj = getattr(instance, field, instance) - if not getattr(obj, 'pk', None): - return '---' - opts = obj._meta - view_name = 'admin:%s_%s_change' % (opts.app_label, opts.model_name) - url = reverse(view_name, args=(obj.pk,)) - extra = '' - if popup: - extra = 'onclick="return showAddAnotherPopup(this);"' - return '%s' % (url, extra, obj) - display_link.allow_tags = True - display_link.short_description = _(field.replace('_', ' ')) - display_link.admin_order_field = order - return display_link + instance = args[-1] + obj = get_field_value(instance, kwargs['field']) + if not getattr(obj, 'pk', None): + return '---' + opts = obj._meta + view_name = 'admin:%s_%s_change' % (opts.app_label, opts.model_name) + url = reverse(view_name, args=(obj.pk,)) + extra = '' + if kwargs['popup']: + extra = 'onclick="return showAddAnotherPopup(this);"' + return '%s' % (url, extra, obj) -def colored(field_name, colours, description='', verbose=False, bold=True): - """ returns a method that will render obj with colored html """ - def colored_field(obj, field=field_name, colors=colours, verbose=verbose): - value = escape(get_field_value(obj, field)) - color = colors.get(value, "black") - if verbose: - # Get the human-readable value of a choice field - value = getattr(obj, 'get_%s_display' % field)() - colored_value = '%s' % (color, value) - if bold: - colored_value = '%s' % colored_value - return mark_safe(colored_value) - if not description: - description = field_name.split('__').pop().replace('_', ' ').capitalize() - colored_field.short_description = description - colored_field.allow_tags = True - colored_field.admin_order_field = field_name - return colored_field +@admin_field +def admin_colored(*args, **kwargs): + instance = args[-1] + field = kwargs['field'] + value = escape(get_field_value(instance, field)) + color = kwargs.get('colors', {}).get(value, 'black') + value = getattr(instance, 'get_%s_display' % field)().upper() + colored_value = '%s' % (color, value) + if kwargs.get('bold', True): + colored_value = '%s' % colored_value + return mark_safe(colored_value) -#def display_timesince(date, double=False): -# """ -# Format date for messages create_on: show a relative time -# with contextual helper to show fulltime format. -# """ -# if not date: -# return 'Never' -# date_rel = timesince(date) -# if not double: -# date_rel = date_rel.split(',')[0] -# date_rel += ' ago' -# date_abs = date.strftime("%Y-%m-%d %H:%M:%S %Z") -# return mark_safe("%s" % (date_abs, date_rel)) - - -def admin_date(field, **kwargs): - """ utility function for creating admin dates """ - default = kwargs.pop('default', '') - order = kwargs.pop('order', field) - - def display_date(*args): - instance = args[-1] - value = get_field_value(instance, field) - if not value: - return default - return '{1}'.format( - escape(str(value)), escape(naturaldate(value)), - ) - display_date.short_description = _(field.replace('_', ' ')) - display_date.admin_order_field = order - display_date.allow_tags = True - return display_date - - -#def display_timeuntil(date): -# date_rel = timeuntil(date) + ' left' -# date_abs = date.strftime("%Y-%m-%d %H:%M:%S %Z") -# return mark_safe("%s" % (date_abs, date_rel)) +@admin_field +def admin_date(*args, **kwargs): + instance = args[-1] + value = get_field_value(instance, kwargs['field']) + if not value: + return kwargs.get('default', '') + return '{1}'.format( + escape(str(value)), escape(naturaldate(value)), + ) diff --git a/orchestra/apps/issues/admin.py b/orchestra/apps/issues/admin.py index d0c43ea8..0f4896ed 100644 --- a/orchestra/apps/issues/admin.py +++ b/orchestra/apps/issues/admin.py @@ -12,7 +12,8 @@ from django.utils.translation import ugettext_lazy as _ from markdown import markdown from orchestra.admin import ChangeListDefaultFilter, ExtendedModelAdmin#, ChangeViewActions -from orchestra.admin.utils import admin_link, colored, wrap_admin_view, admin_date +from orchestra.admin.utils import (admin_link, admin_colored, wrap_admin_view, + admin_date) from orchestra.apps.contacts import settings as contacts_settings from .actions import (reject_tickets, resolve_tickets, take_tickets, close_tickets, @@ -111,19 +112,13 @@ class TicketInline(admin.TabularInline): owner_link = admin_link('owner') created = admin_link('created_on') last_modified = admin_link('last_modified_on') + colored_state = admin_colored('state', colors=STATE_COLORS, bold=False) + colored_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False) def ticket_id(self, instance): return '%s' % admin_link()(instance) ticket_id.short_description = '#' ticket_id.allow_tags = True - - def colored_state(self, instance): - return colored('state', STATE_COLORS, bold=False)(instance) - colored_state.short_description = _("State") - - def colored_priority(self, instance): - return colored('priority', PRIORITY_COLORS, bold=False)(instance) - colored_priority.short_description = _("Priority") class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeViewActions, @@ -198,6 +193,8 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView display_queue = admin_link('queue') display_owner = admin_link('owner') last_modified = admin_date('last_modified_on') + display_state = admin_colored('state', colors=STATE_COLORS, bold=False) + display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False) def display_summary(self, ticket): context = { @@ -216,18 +213,6 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView display_summary.short_description = 'Summary' display_summary.allow_tags = True - def display_priority(self, ticket): - """ State colored for change_form """ - return colored('priority', PRIORITY_COLORS, bold=False, verbose=True)(ticket) - display_priority.short_description = _("Priority") - display_priority.admin_order_field = 'priority' - - def display_state(self, ticket): - """ State colored for change_form """ - return colored('state', STATE_COLORS, bold=False, verbose=True)(ticket) - display_state.short_description = _("State") - display_state.admin_order_field = 'state' - def unbold_id(self, ticket): """ Unbold id if ticket is read """ if ticket.is_read_by(self.user): diff --git a/orchestra/apps/orchestration/admin.py b/orchestra/apps/orchestration/admin.py index 6cdd0059..cff38be3 100644 --- a/orchestra/apps/orchestration/admin.py +++ b/orchestra/apps/orchestration/admin.py @@ -4,7 +4,7 @@ from django.utils.html import escape from django.utils.translation import ugettext_lazy as _ from orchestra.admin.html import monospace_format -from orchestra.admin.utils import admin_link, admin_date, colored +from orchestra.admin.utils import admin_link, admin_date, admin_colored from .models import Server, Route, BackendLog, BackendOperation @@ -90,7 +90,7 @@ class BackendLogAdmin(admin.ModelAdmin): server_link = admin_link('server') display_last_update = admin_date('last_update') display_created = admin_date('created') - display_state = colored('state', STATE_COLORS) + display_state = admin_colored('state', colors=STATE_COLORS) def mono_script(self, log): return monospace_format(escape(log.script)) diff --git a/orchestra/apps/payments/admin.py b/orchestra/apps/payments/admin.py index 4c150b72..6a7122a5 100644 --- a/orchestra/apps/payments/admin.py +++ b/orchestra/apps/payments/admin.py @@ -1,7 +1,30 @@ from django.contrib import admin +from orchestra.admin.utils import admin_colored, admin_link + from .models import PaymentSource, Transaction +STATE_COLORS = { + Transaction.WAITTING_PROCESSING: 'darkorange', + Transaction.WAITTING_CONFIRMATION: 'orange', + Transaction.CONFIRMED: 'green', + Transaction.REJECTED: 'red', + Transaction.LOCKED: 'magenta', + Transaction.DISCARTED: 'blue', +} + + +class TransactionAdmin(admin.ModelAdmin): + list_display = ( + 'id', 'bill_link', 'account_link', 'method', 'display_state', 'amount' + ) + list_filter = ('method', 'state') + + bill_link = admin_link('bill') + account_link = admin_link('bill__account') + display_state = admin_colored('state', colors=STATE_COLORS) + + admin.site.register(PaymentSource) -admin.site.register(Transaction) +admin.site.register(Transaction, TransactionAdmin) diff --git a/orchestra/apps/payments/models.py b/orchestra/apps/payments/models.py index a2f7a79c..b225f6c5 100644 --- a/orchestra/apps/payments/models.py +++ b/orchestra/apps/payments/models.py @@ -12,6 +12,7 @@ class PaymentSource(models.Model): method = models.CharField(_("method"), max_length=32, choices=PaymentMethod.get_plugin_choices()) data = JSONField(_("data")) + is_active = models.BooleanField(_("is active"), default=True) class Transaction(models.Model): @@ -22,14 +23,15 @@ class Transaction(models.Model): LOCKED = 'LOCKED' DISCARTED = 'DISCARTED' STATES = ( - (WAITTING_PROCESSING, _("Waitting for processing")), - (WAITTING_CONFIRMATION, _("Waitting for confirmation")), + (WAITTING_PROCESSING, _("Waitting processing")), + (WAITTING_CONFIRMATION, _("Waitting confirmation")), (CONFIRMED, _("Confirmed")), (REJECTED, _("Rejected")), (LOCKED, _("Locked")), (DISCARTED, _("Discarted")), ) + # TODO account fk? bill = models.ForeignKey('bills.bill', verbose_name=_("bill"), related_name='transactions') method = models.CharField(_("payment method"), max_length=32, @@ -42,3 +44,6 @@ class Transaction(models.Model): created_on = models.DateTimeField(auto_now_add=True) modified_on = models.DateTimeField(auto_now=True) related = models.ForeignKey('self', null=True, blank=True) + + def __unicode__(self): + return "Transaction {}".format(self.id) diff --git a/orchestra/apps/resources/admin.py b/orchestra/apps/resources/admin.py index eefb22e6..023dccda 100644 --- a/orchestra/apps/resources/admin.py +++ b/orchestra/apps/resources/admin.py @@ -15,8 +15,8 @@ from .models import Resource, ResourceData, MonitorData class ResourceAdmin(ExtendedModelAdmin): list_display = ( - 'id', 'name', 'verbose_name', 'content_type', 'period', 'ondemand', - 'default_allocation', 'disable_trigger', 'crontab', + 'id', 'verbose_name', 'content_type', 'period', 'ondemand', + 'default_allocation', 'unit', 'disable_trigger', 'crontab', ) list_filter = (UsedContentTypeFilter, 'period', 'ondemand', 'disable_trigger') fieldsets = ( diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py index 213deac4..57b1f41f 100644 --- a/orchestra/conf/base_settings.py +++ b/orchestra/conf/base_settings.py @@ -179,7 +179,7 @@ FLUENT_DASHBOARD_APP_ICONS = { 'miscellaneous/miscellaneous': 'applications-other.png', # Accounts 'accounts/account': 'Face-monkey.png', - 'contacts/contact': 'contact.png', + 'contacts/contact': 'contact_book.png', 'orders/order': 'basket.png', 'orders/service': 'price.png', 'prices/pack': 'Pack.png', diff --git a/orchestra/static/orchestra/icons/Pack.png b/orchestra/static/orchestra/icons/Pack.png index 24d48ced937fd2e60c69e7c55a5eb5b4b0965790..e7d2a8d4cbf073ce84cd4f4bec75a1786cc86775 100644 GIT binary patch delta 2547 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 JPDHLkV1k7O-Xs73 delta 2776 zcmV;}3Mci(6sZ=FZGQ@;NklDuDCVbGu7&T%{)PxWCih&gc#6S#!AVfsL-9<&vU6{@6 z3^Tj)p8M+CU8nreckXNMJnpP$Jd=~2+jXkyoI3w=>QuF|)_?N`X)FLUZ;(bR74{+m+vaC!y2tD@8ue~4C4;;s-fWlhakpW6;px(ub z>7+YKVXZ}p6~|h8U%5AY_Z3&%7`KyM;(>eb{jQ&jqVM(e_M{!xNw=M0m9Wu{2dZv> zm0lwba)?`4* z+H1Nz*NU+#kJ!@IDk-+XwHCw(V%E1H0G=NNrTqN$v@xIwLe|9Bdwx1-50gfg71>&a zwK`!m#>m>?*oKvM5fk^`{hg0q(rQ4>19yMxeV5>*dbP7Ud{&D;i)vaM5y2Q)bKtfU z^!#*JoPVFb4)}B9`UDhg48l@FV=>*G4qZBBWD79Mk;iWQYFIAt!&!Ti? ziCklxSZp=bCQq!ziXDpi{n!=K+|61#H#xYuypB7B7{;al5LDK{!8#iOOpoVOerL--`B{R-tBq zwH6UUWKq=?oU8zwC0R%;ssU%s6fmNwP(omFko#Dt1yQhg({vVW|rSEpwG`I)e#$wro69p-U`2?Cnq=z`vKA4;no9pQ;AuE~mIvg1 zF4}h~#)+7UM^^7tB}2qqdcYluf zN4aS4eu^@K>F_+TNCv}fe2PtjeFW)%JO6qpd$9aQuTiF6vRI2Wz-YZZUK!^J=W+Jz zJ&(LhVOXE?4lX^;R<`>Wlhs@Bx?P6$4(VT{4mH(74Oz&nIR=(Hnv`vsd*Zt}|Dp>h z$o#T$t5Lr@#O1k<;i>>%XE4MZI)8mVeovtG>>j%G>K_GP{KYk;uR)UslHE=k*et_K zR^BB1yV8A}J9I9gF05PmK6e)fdOkzW9VG4aV65S(;ypx^2yL0ZfxdKKci#94)Zo73Ds^NnV2Dsa+T z4{jVh`1YIqz9tB*9H3@Jp7Z8$y7ZcrS1o`0vmc<>-B~O6ie`=n=DtNl8HH}*ip1;~ z*wU*Vbya;QmSi7L>&T*$a(|lMExp84PNQRQaRMpZROuC9nby5r)_pBqx*Gr? z;$-D0$I1^U=aDuBD+LuDk_&paHEd)?SrNpnEe0CxnN>4v_BNqas((qQ-Q=xp2R3|~ zwC+vDw8C^e!h>@+ky7clj$G_#u>`3uS0@&!lrf@Palc#|H7)qhvp_24qg(!3ajsBJ zZRZ7VAm?tW9yw!XE2I2q{QZoSj&P#%IJ40h7K>6EtsJy+s;5Mav6gfYxQiah7%|F- zp|eZGgkmScQ7(2FsDHHcL|XUqlHiSK;~~~ilxco7^F{LJR5GTmFjX05CK@5<4zMw` z4NrB|`iD}$a~x;UfWQb^#Il|!?y><<6rx*hA{7c(`E2tqB$5)Nl_!D3{g-7vM9Ow! zu^1wrDc#R-`LQKqi#5zfqs&EPWZX^kdfQRT!D1+tqsUcCIe$Q4#V8SJ#@&iLQoS*4 zGK5iypesPDdWEiWIk@5TQjjA{JhZb4Di_VPfr7n zYLZK!$XYAXawpjw4=clHXX!-@KRd!^XGhayM+QxCYzL)il5`rg_}E8 zUMq*Q>;O-XJooG!*Om_fxYk;$lqx+xHGgYgEC-Zo0BMJmCdYB_*sfiME zQ_fp%%KY-xFTePLX?INJ&HR}Rmney5ySIa9|9NWWZ%>YW=(k@E@2Isc-k>R^a{PzB z8xOtJ$({d>;LiPfU%EHS#wbZ)&HXi+vz)W=!i*e#=AWa}Gx@83eSPH# z+j2eE_B-*JQgtCRK=$%4cyHS>6nyDryLb0*=nMP>nPJY%5wmpXRX|2($*B#b;x3*( zHahd%$iyEG|2X=}^M8>spa{%cTi>?-<9`OI_ifs^ zeM?_99e9p*70LyKiHZEb{&rh*%h+G!IFJVl)n~rZh!@HOe$|Lf z3#T`1SN*#VIsyQS@Ff^q7elkZ&9m? zzaR!Q7~)kG531}}*{iZkJ#k2tW0Fhs3J?Nipj0)YWLp$|K@Dg})$)C@h}6_mw=}S! eTHdTS-oF9Sr8vc2#*;1p0000 - - - + transform="matrix(0.67441404,0,0,0.67441404,-1.7640493,12.799498)"> diff --git a/orchestra/static/orchestra/icons/contact_alt.png b/orchestra/static/orchestra/icons/contact_alt.png new file mode 100644 index 0000000000000000000000000000000000000000..ff1239480e7392577f9f361d22c707e524f77494 GIT binary patch literal 4286 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 literal 0 HcmV?d00001 diff --git a/orchestra/static/orchestra/icons/contact_book.png b/orchestra/static/orchestra/icons/contact_book.png new file mode 100644 index 0000000000000000000000000000000000000000..7d3dce3d8dd76ed78e5f5e89ef26c3f9244f35a7 GIT binary patch literal 5771 zcmV;67If)}P)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000ZNNklS+ z)kj|p@y&+`KKSOu7^Bgq5Bgw?5wutniY$}!I5)r(UAIdxY@s0Lpu0DJx@Rz{W z?$-}3D(^lpkVMw6#jd}CuzG7n&9%7@=_0HqXEl~X5Q z1ldCl0Q%5Zj`6Qw`mNWz@!Pjt`IZ4$Y#wB5|GrE10wDkT$3Mai*S?G5MiL=_KmPN- zCIbH8kN%Y6ped8`j6ji;u@KOVhp<>I7^|VG62*a(= zDSZ2X9_Md9{bvXF0ziIn{8>(&_#OabF@E#TPp#GEMfD1mTs@E1PLRMD|1>%nFd`hmo98)5A#? zyH4r*9uFmDBhMD36qLb8xkI#Lu!6hLbsc3W2m)q^1Rjo+kuC|_ukNrh&$tz`IKwa^ z;h2roaYSeP@?Ce`6~Kc>j~@NV8!lwaVPq^uPMKmx>;j-+)Nw$glGL%rue^efJ>0-ax61iE zXTEST36dx&DekeRkO#OK?oeW+n{{|~Odt`IvBw-{vl&sHjNn$O)yPTc6sj9TZ*0sG z5=N~R#Ys4Uk#ZtQFz*t+R{#)bl65p9d>sL^O4fvjF$_-D%;}xe%yP$AD=8(s2Fug6 z0d#L0GS3L$wK7Xezg%KgNdiKU6wE6#72KVE7~ez$qS>mk8X|DdKYecf`Z$iLCPWo; zkXeu=5GqNOEQ;5HU}!+8Mg)w5;cg@q`sxr)7NJfcsyX4#spW{dArYK>`4x^FIRfy~ z#YG^3u~Y(3ggb8kIA8qQ(?rmu(2ej`sN94yR=ULkcVoLB*tap~8;^a52fp&(9RI;f zlxkde_2vBRdwzmDZv7xuot%WRRI)Y@ohsptYJ$*P<-mb0e*f;fIdbI4p^E|#fJe|J zovg4=wX@|wv%f9W<5-9g`k_ZekqEx|#IxM{g@^gccU{JNuDJnC!mHZ@Pd|5@d%xh^ zcFRqi+8Nncbd=%<(3gTtbRwMaAlP#bKoX^zlT^U6+V(%2-&; zXB>O%JKX%Ml;cn}wA+5Hk3NN1Arb|LC1y!LR#tDR?KvHFA81dIpp$kSp0E-}GQlmHnAgPd& zAO!tTDRsK6wn)^<6_;It)r!@MTlOef{U9Ur^pOazirYNLl0hiJ*^?&PEhaOGv2Y|Q6v z;k9CBxV7T+zUvP&l!AvhnpzMZ==2?Rftovn8n+wH8V(9J=&C^F^w7ZJ!YpR3?*XPL~oyW5J#Qs0dw3hzLHF_!WSd zDqXF{SSrh5WGI#DM$QTX7S2=8{SYg)c_C}pGsjP`volbtot402nJ8#ZKNd=Dw0m(Qe}yFK*-lFkg8V$yVSNz!o}A0~&$#j0 zOPUN@U5^xH+Fe1|+EBI@8!SuVKfnGY_y5P^y#L*Y`Q(RhVl0V`c{eFgoYzX|g{?%i z0!EOMo|VM2E>BSs9{Tz-JoWAG@>7>*u0J$qbKVSIF(+aIAJHi3t)X{s9%q)|+RJ9V z|LO&=y!;~fJ@hDDP6VqFB5$Ihk!ERCgyk^u*)Kk_&VjL1a!!~zof?;KD&T}Shr05R z79hHcfUp|ht|GCj&H!5rVWF$xfdaAKguTfqP^UzlcbVIN_2#owsG4J|Nrg8ZcMq!B z^xWHSn@?0|U74@mbH~hJ7ggL15usFL7#kShtDO#lF6T2NazE1(M+DWJE-N>_?>gRl z-Bl!2%$xPrsd%hhs7QNW&6(xY#6e}9YpeTIn)(DmQfU&tX9`WcB|w$4dg8iP3)r_< zu$a%9d-e8uwPr2mUB=AOq=W}H$*x(evMDPuwVaDtrdFqzwL&JsQ0g0_kcgaht#-Op zn|zM*&tH0Uy{paRIHk5qY&Pu}ZI{iAoLV!4G%5zMo^GhhIuBOWzSK5H%Bj_Zq{1vu zl6sEy>Fe|fX~k%?U>ooKx7vNDu9jUcDyJeX+Fs8P+E^$ylR%pLa? zkRYWtM9`@xn7Ay{n)E_Ryt;9*DrB;H!r3{n8u;dlcy&w{Bf-uQ74dY;{%0S5qECD_Wet#e?)G@GS0MSME*svhLrs^7e8 zdcFcy>J$}~HHC&!r|%F%&N$rl7^YMlIX$gu#;)TIJ;(0rwEKsE;@&j8%ZZ#5_kH>C zHHAckajYa&lF0htZr&w!`hwEbqp3>caI5A`7|fcKrlcT3DYllxXo;srGz^P*gr)ez0pE8ny;Gs<~C#3foe)ZlGawLC^@H=n7}Yr zYIRZ)s(Y&qN!VC)Em2h{)~02O>X@XBmH~>4-HSRv` zI9CMNwbN;r?!H~WtMFYwY*%EOyFCv8evB%@+xv)fJ&y?fe*ogZE*;tp8W#Wn002ov JPDHLkV1f`R{cZpN literal 0 HcmV?d00001