Commit a47c4042 authored by Matija Obreza's avatar Matija Obreza

FIX #11 Improved material request handling

parent 2eb4c341
......@@ -544,7 +544,7 @@
<plugin>
<inherited>true</inherited>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.0</version>
<version>3.1</version>
<configuration>
<source>${jdk.source}</source>
<target>${jdk.target}</target>
......@@ -553,6 +553,16 @@
<showWarnings>true</showWarnings>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.17</version>
<configuration>
<forkMode>once</forkMode>
<argLine>-Xms512m -Xmx1024m</argLine>
<testFailureIgnore>false</testFailureIgnore>
</configuration>
</plugin>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
......
......@@ -31,8 +31,14 @@ import org.genesys2.server.model.VersionedAuditedModel;
@Entity
@Table(name = "request")
public class MaterialRequest extends VersionedAuditedModel {
public static final int NOTVALIDATED = 0;
public static final int VALIDATED = 1;
public static final int DISPATCHED = 2;
@Column(length = 36, unique=true)
@Column
private int state = NOTVALIDATED;
@Column(length = 36, unique = true)
private String uuid;
@Column(length = 200, nullable = false)
......@@ -42,7 +48,7 @@ public class MaterialRequest extends VersionedAuditedModel {
@Lob
private String body;
@Column(length = 32, nullable = false)
@Column(length = 32, nullable = true)
private String pid;
@PrePersist
......@@ -55,6 +61,14 @@ public class MaterialRequest extends VersionedAuditedModel {
public MaterialRequest() {
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public String getUuid() {
return uuid;
}
......@@ -85,7 +99,8 @@ public class MaterialRequest extends VersionedAuditedModel {
}
/**
* @param pid EasySMTA PID
* @param pid
* EasySMTA PID
*/
public void setPid(String pid) {
this.pid = pid;
......
/**
* Copyright 2014 Global Crop Diversity Trust
*
* Licensed 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.
**/
package org.genesys2.server.model.genesys;
import java.text.MessageFormat;
import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.PrePersist;
import javax.persistence.Table;
import org.apache.commons.lang.math.RandomUtils;
import org.genesys2.server.model.VersionedAuditedModel;
/**
* {@link MaterialRequest} is broken down into individual requests to
* institutes.
*
* @author matijaobreza
*
*/
@Entity
@Table(name = "requestsub")
public class MaterialSubRequest extends VersionedAuditedModel {
public static final int NOTCONFIRMED = 0;
public static final int CONFIRMED = 1;
@Column
private int state = NOTCONFIRMED;
@Column(length = 36, unique = true)
private String uuid;
@Column(length = 8, nullable = false)
private String instCode;
@Column(length = 200, nullable = true)
private String instEmail;
@Column(length = 100000)
@Lob
private String body;
@ManyToOne(cascade = {}, optional = false)
private MaterialRequest sourceRequest;
@PrePersist
void prepersist() {
if (this.uuid == null)
this.uuid = UUID.nameUUIDFromBytes(
("genesys:request:" + sourceRequest.getUuid() + ":" + instCode + ":" + System.currentTimeMillis() + ":" + RandomUtils.nextInt(100))
.getBytes()).toString();
}
public MaterialSubRequest() {
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String getInstCode() {
return instCode;
}
public void setInstCode(String instCode) {
this.instCode = instCode;
}
public String getInstEmail() {
return instEmail;
}
public void setInstEmail(String instEmail) {
this.instEmail = instEmail;
}
public MaterialRequest getSourceRequest() {
return sourceRequest;
}
public void setSourceRequest(MaterialRequest sourceRequest) {
this.sourceRequest = sourceRequest;
}
@Override
public String toString() {
return MessageFormat.format("SubRequest uuid={0} inst={1} body={2}", uuid, instCode, body);
}
}
......@@ -108,10 +108,20 @@ public interface AccessionRepository extends JpaRepository<Accession, Long> {
@Query("select count(a.id) from Accession a where a.id in ( ?1 ) and a.availability = true")
long countAvailable(Set<Long> accessionIds);
@Query("select a.id from Accession a where a.id in ( ?1 ) and a.availability = true")
Set<Long> filterAvailable(Set<Long> accessionIds);
@Modifying
@Query("update Accession a set a.inSvalbard = true where a in ?1")
void setInSvalbard(List<Accession> matching);
Page<Accession> findByInstituteAndTaxSpecies(FaoInstitute institute, long taxSpecies, Pageable pageable);
@Query("select distinct a.institute from Accession a where a.id in ( ?1 )")
List<FaoInstitute> findDistinctInstitutesFor(Set<Long> accessionIds);
@Query("select distinct a.id from Accession a where a.institute = ?1 and a.id in ( ?2 )")
Set<Long> findByInstituteAndIds(FaoInstitute institute, Set<Long> accessionIds);
}
/**
* Copyright 2014 Global Crop Diversity Trust
*
* Licensed 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.
**/
package org.genesys2.server.persistence.domain;
import org.genesys2.server.model.genesys.MaterialSubRequest;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MaterialSubRequestRepository extends JpaRepository<MaterialSubRequest, Long> {
MaterialSubRequest findByUuid(String uuid);
}
......@@ -123,6 +123,7 @@ public interface GenesysService {
void saveSvalbards(List<SvalbardData> svalbards);
long countAvailable(Set<Long> accessionIds);
Set<Long> filterAvailable(Set<Long> accessionIds);
void saveAccession(Accession... accession);
......@@ -171,4 +172,8 @@ public interface GenesysService {
*/
Accession getAccession(String instCode, String acceNumb);
List<FaoInstitute> findHoldingInstitutes(Set<Long> accessionIds);
Set<Long> listAccessions(FaoInstitute holdingInstitute, Set<Long> accessionIds);
}
......@@ -16,17 +16,81 @@
package org.genesys2.server.service;
import java.io.IOException;
import java.util.Set;
import org.genesys2.server.model.genesys.MaterialRequest;
import org.genesys2.server.service.impl.EasySMTAConnector.EasySMTAUserData;
import org.genesys2.server.model.genesys.MaterialSubRequest;
import org.genesys2.server.service.TokenVerificationService.NoSuchVerificationTokenException;
public interface RequestService {
MaterialRequest sendRequest(EasySMTAUserData pid, Set<Long> accessionIds);
MaterialRequest sendRequest(RequestInfo requestInfo, Set<Long> accessionIds) throws RequestException;
MaterialRequest createRequest(EasySMTAUserData pid, Set<Long> accessionIds);
MaterialRequest createRequest(RequestInfo requestInfo, EasySMTA.EasySMTAUserData pid, Set<Long> accessionIds) throws RequestException;
MaterialRequest getRequest(String uuid);
MaterialRequest validateClientRequest(String tokenUuid, String key) throws RequestException, NoSuchVerificationTokenException;
void relayRequest(MaterialSubRequest materialSubRequest);
MaterialSubRequest validateReceipt(String tokenUuid, String key) throws NoSuchVerificationTokenException;
static class RequestException extends Exception {
public RequestException(String message) {
super(message);
}
public RequestException(String message, IOException e) {
super(message, e);
}
}
static class NoPidException extends RuntimeException {
public NoPidException(String message) {
super(message);
}
}
public static class RequestInfo {
private String email;
private int purposeType;
private boolean preacceptSMTA;
private String notes;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public int getPurposeType() {
return purposeType;
}
public void setPurposeType(int purposeType) {
this.purposeType = purposeType;
}
public boolean isPreacceptSMTA() {
return preacceptSMTA;
}
public void setPreacceptSMTA(boolean preacceptSMTA) {
this.preacceptSMTA = preacceptSMTA;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
}
}
......@@ -24,6 +24,7 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
......@@ -197,6 +198,16 @@ public class GenesysServiceImpl implements GenesysService, TraitService, Dataset
return accessionRepository.findByInstitute(faoInstitute, pageable);
}
@Override
public List<FaoInstitute> findHoldingInstitutes(Set<Long> accessionIds) {
return accessionRepository.findDistinctInstitutesFor(accessionIds);
}
@Override
public Set<Long> listAccessions(FaoInstitute institute, Set<Long> accessionIds) {
return accessionRepository.findByInstituteAndIds(institute, accessionIds);
}
@Override
public Page<Accession> listAccessions(Collection<Long> accessionIds, Pageable pageable) {
if (accessionIds == null || accessionIds.size() == 0) {
......@@ -580,6 +591,13 @@ public class GenesysServiceImpl implements GenesysService, TraitService, Dataset
return accessionRepository.countAvailable(accessionIds);
}
@Override
public Set<Long> filterAvailable(Set<Long> accessionIds) {
if (accessionIds == null || accessionIds.size() == 0)
return Collections.emptySet();
return accessionRepository.filterAvailable(accessionIds);
}
/**
* Returns datasets to which current user has 'WRITE'
*/
......
......@@ -16,6 +16,8 @@
package org.genesys2.server.servlet.controller;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang.StringUtils;
......@@ -23,8 +25,9 @@ import org.genesys2.server.model.impl.User;
import org.genesys2.server.service.ContentService;
import org.genesys2.server.service.GenesysService;
import org.genesys2.server.service.RequestService;
import org.genesys2.server.service.impl.EasySMTAConnector;
import org.genesys2.server.service.impl.EasySMTAConnector.EasySMTAUserData;
import org.genesys2.server.service.RequestService.RequestException;
import org.genesys2.server.service.RequestService.RequestInfo;
import org.genesys2.server.service.TokenVerificationService.NoSuchVerificationTokenException;
import org.genesys2.spring.SecurityContextUtil;
import org.genesys2.util.ReCaptchaUtil;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -34,6 +37,7 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
......@@ -57,9 +61,6 @@ public class RequestController extends BaseController {
@Autowired
private ContentService contentService;
@Autowired
private EasySMTAConnector pidChecker;
@Value("${captcha.privateKey}")
private String captchaPrivateKey;
......@@ -91,20 +92,26 @@ public class RequestController extends BaseController {
User currentUser = SecurityContextUtil.getCurrentUser();
model.addAttribute("blurp", contentService.getGlobalArticle("request-personal", getLocale()));
model.addAttribute("captchaPublicKey", captchaPublicKey);
if (currentUser != null)
if (!model.containsAttribute("requestEmail") && currentUser != null)
model.addAttribute("requestEmail", currentUser.getEmail());
return "/request/personal";
}
@RequestMapping(method = RequestMethod.POST, value = "/submit")
public String submit(ModelMap model, HttpServletRequest req, @RequestParam(value = "email", defaultValue = "", required = true) String emailAddress,
public String submit(ModelMap model, HttpServletRequest req, @RequestParam(value = "email", defaultValue = "", required = true) String requestEmail,
@RequestParam(value = "recaptcha_challenge_field", required = false) String challenge,
@RequestParam(value = "recaptcha_response_field", required = false) String response) {
emailAddress = emailAddress.trim();
User currentUser = SecurityContextUtil.getCurrentUser();
@RequestParam(value = "recaptcha_response_field", required = false) String response, @RequestParam(value = "notes") String notes,
@RequestParam(value = "purpose") int purposeType, @RequestParam(value = "smta", required = false) Boolean preacceptSMTA) {
if (StringUtils.isBlank(emailAddress)) {
_logger.warn("No email address was specified for request. Stopping here.");
User currentUser = SecurityContextUtil.getCurrentUser();
model.addAttribute("notes", notes);
model.addAttribute("requestEmail", requestEmail);
InternetAddress internetAddress;
try {
internetAddress = new InternetAddress(requestEmail, true);
} catch (AddressException e1) {
_logger.warn("Email not valid. " + e1.getMessage());
return start(model);
}
......@@ -114,15 +121,29 @@ public class RequestController extends BaseController {
return start(model);
}
_logger.info("Checking for ITPGRFA PID account for " + emailAddress);
EasySMTAUserData pid = pidChecker.getUserData(emailAddress);
if (preacceptSMTA == null || preacceptSMTA == false) {
model.addAttribute("smta", false);
return start(model);
}
if (pid == null) {
_logger.warn("No such user in ITPGRFA system");
RequestInfo requestInfo = new RequestInfo();
requestInfo.setEmail(internetAddress.toString());
requestInfo.setPreacceptSMTA(preacceptSMTA);
requestInfo.setPurposeType(purposeType);
requestInfo.setNotes(StringUtils.defaultIfBlank(notes, null));
} else {
// TODO send email to registered email address, wait for response
requestService.sendRequest(pid, selectionBean.copy());
if (StringUtils.isBlank(requestInfo.getEmail())) {
_logger.warn("No email address was specified for request. Stopping here.");
return start(model);
}
try {
// send email to registered email address, wait for
// response
requestService.sendRequest(requestInfo, selectionBean.copy());
} catch (RequestException e) {
_logger.warn(e.getMessage());
return start(model);
}
// Whatever the response is, we render the same message
......@@ -130,4 +151,67 @@ public class RequestController extends BaseController {
return "/request/received";
}
@RequestMapping(value = "/{tokenUuid:.+}/validate", method = RequestMethod.GET)
public String validateClientRequest(ModelMap model, @PathVariable("tokenUuid") String tokenUuid, @RequestParam(value = "key", required = false) String key) {
if (!model.containsAttribute("blurp"))
model.addAttribute("blurp", contentService.getGlobalArticle("request-validate", getLocale()));
model.addAttribute("tokenUuid", tokenUuid);
model.addAttribute("key", key);
return "/request/validaterequest";
}
@RequestMapping(value = "/{tokenUuid:.+}/validate", method = RequestMethod.POST)
public String validateClientRequest2(ModelMap model, @PathVariable("tokenUuid") String tokenUuid, @RequestParam(value = "key", required = true) String key) {
try {
requestService.validateClientRequest(tokenUuid, key);
} catch (RequestService.NoPidException e) {
_logger.error(e.getMessage());
model.addAttribute("blurp", contentService.getGlobalArticle("request-validate-no-pid", getLocale()));
return validateClientRequest(model, tokenUuid, key);
} catch (RequestException e) {
_logger.error(e.getMessage(), e);
} catch (NoSuchVerificationTokenException e) {
_logger.error("Verification token is not valid");
model.addAttribute("error", e);
return validateClientRequest(model, tokenUuid, null);
}
return "redirect:/content/request-validated";
}
/**
* Genebank confirms receipt of request
*
* @param model
* @param tokenUuid
* @return
*/
@RequestMapping(value = "/{tokenUuid:.+}/confirm", method = RequestMethod.GET)
public String confirmReceipt(ModelMap model, @PathVariable("tokenUuid") String tokenUuid, @RequestParam(value = "key", required = false) String key) {
if (!model.containsAttribute("blurp"))
model.addAttribute("blurp", contentService.getGlobalArticle("request-confirm-receipt", getLocale()));
model.addAttribute("tokenUuid", tokenUuid);
model.addAttribute("key", key);
return "/request/confirmrequest";
}
@RequestMapping(value = "/{tokenUuid:.+}/confirm", method = RequestMethod.POST)
public String confirmReceipt2(ModelMap model, @PathVariable("tokenUuid") String tokenUuid, @RequestParam(value = "key", required = true) String key) {
try {
requestService.validateReceipt(tokenUuid, key);
} catch (NoSuchVerificationTokenException e) {
_logger.error("Verification token is not valid");
model.addAttribute("error", e);
return confirmReceipt(model, tokenUuid, null);
}
return "redirect:/content/request-confirmed";
}
}
......@@ -128,7 +128,7 @@ public class WiewsController extends BaseController {
@RequestMapping("/{wiewsCode}/update")
public String update(ModelMap model, @PathVariable(value = "wiewsCode") String wiewsCode, @RequestParam("blurp") String blurp,
@RequestParam("gaTracker") String gaTracker, @RequestParam("uniqueAcceNumbs") boolean uniqueAcceNumbs) {
@RequestParam("gaTracker") String gaTracker, @RequestParam("mailto") String mailto, @RequestParam("uniqueAcceNumbs") boolean uniqueAcceNumbs) {
_logger.debug("Updating institite " + wiewsCode);
FaoInstitute faoInstitute = instituteService.getInstitute(wiewsCode);
if (faoInstitute == null) {
......@@ -138,6 +138,7 @@ public class WiewsController extends BaseController {
instituteService.updateBlurp(faoInstitute, blurp, getLocale());
Map<String, String> settings = new HashMap<String, String>();
settings.put("googleAnalytics.tracker", gaTracker);
settings.put("requests.mailto", mailto);
instituteService.updateSettings(faoInstitute, settings);
instituteService.setUniqueAcceNumbs(faoInstitute, uniqueAcceNumbs);
......
......@@ -156,6 +156,7 @@ faoInstitute.url=Web link
faoInstitute.member-of-organizations-and-networks=Organizations and Networks:
faoInstitute.uniqueAcceNumbs.true=Each accession number is unique within this institute.
faoInstitute.uniqueAcceNumbs.false=The same accession number may be used in separate collections in this institute.
faoInstitute.requests.mailto=Email address for material requests:
view.accessions=View accessions...
view.datasets=View datasets...
......@@ -400,7 +401,19 @@ request.page.title=Requesting material from holding institutes
request.total-vs-available=Out of {0} accessions listed, {1} are known to be available for distribution.
request.start-request=Request available germplasm
request.your-email=Your E-mail address:
request.accept-smta=SMTA/MTA acceptance:
request.smta-will-accept=I will accept the terms and conditions of SMTA/MTA
request.smta-will-not-accept=I will NOT accept the terms and conditions of SMTA/MTA
request.smta-not-accepted=You did not indicate that you will accept the terms and conditions of SMTA/MTA. Your request for material will not be processed.
request.purpose=Specify use of material:
request.purpose.0=Other (please elaborate in Notes field)
request.purpose.1=Research for food and agriculture