Unverified Commit 0085dc1e authored by Ian Sillitoe's avatar Ian Sillitoe Committed by GitHub
Browse files

Merge pull request #4 from CATH-SWISSMODEL/merge-mac-changes

Merge mac changes
parents b49d0d60 68bb87e3
......@@ -126,3 +126,4 @@ secret_key.txt
# Emacs backup files
\#*#
\.\#*
static/
......@@ -6,7 +6,8 @@ var QUERY = {
(function ($) {
"use strict"; // Start of use strict
var stepper = new Stepper($(".bs-stepper")[0], {
var stepperEl = document.getElementById('cathsm-stepper');
var stepper = new Stepper(stepperEl, {
linear: false,
animation: true,
});
......@@ -63,19 +64,62 @@ var QUERY = {
// Do whatever with pasteddata
};
var forms = document.getElementsByClassName('needs-validation');
// Loop over them and prevent submission
var validation = Array.prototype.filter.call(forms, function (form) {
form.addEventListener('submit', function (event) {
console.log('checking form for validity: ', form, event, form.checkValidity());
if (form.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
// https://stackoverflow.com/a/21311717/821642
function validateTextarea() {
console.log("validateTextarea", this);
var errorMsg = "Please match the format requested.";
var textarea = this;
var pattern = new RegExp('^' + $(textarea).attr('pattern') + '$');
// check each line of text
$.each($(this).val().split("\n"), function () {
// check if the line matches the pattern
var hasError = !this.match(pattern);
console.log("match: ", this, pattern, this.match(pattern), hasError);
if (typeof textarea.setCustomValidity === 'function') {
textarea.setCustomValidity(hasError ? errorMsg : '');
} else {
// Not supported by the browser, fallback to manual error display...
$(textarea).toggleClass('error', !!hasError);
$(textarea).toggleClass('ok', !hasError);
if (hasError) {
$(textarea).attr('title', errorMsg);
} else {
$(textarea).removeAttr('title');
}
}
form.classList.add('was-validated');
return !hasError;
});
}
$('.example-sequence').on('click', function (event) {
var seq = $(this).data('sequence');
var seq_id = $(this).data('sequenceId');
$input_sequence.val(seq);
$input_sequence_id.val(seq_id);
})
// Loop over forms and apply our own validation (prevent auto submission)
var validateForm = function (event) {
var $form = $(this);
var formEl = $form[0];
console.log('checking form for validity: ', formEl, $form.find('textarea'), event, formEl.checkValidity());
$form.find('textarea[pattern]').each(validateTextarea);
if (formEl.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
}
$form.addClass('was-validated');
};
$('form.needs-validation').each(function (idx, form) {
$(form).on('submit', function (event) {
validateForm(event);
}, false);
});
$input_sequence.on('change keyup', validateForm);
$input_sequence.on('paste', handleSequencePaste);
$('.bs-stepper form').submit(function (event) {
......@@ -93,7 +137,7 @@ var QUERY = {
};
stepper.addEventListener('shown.bs-stepper', function (event) {
stepperEl.addEventListener('shown.bs-stepper', function (event) {
console.warn('step shown')
switch (event.detail.indexStep) {
......
......@@ -121,7 +121,7 @@
<!-- About Section Content -->
<div class="row">
<div class="col-lg-4 ml-auto">
<div class="col-lg-6 ml-auto">
<h4>CATH</h4>
<p>CATH is a classification of protein structures that groups
protein domains into superfamilies when there is sufficient evidence
......@@ -129,7 +129,7 @@
information can be used to provide unique insights into relationships
between protein sequence, structure and function.</p>
</div>
<div class="col-lg-4 mr-auto">
<div class="col-lg-6 mr-auto">
<h4>SWISS-MODEL</h4>
<p>SWISS-MODEL is a fully automated protein structure homology-modelling
server, accessible via the ExPASy web server, or from the program
......@@ -138,10 +138,6 @@
worldwide.</p>
</div>
<div class="col-lg-4 ml-auto">
<h4>CATHSM</h4>
<p>Foo</p>
</div>
</div>
<!-- About Section Button -->
......
{% extends 'base.html' %}
{% block content %}
<div class="d-flex justify-content-center">
<div class="bs-stepper">
<div id="cathsm-stepper" class="bs-stepper">
<div class="bs-stepper-header" role="tablist">
<!-- your steps here -->
<div class="step" data-target="#submit-sequence-part">
......@@ -29,7 +29,6 @@
</div>
</div>
<div class="bs-stepper-content">
<!-- Submit Sequence -->
<div id="submit-sequence-part" class="content fade" role="tabpanel"
aria-labelledby="submit-sequence-part-trigger">
......@@ -45,16 +44,23 @@
</div>
<div class="form-group">
<label for="input-sequence">Sequence</label>
<textarea rows="10" class="form-control" id="input-sequence"
<textarea rows="5" class="form-control" id="input-sequence"
placeholder="Enter the sequence as a string of amino acids" required
pattern="[^GPAVLIMCFYWHKRQNEDST\s]"></textarea>
pattern="[GPAVLIMCFYWHKRQNEDST]+"></textarea>
<div class="invalid-feedback">Please enter a valid amino-acid sequence</div>
<small id="input-sequence-help" class="form-text text-muted">Copy and paste sequence as a string of
amino-acids (or FASTA format).</small>
</div>
<div class="example-data">
<button type="button" class="btn btn-link example-sequence" data-sequence-id="A0A0Q0Y989"
data-sequence="MNDFHRDTWAEVDLDAIYDNVANLRRLLPDDTHIMAVVKANAYGHGDVQVARTALEAGASRLAVAFLDEALALREKGIEAPILVLGASRPADAALAAQQRIALTVFRSDWLEEASALYSGPFPIHFHLKMDTGMGRLGVKDEEETKRIVALIERHPHFVLEGVYTHFATADEVNTDYFSYQYTRFLHMLEWLPSRPPLVHCANSAASLRFPDRTFNMVRFGIAMYGLAPSPGIKPLLPYPLKEAFSLHSRLVHVKKLQPGEKVSYGATYTAQTEEWIGTIPIGYADGWLRRLQHFHVLVDGQKAPIVGRICMDQCMIRLPGPLPVGTKVTLIGRQGDEVISIDDVARHLETINYEVPCTISYRVPRIFFRHKRIMEVRNAIGRGESSA">Example
1</button>
</div>
<div class="step-actions d-flex justify-content-center">
<button class="btn btn-lg btn-inactive" data-action="clear">Clear</button>
<button class="btn btn-lg btn-primary btn-inactive" data-action="next" type="submit">Next</button>
<button class="btn btn-lg btn-inactive" type="reset">Clear</button>
<button class="btn btn-lg btn-primary btn-inactive" type="submit">Next</button>
</div>
</form>
</div>
......
......@@ -31,8 +31,8 @@ class CustomOpenAPISchemaGenerator(OpenAPISchemaGenerator):
SelectTemplateApi = get_schema_view(
openapi.Info(
title="CATH-SWISSMODEL API",
default_version='v0.0.1',
title="CATH SelectTemplate API",
default_version='v0.0.2',
description=("Select a template structure on which to model "
"the 3D coordinates of a given protein sequence."),
terms_of_service="https://www.google.com/policies/terms/",
......
......@@ -3,9 +3,9 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react-scripts": "2.1.1"
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-scripts": "3.1.1"
},
"scripts": {
"precommit": "pretty-quick staged",
......@@ -25,10 +25,11 @@
"not op_mini all"
],
"devDependencies": {
"@material-ui/core": "^3.5.1",
"@material-ui/icons": "^3.0.1",
"@material-ui/core": "^4.3.3",
"@material-ui/icons": "^4.2.1",
"@types/react": "^16.7.6",
"@types/react-dom": "^16.0.9",
"classnames": "^2.2.6",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.7.0",
"eslint-plugin-prettier": "^3.0.0",
......
......@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import './App.css';
import CssBaseline from '@material-ui/core/CssBaseline';
import AppBar from './components/AppBar.js';
import Container from '@material-ui/core/Container';
import WorkFlow from './components/WorkFlow.js';
class App extends Component {
......@@ -10,8 +10,9 @@ class App extends Component {
return (
<div className="App">
<CssBaseline />
<AppBar />
<WorkFlow />
<Container>
<WorkFlow />
</Container>
</div>
);
}
......
......@@ -12,7 +12,7 @@ import TableSortLabel from "@material-ui/core/TableSortLabel";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
//import Button from "@material-ui/core/Button";
import Badge from "@material-ui/core/Badge";
import Chip from "@material-ui/core/Chip";
import IconButton from "@material-ui/core/IconButton";
......@@ -112,18 +112,18 @@ FunfamMatchTableHead.propTypes = {
const toolbarStyles = theme => ({
root: {
paddingRight: theme.spacing.unit
paddingRight: theme.spacing(1)
},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85)
}
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85)
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark
},
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark
},
spacer: {
flex: "1 1 100%"
},
......@@ -150,10 +150,10 @@ let FunfamMatchTableToolbar = props => {
{numSelected} selected
</Typography>
) : (
<Typography variant="h6" id="tableTitle">
Domain Matches
<Typography variant="h6" id="tableTitle">
Domain Matches
</Typography>
)}
)}
</div>
<div className={classes.spacer} />
<div className={classes.actions}>
......@@ -164,12 +164,12 @@ let FunfamMatchTableToolbar = props => {
</IconButton>
</Tooltip>
) : (
<Tooltip title="Filter list">
<IconButton aria-label="Filter list">
<FilterListIcon />
</IconButton>
</Tooltip>
)}
<Tooltip title="Filter list">
<IconButton aria-label="Filter list">
<FilterListIcon />
</IconButton>
</Tooltip>
)}
</div>
</Toolbar>
);
......@@ -187,7 +187,7 @@ FunfamMatchTableToolbar = withStyles(toolbarStyles)(FunfamMatchTableToolbar);
const styles = theme => ({
root: {
width: "100%",
marginTop: theme.spacing.unit * 1
marginTop: theme.spacing(1)
},
table: {
minWidth: 1020
......@@ -328,7 +328,7 @@ class FunfamMatchList extends React.Component {
<TableCell>
<ScanMatchFigure
width={250}
residueLength={querySequence.length}
sequenceLength={querySequence.length}
segments={n.segments}
/>
</TableCell>
......
......@@ -3,29 +3,29 @@ import PropTypes from "prop-types";
import { withStyles } from '@material-ui/core/styles';
const styles = theme => ({
root: {
flexGrow: 1,
padding: theme.spacing.unit * 2,
},
root: {
flexGrow: 1,
padding: theme.spacing(2),
},
});
class ModelStructure extends Component {
static propTypes = {
apiBase: PropTypes.string.isRequired,
submitEndpoint: PropTypes.string.isRequired,
checkEndpoint: PropTypes.string.isRequired,
resultsEndpoint: PropTypes.string.isRequired,
checkTimeout: PropTypes.number.isRequired,
checkMaxAttempts: PropTypes.number.isRequired,
queryId: PropTypes.string.isRequired,
querySequence: PropTypes.string.isRequired,
};
render() {
const { data, loaded, placeholder } = this.state;
return loaded ? data : <p>{placeholder}</p>;
}
static propTypes = {
apiBase: PropTypes.string.isRequired,
submitEndpoint: PropTypes.string.isRequired,
checkEndpoint: PropTypes.string.isRequired,
resultsEndpoint: PropTypes.string.isRequired,
checkTimeout: PropTypes.number.isRequired,
checkMaxAttempts: PropTypes.number.isRequired,
queryId: PropTypes.string.isRequired,
querySequence: PropTypes.string.isRequired,
};
render() {
const { data, loaded, placeholder } = this.state;
return loaded ? data : <p>{placeholder}</p>;
}
}
ModelStructure.propTypes = {
......
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import FormControl from '@material-ui/core/FormControl';
import Button from '@material-ui/core/Button';
import Divider from '@material-ui/core/Divider';
import CardActions from '@material-ui/core/CardActions';
const styles = theme => ({
root: {
width: '100%',
backgroundColor: theme.palette.background.paper,
},
section: {
margin: theme.spacing(3, 2),
},
helpTitle: {
fontSize: 14,
},
formControl: {
},
querySequence: {
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
},
querySequenceInput: {
fontFamily: "Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace",
fontSize: 12,
lineHeight: 1.2,
},
});
const exampleSequences = {
'A0A0Q0Y989': 'MNDFHRDTWAEVDLDAIYDNVANLRRLLPDDTHIMAVVKANAYGHGDVQVARTALEAGASRLAVAFLDEALALREKGIEAPILVLGASRPADAALAAQQRIALTVFRSDWLEEASALYSGPFPIHFHLKMDTGMGRLGVKDEEETKRIVALIERHPHFVLEGVYTHFATADEVNTDYFSYQYTRFLHMLEWLPSRPPLVHCANSAASLRFPDRTFNMVRFGIAMYGLAPSPGIKPLLPYPLKEAFSLHSRLVHVKKLQPGEKVSYGATYTAQTEEWIGTIPIGYADGWLRRLQHFHVLVDGQKAPIVGRICMDQCMIRLPGPLPVGTKVTLIGRQGDEVISIDDVARHLETINYEVPCTISYRVPRIFFRHKRIMEVRNAIGRGESSA',
'O014992': 'MSVVGIDLGFQSCYVAVARAGGIETIANEYSDRCTPACISFGPKNRSIGAAAKSQVISNAKNTVQGFKRFHGRAFSDPFVEAEKSNLAYDIVQLPTGLTGIKVTYMEEERNFTTEQVTAMLLSKLKETAESVLKKPVVDCVVSVPCFYTDAERRSVMDATQIAGLNCLRLMNETTAVALAYGIYKQDLPALEEKPRNVVFVDMGHSAYQVSVCAFNRGKLKVLATAFDTTLGGRKFDEVLVNHFCEEFGKKYKLDIKSKIRALLRLSQECEKLKKLMSANASDLPLSIECFMNDVDVSGTMNRGKFLEMCNDLLARVEPPLRSVLEQTKLKKEDIYAVEIVGGATRIPAVKEKISKFFGKELSTTLNADEAVTRGCALQCAILSPAFKVREFSITDVVPYPISLRWNSPAEEGSSDCEVFSKNHAAPFSKVLTFYRKEPFTLEAYYSSPSGFALSRSQFSVQKVLLSLMAPVQK',
};
class QuerySequenceStep extends React.Component {
constructor(props) {
super(props);
this.state = {
error: false,
errorMessage: null,
querySequenceId: "",
querySequence: "",
};
// This binding is necessary to make `this` work in the callback
this._handleTextFieldChange = this._handleTextFieldChange.bind(this);
this._handleSubmit = this._handleSubmit.bind(this);
}
setError(msg) {
this.setState({
error: true,
errorMessage: msg,
});
}
_handleTextFieldChange(e) {
const queryFasta = e.target.value;
this.setSequenceFromFasta(queryFasta);
}
_handleSubmit(ev) {
const { querySequence, querySequenceId } = this.state;
this.props.onSubmit(ev, { id: querySequenceId, seq: querySequence });
}
setSequenceFromFasta(queryFasta) {
this.setState({ queryFasta });
queryFasta = queryFasta.trim();
if (queryFasta === "") {
this.setState({ error: false, errorMessage: null, queryId: null, querySequence: null });
return;
}
const lines = queryFasta.split('\n');
const header = lines.shift();
if (!header.startsWith('>')) {
return this.setError("Expected FASTA header to start with '>'");
}
const id_re = /^>(\S+)/;
const id_match = header.match(id_re);
if (!id_match) {
return this.setError(`Failed to parse ID from FASTA header '${header}'`);
}
const id = id_match[1];
let seq = '';
lines.forEach(function (line, line_num) {
if (line.match(id_re)) {
return;
}
seq += line.trim();
});
console.log("Calling onChange with seq details", this.props, id, seq);
this.props.onChange(id, seq);
}
_handleExampleClick(exampleId) {
const exampleSeq = exampleSequences[exampleId];
console.log("_handleExampleClick", this, exampleId, exampleSeq);
this.setState(state => {
return { querySequenceId: exampleId, querySequence: exampleSeq };
});
}
getFasta() {
if (this.state.queryId && this.state.querySequence) {
return '>' + this.state.queryId + '\n' + this.state.querySequence + '\n';
}
else {
return;
}
}
renderError() {
}
render() {
const { classes } = this.props;
return (
<div className={classes.root}>
<div className={classes.section}>
<FormControl fullWidth>
<TextField
InputProps={{ classes: { input: classes.querySequenceInput } }}
className={classes.querySequence}
id="query-sequence"
label="Query Protein Sequence"
placeholder="Paste your protein sequence here"
helperText="Protein sequence should be a string of amino-acids"
multiline
autoFocus
value={this.state.querySequence}
error={this.state.error}
required
/>
</FormControl>
<FormControl>
<TextField
InputProps={{ classes: { input: classes.querySequenceInput } }}
className={classes.querySequence}
id="query-sequence-id"
label="Sequence ID"
placeholder="Add a name/id for your sequence"
helperText="Add a name/id for your sequence"
value={this.state.querySequenceId}
error={this.state.error}
required
/>
</FormControl>
</div>
<Divider variant="middle" />
<div className={classes.section}>
<CardActions>
<Button variant="contained"
onClick={() => this._handleExampleClick("A0A0Q0Y989")}>Example1</Button>
<Button variant="contained"
onClick={() => this._handleExampleClick("O014992")}>Example2</Button>
<Button variant="contained" color="secondary"
onClick={this._handleClear}>Clear</Button>
<Button variant="contained" color="primary"
onClick={this._handleSubmit}>Submit</Button>
</CardActions>
</div>
</div >);
}
}
QuerySequenceStep.propTypes = {
classes: PropTypes.object.isRequired,
onSubmit: PropTypes.func.isRequired,
queryId: PropTypes.string,
querySequence: PropTypes.string,
};
export default withStyles(styles)(QuerySequenceStep);
......@@ -7,30 +7,29 @@ const styles = theme => ({
position: "relative",
display: "block",
height: 2,
"background-color": "#ddd"
"background-color": "#ddd",
},
hsp: {
match: {
height: 7,
position: "absolute",
"background-color": "#c00"
"background-color": "#c00",
}
});
class ScanMatchFigure extends React.Component {
render() {
const { classes, width, residueLength, segments } = this.props;
const resPerPixel = residueLength / width;
const { classes, width, sequenceLength, segments } = this.props;
const pixelsPerRes = width / sequenceLength;
const style = { width };
console.log('width', width, 'sequenceLength', sequenceLength, 'pixelsPerRes', pixelsPerRes);
return (
<div className={classes.root} style={style}>
{segments.map(n => {
const { id, start, end } = n;
const w = (end - start) * resPerPixel;
const l = start * resPerPixel;
const w = (end - start) * pixelsPerRes;
const l = start * pixelsPerRes;
const spanStyle = { width: w + "%", left: l + "%" };
console.log("ScanMatchFigure.style: ", n, spanStyle);
return <span key={id} style={spanStyle} />;
return <div key={id} className={classes.match} style={spanStyle} />;
})}
</div>