Merge pull request #115 from awoods/sprint-search
Update sprint-search to current develop branch
This commit is contained in:
commit
48df6fa4ac
14 changed files with 179 additions and 61 deletions
|
@ -1,4 +1,7 @@
|
|||
# What is Vitro?
|
||||
|
||||
[](https://travis-ci.org/vivo-project/Vitro)
|
||||
|
||||
Vitro is a general-purpose web-based ontology and instance editor with customizable public browsing.
|
||||
|
||||
Vitro is an integrated ontology editor and semantic web application.
|
||||
|
@ -13,7 +16,7 @@ With Vitro, you can:
|
|||
* Search your data with Apache Solr
|
||||
|
||||
Vitro was originally developed at Cornell University, and is used as the core of the popular
|
||||
research and scholarship portal, [VIVO](http://vivoweb.org) .
|
||||
research and scholarship portal, [VIVO](https://duraspace.org/vivo/).
|
||||
|
||||
For more information, contact the [VIVO community](http://www.vivoweb.org/support/user-feedback).
|
||||
For more information, contact the [VIVO community](https://duraspace.org/vivo/resources/contact/).
|
||||
|
||||
|
|
|
@ -4,6 +4,13 @@ package edu.cornell.mannlib.vitro.webapp.config;
|
|||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.Path;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletContextEvent;
|
||||
|
@ -25,10 +32,11 @@ public class ConfigurationPropertiesSmokeTests implements
|
|||
.getLog(ConfigurationPropertiesSmokeTests.class);
|
||||
|
||||
private static final String PROPERTY_DEFAULT_NAMESPACE = "Vitro.defaultNamespace";
|
||||
private static final String PROPERTY_LANGUAGE_BUILD = "languages.addToBuild";
|
||||
private static final String PROPERTY_LANGUAGE_SELECTABLE = "languages.selectableLocales";
|
||||
private static final String PROPERTY_LANGUAGE_FORCE = "languages.forceLocale";
|
||||
private static final String PROPERTY_LANGUAGE_FILTER = "RDFService.languageFilter";
|
||||
private static final String VIVO_BUNDLE_PREFIX = "vivo_all_";
|
||||
private static final String VITRO_BUNDLE_PREFIX = "all_";
|
||||
private static final String PROPERTY_ARGON2_TIME = "argon2.time";
|
||||
private static final String PROPERTY_ARGON2_MEMORY = "argon2.memory";
|
||||
private static final String PROPERTY_ARGON2_PARALLELISM = "argon2.parallelism";
|
||||
|
@ -41,8 +49,9 @@ public class ConfigurationPropertiesSmokeTests implements
|
|||
|
||||
checkDefaultNamespace(ctx, props, ss);
|
||||
checkMultipleRPFs(ctx, props, ss);
|
||||
checkLanguages(props, ss);
|
||||
checkLanguages(ctx, props, ss);
|
||||
checkEncryptionParameters(props, ss);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -110,10 +119,7 @@ public class ConfigurationPropertiesSmokeTests implements
|
|||
* languages and force language. Shouldn't build with language unless
|
||||
* language filtering is enabled.
|
||||
*/
|
||||
private void checkLanguages(ConfigurationProperties props, StartupStatus ss) {
|
||||
String buildString = props.getProperty(PROPERTY_LANGUAGE_BUILD);
|
||||
boolean buildWithLanguages = StringUtils.isNotBlank(buildString);
|
||||
|
||||
private void checkLanguages(ServletContext ctx, ConfigurationProperties props, StartupStatus ss) {
|
||||
String selectString = props.getProperty(PROPERTY_LANGUAGE_SELECTABLE);
|
||||
boolean selectableLanguages = StringUtils.isNotBlank(selectString);
|
||||
|
||||
|
@ -121,16 +127,44 @@ public class ConfigurationPropertiesSmokeTests implements
|
|||
boolean forceLanguage = StringUtils.isNotBlank(forceString);
|
||||
|
||||
String filterString = props.getProperty(PROPERTY_LANGUAGE_FILTER,
|
||||
"true");
|
||||
"false");
|
||||
boolean languageFilter = Boolean.valueOf(filterString);
|
||||
String i18nDirPath = ctx.getRealPath("/i18n");
|
||||
|
||||
if (selectableLanguages && !buildWithLanguages) {
|
||||
ss.warning(this, String.format("Problem with Language setup - "
|
||||
+ "runtime.properties specifies a "
|
||||
+ "list of selectable languages (%s = %s), but "
|
||||
+ "build.properties did not include any languages with %s",
|
||||
PROPERTY_LANGUAGE_SELECTABLE, selectString,
|
||||
PROPERTY_LANGUAGE_BUILD));
|
||||
if (i18nDirPath == null) {
|
||||
throw new IllegalStateException(
|
||||
"Application does not have an /i18n directory.");
|
||||
}
|
||||
|
||||
List<String> i18nNames = null;
|
||||
|
||||
i18nNames = geti18nNames(i18nDirPath);
|
||||
|
||||
log.debug("i18nNames: " + i18nNames);
|
||||
|
||||
if (i18nNames.isEmpty()) {
|
||||
ss.fatal(this, "The application found no files in '"
|
||||
+ i18nDirPath
|
||||
+ "' .");
|
||||
}
|
||||
else {
|
||||
ss.info(this, "Base language files loaded: " + i18nNames);
|
||||
}
|
||||
|
||||
/* Make sure language files exist for values in the selectableLocales propery.
|
||||
The prefixes of vitro and vivo are hard coded into the app,
|
||||
so we can assume the bundle names must have the same file format */
|
||||
if (selectableLanguages) {
|
||||
List<String> selectableLanguagesList = Arrays.asList(selectString.split("\\s*,\\s*"));
|
||||
for (String language : selectableLanguagesList) {
|
||||
String vivoBundle = VIVO_BUNDLE_PREFIX + language + ".properties";
|
||||
String vitroBundle = VITRO_BUNDLE_PREFIX + language + ".properties";
|
||||
if (!i18nNames.contains(vivoBundle) && !i18nNames.contains(vitroBundle)) {
|
||||
ss.warning(this, language + " was found in the value for "
|
||||
+ PROPERTY_LANGUAGE_SELECTABLE + " but no corresponding "
|
||||
+ "language file was found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectableLanguages && forceLanguage) {
|
||||
|
@ -142,17 +176,31 @@ public class ConfigurationPropertiesSmokeTests implements
|
|||
PROPERTY_LANGUAGE_SELECTABLE, selectString));
|
||||
}
|
||||
|
||||
if (buildWithLanguages && !languageFilter) {
|
||||
if (selectableLanguages && !languageFilter) {
|
||||
ss.warning(this, String.format("Problem with Language setup - "
|
||||
+ "build.properties includes one or more additional "
|
||||
+ "languages.selectableLocales in runtime.properties "
|
||||
+ "includes one or more additional "
|
||||
+ "languages (%s = %s), but runtime.properties has "
|
||||
+ "disabled language filtering (%s = %s). This will "
|
||||
+ "likely result in a mix of languages in the "
|
||||
+ "application.", PROPERTY_LANGUAGE_BUILD, buildString,
|
||||
+ "application.", PROPERTY_LANGUAGE_SELECTABLE, selectString,
|
||||
PROPERTY_LANGUAGE_FILTER, filterString));
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a list of the names of available language files. */
|
||||
private List<String> geti18nNames(String i18nBaseDirPath) {
|
||||
try {
|
||||
return Files.walk(Paths.get(i18nBaseDirPath))
|
||||
.filter(Files::isRegularFile)
|
||||
.map(Path::getFileName)
|
||||
.map(p -> {return p.toString();})
|
||||
.collect(Collectors.toList());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to find language files", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail if there are no config properties for the Argon2 encryption.
|
||||
*/
|
||||
|
@ -170,6 +218,7 @@ public class ConfigurationPropertiesSmokeTests implements
|
|||
ss.fatal(this, "runtime.properties does not contain a value for '"
|
||||
+ name + "'");
|
||||
return;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import javax.servlet.ServletException;
|
|||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import edu.cornell.mannlib.vitro.webapp.dao.jena.MenuDaoJena;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
@ -461,7 +462,7 @@ public class FreemarkerHttpServlet extends VitroHttpServlet {
|
|||
|
||||
protected MainMenu getDisplayModelMenu(VitroRequest vreq){
|
||||
String url = vreq.getRequestURI().substring(vreq.getContextPath().length());
|
||||
return vreq.getWebappDaoFactory().getMenuDao().getMainMenu(url);
|
||||
return vreq.getWebappDaoFactory().getMenuDao().getMainMenu(vreq, url);
|
||||
}
|
||||
|
||||
// NIHVIVO-3307: we need this here instead of FreemarkerConfiguration.java so that updates to
|
||||
|
|
|
@ -3,10 +3,19 @@ package edu.cornell.mannlib.vitro.webapp.dao;
|
|||
|
||||
import edu.cornell.mannlib.vitro.webapp.web.templatemodels.menu.MainMenu;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
|
||||
public interface MenuDao {
|
||||
/**
|
||||
* @param url - url of request for setting active menu items. This should start with a / and not have the context path.
|
||||
* These values will be checked against urlMapping. ex. /people or /home
|
||||
*/
|
||||
public MainMenu getMainMenu( String url);
|
||||
|
||||
/**
|
||||
* @param req - the ServletRequest for the current request
|
||||
* @param url - url of request for setting active menu items. This should start with a / and not have the context path.
|
||||
* These values will be checked against urlMapping. ex. /people or /home
|
||||
*/
|
||||
public MainMenu getMainMenu(ServletRequest req, String url);
|
||||
}
|
||||
|
|
|
@ -5,9 +5,11 @@ package edu.cornell.mannlib.vitro.webapp.dao.jena;
|
|||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import edu.cornell.mannlib.vitro.webapp.rdfservice.filter.LanguageFilteringUtils;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.apache.jena.ontology.OntModel;
|
||||
import org.apache.jena.query.Query;
|
||||
import org.apache.jena.query.QueryExecution;
|
||||
import org.apache.jena.query.QueryExecutionFactory;
|
||||
|
@ -23,6 +25,8 @@ import edu.cornell.mannlib.vitro.webapp.dao.MenuDao;
|
|||
import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary;
|
||||
import edu.cornell.mannlib.vitro.webapp.web.templatemodels.menu.MainMenu;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
|
||||
public class MenuDaoJena extends JenaBaseDao implements MenuDao {
|
||||
|
||||
private static final Log log = LogFactory.getLog(MenuDaoJena.class);
|
||||
|
@ -68,6 +72,10 @@ public class MenuDaoJena extends JenaBaseDao implements MenuDao {
|
|||
return getMenu( getOntModelSelector().getDisplayModel(), url );
|
||||
}
|
||||
|
||||
public MainMenu getMainMenu( ServletRequest req, String url ) {
|
||||
OntModel displayModel = LanguageFilteringUtils.wrapOntModelInALanguageFilter(getOntModelSelector().getDisplayModel(), req );
|
||||
return getMenu(displayModel, url) ;
|
||||
}
|
||||
|
||||
protected MainMenu getMenu(Model displayModel, String url){
|
||||
//setup query parameters
|
||||
|
|
|
@ -365,10 +365,8 @@ public class WebappDaoFactoryJena implements WebappDaoFactory {
|
|||
@Override
|
||||
public ObjectPropertyStatementDao getObjectPropertyStatementDao() {
|
||||
if( objectPropertyStatementDao == null )
|
||||
// TODO supply a valid RDFService as the first argument if we keep this
|
||||
// implementation
|
||||
objectPropertyStatementDao = new ObjectPropertyStatementDaoJena(
|
||||
null, dwf, this);
|
||||
rdfService, dwf, this);
|
||||
return objectPropertyStatementDao;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
|||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.http.Consts;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.auth.AuthScope;
|
||||
import org.apache.http.auth.AuthenticationException;
|
||||
|
@ -495,8 +496,8 @@ public class RDFServiceSparql extends RDFServiceImpl implements RDFService {
|
|||
protected void executeUpdate(String updateString) throws RDFServiceException {
|
||||
try {
|
||||
HttpPost meth = new HttpPost(updateEndpointURI);
|
||||
meth.addHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
meth.setEntity(new UrlEncodedFormEntity(Arrays.asList(new BasicNameValuePair("update", updateString))));
|
||||
meth.addHeader("Content-Type", "application/x-www-form-urlencoded; charset="+Consts.UTF_8);
|
||||
meth.setEntity(new UrlEncodedFormEntity(Arrays.asList(new BasicNameValuePair("update", updateString)),Consts.UTF_8));
|
||||
HttpContext context = getContext(meth);
|
||||
HttpResponse response = context != null ? httpClient.execute(meth, context) : httpClient.execute(meth);
|
||||
try {
|
||||
|
@ -510,6 +511,7 @@ public class RDFServiceSparql extends RDFServiceImpl implements RDFService {
|
|||
EntityUtils.consume(response.getEntity());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("update string: \n" + updateString);
|
||||
throw new RDFServiceException("Unable to perform change set update", e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -176,8 +176,6 @@ public class UpdateDocumentWorkUnit implements Runnable {
|
|||
classGroupUris.add(classGroupUri);
|
||||
}
|
||||
|
||||
addToAlltext(doc, clz.getName());
|
||||
|
||||
Float boost = clz.getSearchBoost();
|
||||
if (boost != null) {
|
||||
doc.setDocumentBoost(doc.getDocumentBoost() + boost);
|
||||
|
|
|
@ -197,7 +197,12 @@ public class CustomListViewConfigFile {
|
|||
*/
|
||||
NodeList doomed = element.getElementsByTagName(tagName);
|
||||
for (int i = doomed.getLength() - 1; i >= 0; i--) {
|
||||
element.removeChild(doomed.item(i));
|
||||
/*
|
||||
* As the node to be removed may be at an arbitrary depth in the DOM tree,
|
||||
* we need to get the parent of the node that we are trying to remove,
|
||||
* and then remove the child from there.
|
||||
*/
|
||||
doomed.item(i).getParentNode().removeChild(doomed.item(i));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,15 +10,18 @@ import static org.junit.Assert.assertEquals;
|
|||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.log4j.Level;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
|
@ -119,16 +122,33 @@ public class AuthenticateTest extends AbstractTestClass {
|
|||
private static final String NO_USER = "";
|
||||
private static final String NO_MSG = "";
|
||||
|
||||
/**
|
||||
* Due to increased overhead of password hashing, create a list of UserAccount objects once
|
||||
* when the class is cloaded.
|
||||
*
|
||||
* These prepared user accounts will then be (re)used to populate the authenticator stubs prior to each test
|
||||
*/
|
||||
private static List<UserAccount> userAccounts = new ArrayList<>();
|
||||
|
||||
@BeforeClass
|
||||
public static void prepareUserAccounts() {
|
||||
userAccounts.add(createUserFromUserInfo(NEW_DBA));
|
||||
userAccounts.add(createUserFromUserInfo(OLD_DBA));
|
||||
userAccounts.add(createUserFromUserInfo(OLD_SELF));
|
||||
userAccounts.add(createUserFromUserInfo(OLD_STRANGER));
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
I18nStub.setup();
|
||||
|
||||
authenticatorFactory = new AuthenticatorStub.Factory();
|
||||
authenticator = authenticatorFactory.getInstance(request);
|
||||
authenticator.addUser(createUserFromUserInfo(NEW_DBA));
|
||||
authenticator.addUser(createUserFromUserInfo(OLD_DBA));
|
||||
authenticator.addUser(createUserFromUserInfo(OLD_SELF));
|
||||
authenticator.addUser(createUserFromUserInfo(OLD_STRANGER));
|
||||
|
||||
for (UserAccount account : userAccounts) {
|
||||
authenticator.addUser(account);
|
||||
}
|
||||
|
||||
authenticator.setAssociatedUri(OLD_SELF.username,
|
||||
"old_self_associated_uri");
|
||||
|
||||
|
@ -143,10 +163,9 @@ public class AuthenticateTest extends AbstractTestClass {
|
|||
|
||||
userAccountsDao = new UserAccountsDaoStub();
|
||||
userAccountsDao.addPermissionSet(adminPermissionSet);
|
||||
userAccountsDao.addUser(createUserFromUserInfo(NEW_DBA));
|
||||
userAccountsDao.addUser(createUserFromUserInfo(OLD_DBA));
|
||||
userAccountsDao.addUser(createUserFromUserInfo(OLD_SELF));
|
||||
userAccountsDao.addUser(createUserFromUserInfo(OLD_STRANGER));
|
||||
for (UserAccount account : userAccounts) {
|
||||
userAccountsDao.addUser(account);
|
||||
}
|
||||
|
||||
individualDao = new IndividualDaoStub();
|
||||
|
||||
|
@ -186,7 +205,7 @@ public class AuthenticateTest extends AbstractTestClass {
|
|||
new HasPermissionFactory(servletContext));
|
||||
}
|
||||
|
||||
private UserAccount createUserFromUserInfo(UserInfo userInfo) {
|
||||
private static UserAccount createUserFromUserInfo(UserInfo userInfo) {
|
||||
UserAccount user = new UserAccount();
|
||||
user.setEmailAddress(userInfo.username);
|
||||
user.setUri(userInfo.uri);
|
||||
|
|
|
@ -5,6 +5,8 @@ package stubs.edu.cornell.mannlib.vitro.webapp.dao;
|
|||
import edu.cornell.mannlib.vitro.webapp.dao.MenuDao;
|
||||
import edu.cornell.mannlib.vitro.webapp.web.templatemodels.menu.MainMenu;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
|
||||
/**
|
||||
* A minimal implementation of the MenuDao.
|
||||
*
|
||||
|
@ -35,6 +37,11 @@ public class MenuDaoStub implements MenuDao {
|
|||
return this.mainMenu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MainMenu getMainMenu(ServletRequest req, String url) {
|
||||
return this.mainMenu;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Un-implemented methods
|
||||
// ----------------------------------------------------------------------
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<version>3.8.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
|
|
53
pom.xml
53
pom.xml
|
@ -193,6 +193,30 @@
|
|||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
<profile>
|
||||
<!-- JDK 8 profile for the Error Prone plugin -->
|
||||
<id>jdk8</id>
|
||||
<activation>
|
||||
<jdk>1.8</jdk>
|
||||
</activation>
|
||||
<properties>
|
||||
<javac.version>9+181-r4173-1</javac.version>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<fork>true</fork>
|
||||
<compilerArgs combine.children="append">
|
||||
<arg>-J-Xbootclasspath/p:${settings.localRepository}/com/google/errorprone/javac/${javac.version}/javac-${javac.version}.jar</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<build>
|
||||
|
@ -204,23 +228,18 @@
|
|||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
<compilerId>javac-with-errorprone</compilerId>
|
||||
<forceJavacCompilerUse>true</forceJavacCompilerUse>
|
||||
<compilerArgs>
|
||||
<arg>-XDcompilePolicy=simple</arg>
|
||||
<arg>-Xplugin:ErrorProne</arg>
|
||||
</compilerArgs>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>com.google.errorprone</groupId>
|
||||
<artifactId>error_prone_core</artifactId>
|
||||
<version>2.3.2</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.plexus</groupId>
|
||||
<artifactId>plexus-compiler-javac-errorprone</artifactId>
|
||||
<version>2.8</version>
|
||||
</dependency>
|
||||
<!-- override plexus-compiler-javac-errorprone's dependency on
|
||||
Error Prone with the latest version -->
|
||||
<dependency>
|
||||
<groupId>com.google.errorprone</groupId>
|
||||
<artifactId>error_prone_core</artifactId>
|
||||
<version>2.1.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
|
@ -267,7 +286,7 @@
|
|||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<version>3.8.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<hr class="hidden" />
|
||||
|
||||
<div id="contentwrap">
|
||||
<#include "flash.html">
|
||||
<#include "flash.ftl">
|
||||
|
||||
<div id="content">
|
||||
${body}
|
||||
|
|
Loading…
Add table
Reference in a new issue