diff --git a/.gitmodules b/.gitmodules
index c3ac1c7f45b8c0b275af49c6f826df90849ab71d..324812f5ee189c3ddf42b5ead52f542ea9fec75d 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -41,4 +41,7 @@
 [submodule "dependencies/parsers/wien2k"]
 	path = dependencies/parsers/wien2k
 	url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-wien2k
-	branch = nomad-fair
\ No newline at end of file
+	branch = nomad-fair
+[submodule "dependencies/parsers/parser-band"]
+	path = dependencies/parsers/band
+	url = git@gitlab.mpcdf.mpg.de:nomad-lab/parser-band.git
diff --git a/.vscode/launch.json b/.vscode/launch.json
index e832da43012039853b691d44a2f87a40beca9e64..8c7a98bc22c8ff21efb69144a37194bb133dc1e7 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -44,7 +44,7 @@
       "cwd": "${workspaceFolder}",
       "program": "${workspaceFolder}/.pyenv/bin/pytest",
       "args": [
-        "-sv", "tests/test_parsing.py::test_parser[parsers/vasp-tests/data/parsers/vasp_compressed/vasp.xml.gz]"
+        "-sv", "tests/test_api.py::TestRepo::test_search[2-user-other_test_user]"
       ]
     },
     {
diff --git a/dependencies/parsers/band b/dependencies/parsers/band
new file mode 160000
index 0000000000000000000000000000000000000000..2e47c739d555088c51900fbbe973b86e6b1e603b
--- /dev/null
+++ b/dependencies/parsers/band
@@ -0,0 +1 @@
+Subproject commit 2e47c739d555088c51900fbbe973b86e6b1e603b
diff --git a/gui/src/components/Repo.js b/gui/src/components/Repo.js
index ba3e5fbffd99767210defdf7a9b2e759b2cf4c15..c36252ce1c887b44943d1ffa38bfd7a74bdc2a9f 100644
--- a/gui/src/components/Repo.js
+++ b/gui/src/components/Repo.js
@@ -49,13 +49,13 @@ class Repo extends React.Component {
   })
 
   static rowConfig = {
-    chemical_composition: 'Formula',
-    program_name: 'Code',
-    basis_set_type: 'Basis set',
-    system_type: 'System',
-    crystal_system: 'Crystal',
-    space_group_number: 'Space group',
-    XC_functional_name: 'XT treatment'
+    formula: 'Formula',
+    code_name: 'Code',
+    basis_set: 'Basis set',
+    system: 'System',
+    crystal_system: 'Crystal system',
+    spacegroup: 'Spacegroup',
+    xc_functional: 'XT treatment'
   }
 
   state = {
diff --git a/nomad/api/repo.py b/nomad/api/repo.py
index 7131715ee52d1f937bb8df188b7fa34412ca3d3b..292ba8eecb56e04cf0eb70b384a7b7eb2883b672 100644
--- a/nomad/api/repo.py
+++ b/nomad/api/repo.py
@@ -18,8 +18,11 @@ meta-data.
 """
 
 from flask_restplus import Resource, abort, fields
+from flask import request, g
+from elasticsearch_dsl import Q
 
 from nomad.files import UploadFiles, Restricted
+from nomad.search import Entry
 
 from .app import api
 from .auth import login_if_available, create_authorization_predicate
@@ -80,38 +83,42 @@ class RepoCalcsResource(Resource):
 
         This is currently not implemented!
         """
-        return dict(pagination=dict(total=0, page=1, per_page=10), results=[]), 200
-        # page = int(request.args.get('page', 1))
-        # per_page = int(request.args.get('per_page', 10))
-        # owner = request.args.get('owner', 'all')
-
-        # try:
-        #     assert page >= 1
-        #     assert per_page > 0
-        # except AssertionError:
-        #     abort(400, message='invalid pagination')
-
-        # if owner == 'all':
-        #     search = RepoCalc.search().query('match_all')
-        # elif owner == 'user':
-        #     if g.user is None:
-        #         abort(401, message='Authentication required for owner value user.')
-        #     search = RepoCalc.search().query('match_all')
-        #     search = search.filter('term', user_id=str(g.user.user_id))
-        # elif owner == 'staging':
-        #     if g.user is None:
-        #         abort(401, message='Authentication required for owner value user.')
-        #     search = RepoCalc.search().query('match_all')
-        #     search = search.filter('term', user_id=str(g.user.user_id)).filter('term', staging=True)
-        # else:
-        #     abort(400, message='Invalid owner value. Valid values are all|user|staging, default is all')
-
-        # search = search[(page - 1) * per_page: page * per_page]
-        # return {
-        #     'pagination': {
-        #         'total': search.count(),
-        #         'page': page,
-        #         'per_page': per_page
-        #     },
-        #     'results': [result.json_dict for result in search]
-        # }, 200
+        # return dict(pagination=dict(total=0, page=1, per_page=10), results=[]), 200
+
+        page = int(request.args.get('page', 1))
+        per_page = int(request.args.get('per_page', 10))
+        owner = request.args.get('owner', 'all')
+
+        try:
+            assert page >= 1
+            assert per_page > 0
+        except AssertionError:
+            abort(400, message='invalid pagination')
+
+        if owner == 'all':
+            if g.user is None:
+                q = Q('term', published=True)
+            else:
+                q = Q('term', published=True) | Q('term', uploader__user_id=g.user.user_id)
+        elif owner == 'user':
+            if g.user is None:
+                abort(401, message='Authentication required for owner value user.')
+
+            q = Q('term', uploader__user_id=g.user.user_id)
+        elif owner == 'staging':
+            if g.user is None:
+                abort(401, message='Authentication required for owner value user.')
+            q = Q('term', published=False) & Q('term', uploader__user_id=g.user.user_id)
+        else:
+            abort(400, message='Invalid owner value. Valid values are all|user|staging, default is all')
+
+        search = Entry.search().query(q)
+        search = search[(page - 1) * per_page: page * per_page]
+        return {
+            'pagination': {
+                'total': search.count(),
+                'page': page,
+                'per_page': per_page
+            },
+            'results': [hit.to_dict() for hit in search]
+        }, 200
diff --git a/nomad/datamodel.py b/nomad/datamodel.py
index 2df4069f63b260a9130cb2da3bb98f924d19fc52..edbcee639363203c3cd3f41f1d8d8c8a371b14c8 100644
--- a/nomad/datamodel.py
+++ b/nomad/datamodel.py
@@ -98,6 +98,7 @@ class CalcWithMetadata():
         self.uploader: utils.POPO = None
 
         self.with_embargo: bool = None
+        self.published: bool = False
         self.coauthors: List[utils.POPO] = []
         self.shared_with: List[utils.POPO] = []
         self.comment: str = None
@@ -114,8 +115,7 @@ class CalcWithMetadata():
         self.code_name: str = None
         self.code_version: str = None
 
-        for key, value in kwargs.items():
-            setattr(self, key, value)
+        self.update(**kwargs)
 
     def to_dict(self):
         return {
@@ -123,6 +123,10 @@ class CalcWithMetadata():
             if value is not None
         }
 
+    def update(self, **kwargs):
+        for key, value in kwargs.items():
+            setattr(self, key, value)
+
     def apply_user_metadata(self, metadata: dict):
         """
         Applies a user provided metadata dict to this calc.
diff --git a/nomad/processing/data.py b/nomad/processing/data.py
index 8d06aa9410e230bf6440a3c0c7deb47e5f37b90f..25c7bdf69193133d27db0a9f5b22d6738e1ff976 100644
--- a/nomad/processing/data.py
+++ b/nomad/processing/data.py
@@ -239,7 +239,8 @@ class Calc(Proc):
 
         # index in search
         with utils.timer(logger, 'indexed', step='persist'):
-            search.Entry.from_calc_with_metadata(calc_with_metadata, published=False).save()
+            calc_with_metadata.update(published=False, uploader=self.upload.uploader.to_popo())
+            search.Entry.from_calc_with_metadata(calc_with_metadata).save()
 
         # persist the archive
         with utils.timer(
diff --git a/nomad/search.py b/nomad/search.py
index db5c79966b0a7ca186df94737c69806bebb6076b..b4572ccdc4d6629b3a0f69b7f29190c923cbb2ec 100644
--- a/nomad/search.py
+++ b/nomad/search.py
@@ -17,7 +17,7 @@ This module represents calculations in elastic search.
 """
 
 from elasticsearch_dsl import Document, InnerDoc, Keyword, Text, Date, \
-    Nested, Boolean, Search
+    Object, Boolean, Search
 
 from nomad import config, datamodel, infrastructure, datamodel, coe_repo
 
@@ -29,7 +29,7 @@ class User(InnerDoc):
 
     @classmethod
     def from_user_popo(cls, user):
-        self = cls(id=user.id)
+        self = cls(user_id=user.id)
 
         if 'first_name' not in user:
             user = coe_repo.User.from_user_id(user.id).to_popo()
@@ -39,7 +39,7 @@ class User(InnerDoc):
 
         return self
 
-    id = Keyword()
+    user_id = Keyword()
     name = Text()
     name_keyword = Keyword()
 
@@ -69,16 +69,16 @@ class Entry(Document):
     pid = Keyword()
     mainfile = Keyword()
     files = Keyword(multi=True)
-    uploader = Nested(User)
+    uploader = Object(User)
 
     with_embargo = Boolean()
     published = Boolean()
 
-    coauthors = Nested(User)
-    shared_with = Nested(User)
+    coauthors = Object(User)
+    shared_with = Object(User)
     comment = Text()
     references = Keyword()
-    datasets = Nested(Dataset)
+    datasets = Object(Dataset)
 
     formula = Keyword()
     atoms = Keyword(multi=True)
@@ -91,7 +91,7 @@ class Entry(Document):
     code_version = Keyword()
 
     @classmethod
-    def from_calc_with_metadata(cls, source: datamodel.CalcWithMetadata, published: bool = False) -> 'Entry':
+    def from_calc_with_metadata(cls, source: datamodel.CalcWithMetadata) -> 'Entry':
         return Entry(
             meta=dict(id=source.calc_id),
             upload_id=source.upload_id,
@@ -104,7 +104,7 @@ class Entry(Document):
             uploader=User.from_user_popo(source.uploader) if source.uploader is not None else None,
 
             with_embargo=source.with_embargo,
-            published=published,
+            published=source.published,
             coauthors=[User.from_user_popo(user) for user in source.coauthors],
             shared_with=[User.from_user_popo(user) for user in source.shared_with],
             comment=source.comment,
diff --git a/tests/conftest.py b/tests/conftest.py
index 02350c7c5b706b10eed2e5f0d29d6178ca4b8007..314dca817cea1b4da2cc2600fa5b171ddbd7f8a1 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -36,6 +36,7 @@ from tests.processing import test_data as test_processing
 from tests.test_files import example_file, empty_file
 from tests.bravado_flask import FlaskTestHttpClient
 
+test_log_level = logging.CRITICAL
 example_files = [empty_file, example_file]
 
 
@@ -50,7 +51,7 @@ def monkeysession(request):
 @pytest.fixture(scope='session', autouse=True)
 def nomad_logging():
     config.logstash = config.logstash._replace(enabled=False)
-    config.console_log_level = logging.CRITICAL
+    config.console_log_level = test_log_level
     infrastructure.setup_logging()
 
 
@@ -129,7 +130,7 @@ def celery_inspect(purged_app):
 
 # It might be necessary to make this a function scoped fixture, if old tasks keep
 # 'bleeding' into successive tests.
-@pytest.fixture(scope='session')
+@pytest.fixture(scope='function')
 def worker(celery_session_worker, celery_inspect):
     """ Provides a clean worker (no old tasks) per function. Waits for all tasks to be completed. """
     pass
@@ -170,18 +171,22 @@ def elastic_infra(monkeysession):
         return infrastructure.setup_elastic()
 
 
-@pytest.fixture(scope='function')
-def elastic(elastic_infra):
-    """ Provides a clean elastic per function. Clears elastic before test. """
+def clear_elastic(elastic):
     while True:
         try:
-            elastic_infra.delete_by_query(
+            elastic.delete_by_query(
                 index='test_nomad_fairdi_calcs', body=dict(query=dict(match_all={})),
                 wait_for_completion=True, refresh=True)
             break
         except Exception:
             time.sleep(0.1)
 
+
+@pytest.fixture(scope='function')
+def elastic(elastic_infra):
+    """ Provides a clean elastic per function. Clears elastic before test. """
+    clear_elastic(elastic_infra)
+
     assert infrastructure.elastic_client is not None
     return elastic_infra
 
@@ -302,7 +307,7 @@ def test_user_auth(test_user: coe_repo.User):
 
 
 @pytest.fixture(scope='module')
-def test_other_user_auth(other_test_user: coe_repo.User):
+def other_test_user_auth(other_test_user: coe_repo.User):
     return create_auth_headers(other_test_user)
 
 
@@ -483,14 +488,14 @@ def example_user_metadata(other_test_user, test_user) -> dict:
     }
 
 
-@pytest.fixture(scope='function')
+@pytest.fixture(scope='session')
 def parsed(example_mainfile: Tuple[str, str]) -> parsing.LocalBackend:
     """ Provides a parsed calculation in the form of a LocalBackend. """
     parser, mainfile = example_mainfile
     return test_parsing.run_parser(parser, mainfile)
 
 
-@pytest.fixture(scope='function')
+@pytest.fixture(scope='session')
 def normalized(parsed: parsing.LocalBackend) -> parsing.LocalBackend:
     """ Provides a normalized calculation in the form of a LocalBackend. """
     return test_normalizing.run_normalize(parsed)
diff --git a/tests/test_api.py b/tests/test_api.py
index 48e5c61aff6a5492b54f57c2a3fb51517dd3dcfa..47179e5115e992ecef608fa3513e4a75dec13435 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -21,12 +21,11 @@ import io
 import inspect
 from passlib.hash import bcrypt
 
-from nomad import config, coe_repo
+from nomad import config, coe_repo, search, parsing
 from nomad.files import UploadFiles, PublicUploadFiles
 from nomad.processing import Upload, Calc, SUCCESS
-from nomad.coe_repo import User
 
-from tests.conftest import create_auth_headers
+from tests.conftest import create_auth_headers, clear_elastic
 from tests.test_files import example_file, example_file_mainfile, example_file_contents
 from tests.test_files import create_staging_upload, create_public_upload
 from tests.test_coe_repo import assert_coe_upload
@@ -85,7 +84,7 @@ class TestAdmin:
 
 
 class TestAuth:
-    def test_xtoken_auth(self, client, test_user: User, no_warn):
+    def test_xtoken_auth(self, client, test_user: coe_repo.User, no_warn):
         rv = client.get('/uploads/', headers={
             'X-Token': test_user.first_name.lower()  # the test users have their firstname as tokens for convinience
         })
@@ -110,7 +109,7 @@ class TestAuth:
         })
         assert rv.status_code == 401
 
-    def test_get_user(self, client, test_user_auth, test_user: User, no_warn):
+    def test_get_user(self, client, test_user_auth, test_user: coe_repo.User, no_warn):
         rv = client.get('/auth/user', headers=test_user_auth)
         assert rv.status_code == 200
         self.assert_user(client, json.loads(rv.data))
@@ -521,6 +520,24 @@ class TestArchive(UploadFilesBasedTests):
 
 
 class TestRepo(UploadFilesBasedTests):
+    @pytest.fixture(scope='class')
+    def example_elastic_calcs(
+            self, elastic_infra, normalized: parsing.LocalBackend,
+            test_user: coe_repo.User, other_test_user: coe_repo.User):
+
+        clear_elastic(elastic_infra)
+
+        calc_with_metadata = normalized.to_calc_with_metadata()
+
+        calc_with_metadata.update(calc_id='1', uploader=test_user.to_popo(), published=True)
+        search.Entry.from_calc_with_metadata(calc_with_metadata).save(refresh=True)
+
+        calc_with_metadata.update(calc_id='2', uploader=other_test_user.to_popo(), published=True)
+        search.Entry.from_calc_with_metadata(calc_with_metadata).save(refresh=True)
+
+        calc_with_metadata.update(calc_id='3', uploader=other_test_user.to_popo(), published=False)
+        search.Entry.from_calc_with_metadata(calc_with_metadata).save(refresh=True)
+
     @UploadFilesBasedTests.ignore_authorization
     def test_calc(self, client, upload, auth_headers):
         rv = client.get('/repo/%s/0' % upload, headers=auth_headers)
@@ -531,43 +548,39 @@ class TestRepo(UploadFilesBasedTests):
         rv = client.get('/repo/doesnt/exist', headers=auth_headers)
         assert rv.status_code == 404
 
-    # def test_calcs(self, client, example_elastic_calc, no_warn):
-    #     rv = client.get('/repo/')
-    #     assert rv.status_code == 200
-    #     data = json.loads(rv.data)
-    #     results = data.get('results', None)
-    #     assert results is not None
-    #     assert isinstance(results, list)
-    #     assert len(results) >= 1
-
-    # def test_calcs_pagination(self, client, example_elastic_calc, no_warn):
-    #     rv = client.get('/repo/?page=1&per_page=1')
-    #     assert rv.status_code == 200
-    #     data = json.loads(rv.data)
-    #     results = data.get('results', None)
-    #     assert results is not None
-    #     assert isinstance(results, list)
-    #     assert len(results) == 1
-
-    # def test_calcs_user(self, client, example_elastic_calc, test_user_auth, no_warn):
-    #     rv = client.get('/repo/?owner=user', headers=test_user_auth)
-    #     assert rv.status_code == 200
-    #     data = json.loads(rv.data)
-    #     results = data.get('results', None)
-    #     assert results is not None
-    #     assert len(results) >= 1
-
-    # def test_calcs_user_authrequired(self, client, example_elastic_calc, no_warn):
-    #     rv = client.get('/repo/?owner=user')
-    #     assert rv.status_code == 401
-
-    # def test_calcs_user_invisible(self, client, example_elastic_calc, test_other_user_auth, no_warn):
-    #     rv = client.get('/repo/?owner=user', headers=test_other_user_auth)
-    #     assert rv.status_code == 200
-    #     data = json.loads(rv.data)
-    #     results = data.get('results', None)
-    #     assert results is not None
-    #     assert len(results) == 0
+    @pytest.mark.parametrize('calcs, owner, auth', [
+        (2, 'all', 'none'),
+        (2, 'all', 'test_user'),
+        (1, 'user', 'test_user'),
+        (2, 'user', 'other_test_user'),
+        (0, 'staging', 'test_user'),
+        (1, 'staging', 'other_test_user'),
+    ])
+    def test_search(self, client, example_elastic_calcs, no_warn, test_user_auth, other_test_user_auth, calcs, owner, auth):
+        auth = dict(none=None, test_user=test_user_auth, other_test_user=other_test_user_auth).get(auth)
+        rv = client.get('/repo/?owner=%s' % owner, headers=auth)
+        assert rv.status_code == 200
+        data = json.loads(rv.data)
+        results = data.get('results', None)
+        assert results is not None
+        assert isinstance(results, list)
+        assert len(results) == calcs
+        if calcs > 0:
+            for key in ['uploader', 'calc_id', 'formula', 'upload_id']:
+                assert key in results[0]
+
+    def test_calcs_pagination(self, client, example_elastic_calcs, no_warn):
+        rv = client.get('/repo/?page=1&per_page=1')
+        assert rv.status_code == 200
+        data = json.loads(rv.data)
+        results = data.get('results', None)
+        assert results is not None
+        assert isinstance(results, list)
+        assert len(results) == 1
+
+    def test_search_user_authrequired(self, client, example_elastic_calcs, no_warn):
+        rv = client.get('/repo/?owner=user')
+        assert rv.status_code == 401
 
 
 class TestRaw(UploadFilesBasedTests):
diff --git a/tests/test_search.py b/tests/test_search.py
index 8594182adc8d77febe11b79319268117578aaef8..2aeb618ca069dab8b715779878ebe1c2f7931eb6 100644
--- a/tests/test_search.py
+++ b/tests/test_search.py
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from elasticsearch_dsl import Q
+
 from nomad import datamodel, search, processing, parsing
 from nomad.search import Entry
 
@@ -46,10 +48,15 @@ def test_index_upload(elastic, processed: processing.Upload):
 
 
 def create_entry(calc_with_metadata: datamodel.CalcWithMetadata):
-    search.Entry.from_calc_with_metadata(calc_with_metadata).save()
+    search.Entry.from_calc_with_metadata(calc_with_metadata).save(refresh=True)
     assert_entry(calc_with_metadata.calc_id)
 
 
 def assert_entry(calc_id):
     calc = Entry.get(calc_id)
     assert calc is not None
+
+    search = Entry.search().query(Q('term', calc_id=calc_id))[0:10]
+    assert search.count() == 1
+    results = list(hit.to_dict() for hit in search)
+    assert results[0]['calc_id'] == calc_id