From cbce84c94d0e4267cd148ceea7d4376f214e43a7 Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Mon, 18 May 2020 14:19:46 +0200
Subject: [PATCH] Add URLs to parsers/codes. Added code list and mini doc to
 upload page. #343

---
 gui/src/components/About.js              | 22 +++++++++++++++++++++-
 gui/src/components/uploads/UploadPage.js | 18 +++++++++++++++---
 nomad/app/api/info.py                    | 21 +++++++++++++++------
 nomad/parsing/__init__.py                |  2 +-
 nomad/parsing/parser.py                  |  5 ++++-
 5 files changed, 56 insertions(+), 12 deletions(-)

diff --git a/gui/src/components/About.js b/gui/src/components/About.js
index af9fb48dcc..d270474d03 100644
--- a/gui/src/components/About.js
+++ b/gui/src/components/About.js
@@ -9,6 +9,26 @@ import { domains } from './domains'
 import { Grid, Card, CardContent, Typography, makeStyles, Link } from '@material-ui/core'
 import { Link as RouterLink, useHistory } from 'react-router-dom'
 
+export const CodeList = () => {
+  const {info} = useContext(apiContext)
+
+  if (!info) {
+    return '...'
+  }
+
+  return info.codes.reduce((result, code, index) => {
+    if (index !== 0) {
+      result.push(', ')
+    }
+    if (code.code_homepage) {
+      result.push(<Link target="external" key={code.code_name} href={code.code_homepage}>{code.code_name}</Link>)
+    } else {
+      result.push(code.code_name)
+    }
+    return result
+  }, [])
+}
+
 const useCardStyles = makeStyles(theme => ({
   title: {
     marginBottom: theme.spacing(1)
@@ -173,7 +193,7 @@ export default function About() {
         You can inspect the Archive form and extracted metadata before
         publishing your data.
         </p>
-        <p>NOMAD supports most community codes: {info ? info.codes.join(', ') : '...'}</p>
+        <p>NOMAD supports most community codes: <CodeList/></p>
         <p>
         To use NOMAD&apos;s parsers and normalizers outside of NOMAD.
         Read <Link href="">here</Link> on how to install
diff --git a/gui/src/components/uploads/UploadPage.js b/gui/src/components/uploads/UploadPage.js
index 6416ad8558..68e4050daa 100644
--- a/gui/src/components/uploads/UploadPage.js
+++ b/gui/src/components/uploads/UploadPage.js
@@ -1,7 +1,7 @@
 import React from 'react'
 import PropTypes, { instanceOf } from 'prop-types'
 import Markdown from '../Markdown'
-import { withStyles, Paper, IconButton, FormGroup, FormLabel, Tooltip } from '@material-ui/core'
+import { withStyles, Paper, IconButton, FormGroup, FormLabel, Tooltip, Typography } from '@material-ui/core'
 import UploadIcon from '@material-ui/icons/CloudUpload'
 import Dropzone from 'react-dropzone'
 import Upload from './Upload'
@@ -16,6 +16,7 @@ import Pagination from 'material-ui-flat-pagination'
 import { CopyToClipboard } from 'react-copy-to-clipboard'
 import { guiBase } from '../../config'
 import qs from 'qs'
+import { CodeList } from '../About'
 
 export const help = `
 NOMAD allows you to upload data. After upload, NOMAD will process your data: it will
@@ -126,7 +127,8 @@ class UploadPage extends React.Component {
       '& svg': {
         marginLeft: 'auto',
         marginRight: 'auto'
-      }
+      },
+      marginTop: theme.spacing(3)
     },
     dropzoneAccept: {
       background: theme.palette.primary.main,
@@ -267,6 +269,16 @@ class UploadPage extends React.Component {
 
     return (
       <div className={classes.root}>
+        <Typography>
+          To prepare your data, simply use <b>zip</b> or <b>tar</b> to create a single file that contains
+          all your files as they are. These .zip/.tar files can contain subdirectories and additional files.
+          NOMAD will search through all files and identify the relevant files automatically.
+          Each uploaded file can be <b>up to 32GB</b> in size, you can have <b>up to 10 unpublished
+          uploads</b> simultaneously. Your uploaded data is not published right away.
+        </Typography>
+        <Typography>
+          The following codes are supported: <CodeList/>.
+        </Typography>
         <Paper className={classes.dropzoneContainer}>
           <Dropzone
             accept={[
@@ -288,7 +300,7 @@ class UploadPage extends React.Component {
             rejectClassName={classes.dropzoneReject}
             onDrop={this.onDrop.bind(this)}
           >
-            <p>drop .tar.gz or .zip files here</p>
+            <p>click or drop .tar.gz/.zip files here</p>
             <UploadIcon style={{fontSize: 36}}/>
           </Dropzone>
         </Paper>
diff --git a/nomad/app/api/info.py b/nomad/app/api/info.py
index 1b99a35826..73521c26b0 100644
--- a/nomad/app/api/info.py
+++ b/nomad/app/api/info.py
@@ -54,10 +54,15 @@ statistics_info_model = api.model('StatisticsInfo', {
     # 'archive_file_size': fields.Integer(description='Total amount of binary archive data in TB')
 })
 
+code_info_model = api.model('CodeInfo', {
+    'code_name': fields.String(description='Name of the code or input format', allow_null=True),
+    'code_homepage': fields.String(description='Homepage of the code or input format', allow_null=True)
+}, allow_null=True, skip_none=True)
+
 info_model = api.model('Info', {
     'parsers': fields.List(fields.String),
     'metainfo_packages': fields.List(fields.String),
-    'codes': fields.List(fields.String),
+    'codes': fields.List(fields.Nested(code_info_model)),
     'normalizers': fields.List(fields.String),
     'domains': fields.List(fields.Nested(model=domain_model)),
     'statistics': fields.Nested(model=statistics_info_model, description='General NOMAD statistics'),
@@ -88,10 +93,14 @@ class InfoResource(Resource):
     @api.marshal_with(info_model, skip_none=True, code=200, description='Info send')
     def get(self):
         ''' Return information about the nomad backend and its configuration. '''
-        codes = [
-            parser.code_name
-            for parser in parsing.parser_dict.values()
-            if isinstance(parser, parsing.MatchingParser) and parser.domain == 'dft']
+        codes_dict = {}
+        for parser in parsing.parser_dict.values():
+            if isinstance(parser, parsing.MatchingParser) and parser.domain == 'dft':
+                code_name = parser.code_name
+                if code_name in codes_dict:
+                    continue
+                codes_dict[code_name] = dict(code_name=code_name, code_homepage=parser.code_homepage)
+        codes = sorted(list(codes_dict.values()), key=lambda code_info: code_info['code_name'].lower())
 
         return {
             'parsers': [
@@ -100,7 +109,7 @@ class InfoResource(Resource):
             'metainfo_packages': ['general', 'general.experimental', 'common', 'public'] + sorted([
                 key[key.index('/') + 1:]
                 for key in parsing.parser_dict.keys()]),
-            'codes': sorted(set(codes), key=lambda x: x.lower()),
+            'codes': codes,
             'normalizers': [normalizer.__name__ for normalizer in normalizing.normalizers],
             'statistics': statistics(),
             'domains': [
diff --git a/nomad/parsing/__init__.py b/nomad/parsing/__init__.py
index 92b93cde30..728f61288c 100644
--- a/nomad/parsing/__init__.py
+++ b/nomad/parsing/__init__.py
@@ -178,7 +178,7 @@ parsers = [
         mainfile_name_re=(r'.*/phonopy-FHI-aims-displacement-0*1/control.in$')
     ),
     LegacyParser(
-        name='parsers/vasp', code_name='VASP',
+        name='parsers/vasp', code_name='VASP', code_homepage='https://www.vasp.at/',
         parser_class_name='vaspparser.VASPRunParser',
         mainfile_mime_re=r'(application/.*)|(text/.*)',
         mainfile_contents_re=(
diff --git a/nomad/parsing/parser.py b/nomad/parsing/parser.py
index f039653586..8edc85d0f7 100644
--- a/nomad/parsing/parser.py
+++ b/nomad/parsing/parser.py
@@ -99,6 +99,8 @@ class MatchingParser(Parser):
     A parser implementation that used regular experessions to match mainfiles.
 
     Arguments:
+        code_name: The name of the code or input format
+        code_homepage: The homepage of the code or input format
         mainfile_mime_re: A regexp that is used to match against a files mime type
         mainfile_contents_re: A regexp that is used to match the first 1024 bytes of a
             potential mainfile.
@@ -107,7 +109,7 @@ class MatchingParser(Parser):
         supported_compressions: A list of [gz, bz2], if the parser supports compressed files
     '''
     def __init__(
-            self, name: str, code_name: str,
+            self, name: str, code_name: str, code_homepage: str = None,
             mainfile_contents_re: str = None,
             mainfile_binary_header: bytes = None,
             mainfile_mime_re: str = r'text/.*',
@@ -118,6 +120,7 @@ class MatchingParser(Parser):
         super().__init__()
         self.name = name
         self.code_name = code_name
+        self.code_homepage = code_homepage
         self.domain = domain
         self._mainfile_binary_header = mainfile_binary_header
         self._mainfile_mime_re = re.compile(mainfile_mime_re)
-- 
GitLab