Эх сурвалжийг харах

HUE-2033 [core] Dynamic and robust home page

Pass #1 with koification and simplification.
Creating a temporary home2 page.
Display mocked data. Can add a tag.
Romain Rigaux 11 жил өмнө
parent
commit
ed85ae4

+ 76 - 0
desktop/core/src/desktop/api.py

@@ -20,6 +20,7 @@ import logging
 import json
 import time
 
+from collections import defaultdict
 
 from django.http import HttpResponse
 from django.core.urlresolvers import reverse
@@ -50,6 +51,81 @@ def list_tags(request):
   tags = list(set([tag for doc in docs for tag in doc.tags.all()] + [tag for tag in DocumentTag.objects.get_tags(user=request.user)])) # List of all personal and share tags
   return HttpResponse(json.dumps(massaged_tags_for_json(tags, request.user)), mimetype="application/json")
 
+def massaged_tags_for_json2(tags, user):
+  ts = {
+    'trash': [],
+    'history': [],
+    'mine': [],
+    'notmine': [],
+  }
+  
+  ts['trash'].append(massaged_tags(DocumentTag.objects.get_trash_tag(user)))
+  ts['history'].append(massaged_tags(DocumentTag.objects.get_history_tag(user)))
+
+  for tag in tags:
+    massaged_tag = massaged_tags(tag)
+    if tag.owner == user:
+      ts['mine'].append(massaged_tag)
+    else:
+      ts['notmine'].append(massaged_tag)
+
+  return ts
+
+def massaged_tags(tag):
+  return {
+    'id': tag.id,
+    'name': tag.tag,
+    'owner': tag.owner.username,
+  }
+
+def massaged_documents_for_json2(documents, user):
+  docs = {'mydocs': defaultdict(list), 'shared_docs': defaultdict(list),}
+  tags = {}
+  
+  trash_tag = DocumentTag.objects.get_trash_tag(user)
+  history_tag = DocumentTag.objects.get_history_tag(user)
+  
+  tags[trash_tag.id] = {'owner': trash_tag.owner.username, 'owner_id': trash_tag.owner.id, 'name': trash_tag.tag}
+  tags[history_tag.id] = {'owner': history_tag.owner.username, 'owner_id': history_tag.owner.id, 'name': history_tag.tag}
+  
+  for doc in documents:
+    if doc.is_trashed():
+      docs['mydocs'][trash_tag.id].append(massage_doc_for_json2(doc))
+    elif doc.is_historic():
+      docs['mydocs'][history_tag.id].append(massage_doc_for_json2(doc))   
+    elif doc.owner.username == user.username: 
+      for tag in doc.tags.all():
+        docs['mydocs'][tag.id].append(massage_doc_for_json2(doc))
+        tags[tag.id] = {'owner': tag.owner.username, 'owner_id': tag.owner.id, 'name': tag.tag} 
+    else:
+      for tag in doc.tags.all():
+        docs['shared_docs'][tag.id].append(massage_doc_for_json2(doc)) 
+        tags[tag.id] = {'owner': tag.owner.username, 'owner_id': tag.owner.id, 'name': tag.tag}
+      
+  return {'docs': docs, 'tags': tags}
+
+
+def massage_doc_for_json2(doc):
+  perms = doc.list_permissions()
+  
+  return {
+      'id': doc.id,
+      'contentType': doc.content_type.name,
+      'icon': doc.icon,
+      'name': doc.name,
+      'url': doc.content_object.get_absolute_url(),
+      'description': doc.description,
+      'tags': [{'id': tag.id, 'name': tag.tag} for tag in doc.tags.all()],
+      'perms': {
+#        'read': {
+#          'users': [{'id': user.id, 'username': user.username} for user in perms.users.all()],
+#          'groups': [{'id': group.id, 'name': group.name} for group in perms.groups.all()]
+#        }
+      },
+      'owner': doc.owner.username,
+      'lastModified': doc.last_modified.strftime("%x %X"),
+      'lastModifiedInMillis': time.mktime(doc.last_modified.timetuple())
+    }
 
 def massaged_documents_for_json(documents, user):
   return [massage_doc_for_json(doc, user) for doc in documents]

+ 7 - 7
desktop/core/src/desktop/models.py

@@ -384,8 +384,8 @@ class Document(models.Model):
   def add_to_history(self):
     tag = DocumentTag.objects.get_history_tag(user=self.owner)
     self.tags.add(tag)
-    default_tag = DocumentTag.objects.get_default_tag(user=self.owner)
-    self.tags.remove(default_tag)
+    #default_tag = DocumentTag.objects.get_default_tag(user=self.owner)
+    #self.tags.remove(default_tag)
 
   def share_to_default(self):
     DocumentPermission.objects.share_to_default(self)
@@ -411,7 +411,7 @@ class Document(models.Model):
   def copy(self, name=None, owner=None):
     copy_doc = self
 
-    tags = self.tags.all()
+    tags = self.tags.all() # Don't copy tags
 
     copy_doc.pk = None
     copy_doc.id = None
@@ -421,10 +421,10 @@ class Document(models.Model):
       copy_doc.owner = owner
     copy_doc.save()
 
-    tags = filter(lambda tag: tag.tag != DocumentTag.EXAMPLE, tags)
-    if not tags:
-      default_tag = DocumentTag.objects.get_default_tag(copy_doc.owner)
-      tags = [default_tag]
+    #tags = filter(lambda tag: tag.tag != DocumentTag.EXAMPLE, tags)
+    #if not tags:
+    default_tag = DocumentTag.objects.get_default_tag(copy_doc.owner)
+    tags = [default_tag]
     copy_doc.tags.add(*tags)
 
     return copy_doc

+ 7 - 2
desktop/core/src/desktop/templates/home.mako

@@ -472,7 +472,9 @@ $(document).ready(function () {
     var _tags = "";
     for (var i = 0; i < JSON_TAGS.length; i++) {
       if (!JSON_TAGS[i].isTrash && !JSON_TAGS[i].isHistory && !JSON_TAGS[i].isExample) {
-        _tags += '<div style="margin-right:10px;margin-bottom: 6px;float:left;"><span class="tags-modal-checkbox badge" data-value="' + JSON_TAGS[i].id + '"><i class="fa fa-trash-o hide"></i> ' + JSON_TAGS[i].name + '</span></div>';
+        _tags += '<div style="margin-right:10px;margin-bottom: 6px;float:left;">' + 
+            '<span class="tags-modal-checkbox badge" data-value="' + JSON_TAGS[i].id + '" data-ismine="' + JSON_TAGS[i].isMine + '">' +
+            '<i class="fa fa-trash-o hide"></i> ' + JSON_TAGS[i].name + '</span></div>';
       }
     }
     $("#tagsModalList").html(_tags);
@@ -487,7 +489,10 @@ $(document).ready(function () {
       for (var i = 0; i < JSON_TAGS.length; i++) {
         if (!JSON_TAGS[i].isTrash && !JSON_TAGS[i].isHistory && !JSON_TAGS[i].isExample) {
           var _inTags = isInTags(_doc, JSON_TAGS[i].name);
-          _tags += '<div style="margin-right:10px;margin-bottom: 6px;float:left;"><span class="document-tags-modal-checkbox badge' + (_inTags ? ' badge-info selected' : '') + '" data-value="' + JSON_TAGS[i].id + '"><i class="fa fa-check-circle' + (_inTags ? '' : ' hide') + '"></i> ' + JSON_TAGS[i].name + '</span></div>';
+          _tags += '<div style="margin-right:10px;margin-bottom: 6px;float:left;"><span class="document-tags-modal-checkbox badge' +
+              (_inTags ? ' badge-info selected' : '') + '" data-value="' + JSON_TAGS[i].id +
+              '" data-ismine="' + JSON_TAGS[i].isMine + '">' +
+              '<i class="fa fa-check-circle' + (_inTags ? '' : ' hide') + '"></i> ' + JSON_TAGS[i].name + '</span></div>';
         }
       }
       $("#documentTagsModalList").html(_tags);

+ 271 - 0
desktop/core/src/desktop/templates/home2.mako

@@ -0,0 +1,271 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. licenses this file
+## to you 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.
+<%!
+  from desktop.views import commonheader, commonfooter
+  from django.utils.translation import ugettext as _
+%>
+
+${ commonheader(_('Welcome Home'), "home", user) | n,unicode }
+
+<style type="text/css">
+  .sidebar-nav img {
+    margin-right: 6px;
+  }
+
+  .sidebar-nav .dropdown-menu a {
+    padding-left: 6px;
+  }
+
+  .tag {
+    float: left;
+    margin-right: 6px;
+    margin-bottom: 4px;
+  }
+
+  #trashCounter, #historyCounter {
+    margin-top: 3px;
+  }
+
+  .tag-counter {
+    margin-top: 2px;
+  }
+
+  .toggle-tag, .document-tags-modal-checkbox, .tags-modal-checkbox {
+    cursor: pointer;
+  }
+
+  .badge-left {
+    border-radius: 9px 0px 0px 9px;
+    padding-right: 5px;
+  }
+
+  .badge-right {
+    border-radius: 0px 9px 9px 0px;
+    padding-left: 5px;
+  }
+
+  .airy li {
+    margin-bottom: 6px;
+  }
+
+  .trash-share {
+    cursor: pointer;
+  }
+
+</style>
+
+<div class="navbar navbar-inverse navbar-fixed-top nokids">
+  <div class="navbar-inner">
+    <div class="container-fluid">
+      <div class="nav-collapse">
+        <ul class="nav">
+          <li class="currentApp">
+            <a href="${ url('desktop.views.home') }">
+              <img src="/static/art/home.png" />
+              ${ _('My documents') }
+            </a>
+           </li>
+        </ul>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div class="container-fluid">
+  <div class="row-fluid">
+    <div class="span2">
+      <div class="sidebar-nav">
+         <ul class="nav nav-list">
+          <li class="nav-header">${_('Actions')}</li>
+           <li class="dropdown">
+              <a href="#" data-toggle="dropdown"><i class="fa fa-plus-circle"></i> ${_('New document')}</a>
+              <ul class="dropdown-menu" role="menu">
+                % if 'beeswax' in apps:
+                <li><a href="${ url('beeswax:index') }"><img src="${ apps['beeswax'].icon_path }"/> ${_('Hive Query')}</a></li>
+                % endif
+                % if 'impala' in apps:
+                <li><a href="${ url('impala:index') }"><img src="${ apps['impala'].icon_path }"/> ${_('Impala Query')}</a></li>
+                % endif
+                % if 'pig' in apps:
+                <li><a href="${ url('beeswax:index') }"><img src="${ apps['pig'].icon_path }"/> ${_('Pig Script')}</a></li>
+                % endif
+                % if 'spark' in apps:
+                <li><a href="${ url('spark:index') }"><img src="${ apps['spark'].icon_path }"/> ${_('Spark Job')}</a></li>
+                % endif
+                % if 'oozie' in apps:
+                <li class="dropdown-submenu">
+                  <a href="#"><img src="${ apps['oozie'].icon_path }"/> ${_('Oozie Scheduler')}</a>
+                  <ul class="dropdown-menu">
+                    <li><a href="${ url('oozie:create_workflow') }"><img src="/oozie/static/art/icon_oozie_workflow_24.png"/> ${_('Workflow')}</a></li>
+                    <li><a href="${ url('oozie:create_coordinator') }"><img src="/oozie/static/art/icon_oozie_coordinator_24.png"/> ${_('Coordinator')}</a></li>
+                    <li><a href="${ url('oozie:create_bundle') }"><img src="/oozie/static/art/icon_oozie_bundle_24.png"/> ${_('Bundle')}</a></li>
+                  </ul>
+                </li>
+                % endif
+              </ul>
+           </li>
+           <div data-bind="template: { name: 'tag-template', data: trash }"></div>
+           <div data-bind="template: { name: 'tag-template', data: history }"></div>
+           <li class="nav-header tag-mine-header">
+             ${_('My Projects')}
+             <div class="edit-tags" style="display: inline;cursor: pointer;margin-left: 6px" title="${ _('Edit projects') }">
+               <i class="fa fa-pencil" data-bind="click: editTags"></i>
+             </div>
+           </li>
+           <div data-bind="template: { name: 'tag-template', foreach: myTags }"></div>
+           <li data-bind="visible: myTags().length == 0">
+             <a href="javascript:void(0)" class="edit-tags" style="line-height:24px">
+               <i class="fa fa-plus-circle"></i> ${_('You currently own no projects. Click here to add one now!')}
+             </a>
+           </li>
+          <li class="nav-header tag-shared-header">
+            ${_('Shared with me')}
+          </li>
+          <div data-bind="template: { name: 'tag-template', foreach: sharedTags }"></div>
+          <li data-bind="visible: sharedTags().length == 0">
+            <a href="javascript:void(0)" style="line-height:24px"><i class="fa fa-plus-circle"></i> ${_('There are currently no projects shared with you.')}
+            </a>
+          </li>
+        </ul>
+      </div>
+
+    </div>
+
+    <div class="span10">
+      <div class="card card-home" style="margin-top: 0">
+        <input type="text" placeholder="Search for name, description, etc..." class="input-xlarge search-query pull-right" style="margin-right: 10px;margin-top: 3px" id="filterInput">
+        <h2 class="card-heading simple">${_('My Documents')}</h2>
+
+        <div class="card-body">
+          <p>
+          <table id="datatables" class="table table-striped table-condensed datatables" data-tablescroller-disable="true">
+            <thead>
+              <tr>
+                <th>&nbsp;</th>
+                <th>${_('Name')}</th>
+                <th>${_('Description')}</th>
+                <th>${_('Projects')}</th>
+                <th>${_('Owner')}</th>
+                <th>${_('Last Modified')}</th>
+                <th>${_('Sharing')}</th>
+              </tr>
+            </thead>
+            <tbody data-bind="template: { name: 'document-template', foreach: documents }">
+            </tbody>
+          </table>
+          </p>
+        </div>
+      </div>
+    </div>
+
+  </div>
+</div>
+
+
+<script type="text/html" id="tag-template">
+  <li class="toggle-tag" data-bind="click: $root.filterDocs">
+    <a href="javascript:void(0)">
+      <i class="fa fa-trash-o"></i> <span data-bind="text: name"></span> <span class="badge pull-right" data-bind="text: docs().length"></span>
+    </a>
+  </li>
+</script>
+
+<script type="text/html" id="document-template">
+  <tr>
+    <td><img data-bind="attr: { src: icon }" width="80%"></td>
+    <td><a data-bind="attr: { href: url }, text: name"></a></td>
+    <td></td>
+    <td>
+      <div class="documentTags">
+        <span class="badge">history</span>
+      </div>
+    </td>
+    <td data-bind="text: owner"></td>
+    <td data-bind="text: lastModified"></td>
+    <td>
+      <a rel="tooltip" data-placement="left" style="padding-left:10px" data-original-title="${ _("Share My saved query") }">
+        <i data-bind="visible: isMine" class="fa fa-share-square-o"></i>
+        <i data-bind="visible: ! isMine" class="fa fa-user"></i>
+      </a>
+    </td>
+  </tr>
+</script>
+
+<div id="tagsModal" class="modal hide fade">
+  <div class="modal-header">
+    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+    <h3>${_('Manage projects')}</h3>
+  </div>
+  <div class="modal-body">
+    <p>
+      <div data-bind="template: { name: 'tag-edit-template', foreach: myTags }"></div>
+      <div class="clearfix"></div>
+      <div style="margin-top: 20px">
+        <div class="input-append">
+          <input id="tagsNew" type="text">
+          <a id="tagsNewBtn" class="btn" type="button"><i class="fa fa-plus-circle"></i> ${ _('Add') }</a>
+        </div>
+      </div>
+    </p>
+  </div>
+  <div class="modal-footer">
+    <a href="#" data-dismiss="modal" class="btn">${_('Cancel')}</a>
+    <a id="removeTags" href="#" class="btn btn-danger disable-feedback">${_('Remove selected')}</a>
+  </div>
+</div>
+
+
+<script type="text/html" id="tag-edit-template">
+  <div style="margin-right:10px;margin-bottom: 6px;float:left;">
+    <span class="tags-modal-checkbox badge">
+       <i class="fa fa-trash-o hide"></i> <span data-bind="text: name"></span>
+    </span>
+  </div>
+</script>
+
+<script src="/static/ext/js/datatables-paging-0.1.js" type="text/javascript" charset="utf-8"></script>
+<script src="/static/ext/js/knockout-min.js" type="text/javascript" charset="utf-8"></script>
+<script src="/static/ext/js/knockout.mapping-2.3.2.js" type="text/javascript" charset="utf-8"></script>
+<script src="/static/js/home.vm.js"></script>
+
+
+<script type="text/javascript" charset="utf-8">
+$(document).ready(function () {
+  var viewModel = new HomeViewModel(${ json_tags | n,unicode }, ${ json_documents | n,unicode });
+  ko.applyBindings(viewModel);
+
+  $("#tagsNewBtn").on("click", function () {
+    var tag_name = $("#tagsNew").val(); // use ko var + bind enable/disable button accordingly (blank, duplicate, reserved...)?
+    $.post("/desktop/api/tag/add_tag", {
+      name: tag_name
+    }, function (data) {
+      viewModel.createTag(data);
+      $("#tagsNew").val("");
+      $(document).trigger("info", "${_('Tag created')}");
+    }).fail(function(xhr, textStatus, errorThrown) {
+      $(document).trigger("error", xhr.responseText); // reserved name, duplicate etc
+    });
+  });
+});
+
+function editTags() {
+  // reset selected tags
+  $("#tagsModal").modal("show"); 
+}
+</script>
+
+
+${ commonfooter(messages) | n,unicode }

+ 1 - 0
desktop/core/src/desktop/urls.py

@@ -55,6 +55,7 @@ dynamic_patterns = patterns('desktop.auth.views',
 dynamic_patterns += patterns('desktop.views',
   (r'^logs$','log_view'),
   (r'^home$','home'),
+  (r'^desktop/home2$','home2'),
   (r'^desktop/dump_config$','dump_config'),
   (r'^desktop/download_logs$','download_log_view'),
   (r'^bootstrap.js$', 'bootstrap'), # unused

+ 21 - 1
desktop/core/src/desktop/views.py

@@ -44,11 +44,31 @@ from desktop.models import UserPreferences, Settings, Document, DocumentTag
 from desktop import appmanager
 import desktop.conf
 import desktop.log.log_buffer
-from desktop.api import massaged_tags_for_json, massaged_documents_for_json
+from desktop.api import massaged_tags_for_json, massaged_documents_for_json, massaged_documents_for_json2, massaged_tags_for_json2
 
 
 LOG = logging.getLogger(__name__)
 
+def home2(request):
+  docs = itertools.chain(
+      Document.objects.get_docs(request.user).order_by('-last_modified').exclude(tags__tag__in=['history'])[:500],
+      Document.objects.get_docs(request.user).order_by('-last_modified').filter(tags__tag__in=['history'])[:100]
+  )
+  docs = list(docs)
+  tags = list(set([tag for doc in docs for tag in doc.tags.all()])) # List of all personal and shared tags
+
+  apps = appmanager.get_apps_dict(request.user)
+
+  #print json.dumps(massaged_documents_for_json2(docs, request.user))
+#  for a, v in massaged_documents_for_json2(docs, request.user)['docs']['mydocs'].iteritems():
+#    print a
+#    print v
+  return render('home2.mako', request, {
+    'apps': apps,
+    'documents': massaged_documents_for_json2(docs, request.user),
+    'json_documents': json.dumps(massaged_documents_for_json2(docs, request.user)),
+    'json_tags': json.dumps(massaged_tags_for_json2(tags, request.user))
+  })
 
 def home(request):
   docs = itertools.chain(

+ 92 - 0
desktop/core/static/js/home.vm.js

@@ -0,0 +1,92 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you 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.
+
+
+function HomeViewModel(json_tags, json_docs) {
+  var self = this;
+
+  var TAGS_DEFAULTS = {
+    'history': {'name': 'History', 'id': 1, 'docs': [1], 'type': 'history'},
+    'trash': {'name': 'Trash', 'id': 3, 'docs': [2]},
+    'mine': [{'name': 'default', 'id': 2, 'docs': [3]}, {'name': 'web', 'id': 3, 'docs': [3]}],
+    'notmine': [{'name': 'example', 'id': 20, 'docs': [10]}, {'name': 'ex2', 'id': 30, 'docs': [10, 11]}]
+  };
+
+  var DOCUMENTS_DEFAULTS = {
+    '1': {
+      'id': 1,
+      'name': 'my query history', 'description': '', 'url': '/beeswax/execute/design/83', 'icon': '/beeswax/static/art/icon_beeswax_24.png',
+      'lastModified': '03/11/14 16:06:49', 'owner': 'admin', 'lastModifiedInMillis': 1394579209.0, 'isMine': true
+    },
+    '2': {
+      'id': 2,
+      'name': 'my query 2 trashed', 'description': '', 'url': '/beeswax/execute/design/83', 'icon': '/beeswax/static/art/icon_beeswax_24.png',
+      'lastModified': '03/11/14 16:06:49', 'owner': 'admin', 'lastModifiedInMillis': 1394579209.0, 'isMine': true
+     },
+     '3': {
+       'id': 3,
+       'name': 'my query 3 tagged twice', 'description': '', 'url': '/beeswax/execute/design/83', 'icon': '/beeswax/static/art/icon_beeswax_24.png',
+     'lastModified': '03/11/14 16:06:49', 'owner': 'admin', 'lastModifiedInMillis': 1394579209.0, 'isMine': true
+     },
+    '10': {
+      'id': 10,
+      'name': 'my query 3 shared', 'description': '', 'url': '/beeswax/execute/design/83', 'icon': '/beeswax/static/art/icon_beeswax_24.png',
+      'lastModified': '03/11/14 16:06:49', 'owner': 'admin', 'lastModifiedInMillis': 1394579209.0, 'isMine': true
+     },
+    '11': {
+      'id': 11,
+      'name': 'my query 4 shared', 'description': '', 'url': '/beeswax/execute/design/83', 'icon': '/beeswax/static/art/icon_beeswax_24.png',
+      'lastModified': '03/11/14 16:06:49', 'owner': 'admin', 'lastModifiedInMillis': 1394579209.0, 'isMine': true
+     }
+  };
+
+
+  self.tags = ko.mapping.fromJS(TAGS_DEFAULTS);
+  self.documents = ko.observableArray([]);
+  
+  self.editTagsToCreate = ko.observableArray([]);
+  self.editTagsToDelete = ko.observableArray([]);
+
+  self.trash = ko.computed(function() {
+    return self.tags.trash;
+  });
+
+  self.history = ko.computed(function() {
+	return self.tags.history;
+  });
+
+  self.myTags = ko.computed(function() {
+    return self.tags.mine();
+  });
+
+  self.sharedTags = ko.computed(function() {
+	return self.tags.notmine();
+  });
+
+  self.filterDocs = function(tag) {
+    self.documents.removeAll();
+    $.each(DOCUMENTS_DEFAULTS, function(id, doc) {
+      if (tag.docs().indexOf(parseInt(id)) != -1) { // Beware, keys are strings in js
+    	self.documents.push(doc); // pushall?
+      }
+	})
+  }
+  
+  self.createTag = function(tag_json) {
+	var mapped_tag = ko.mapping.fromJS({'name': 'default2', 'id': 50, 'docs': [3]}); // todo
+	self.tags.mine.push(mapped_tag);
+  }
+}