Thu, 14 May 2020 22:48:01 +0200
projects can now be added and updated
--- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java Wed May 13 21:46:26 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java Thu May 14 22:48:01 2020 +0200 @@ -39,6 +39,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; +import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.sql.Connection; @@ -225,6 +226,18 @@ } /** + * @param req the servlet request object + * @param location the location where to redirect + * @see Constants#REQ_ATTR_REDIRECT_LOCATION + */ + public void setRedirectLocation(HttpServletRequest req, String location) { + if (location.startsWith("./")) { + location = location.replaceFirst("\\./", Functions.baseHref(req)); + } + req.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, location); + } + + /** * Specifies the name of an additional stylesheet used by the module. * <p> * Setting an additional stylesheet is optional, but quite common for HTML @@ -240,6 +253,30 @@ req.setAttribute(Constants.REQ_ATTR_STYLESHEET, Functions.enforceExt(stylesheet, ".css")); } + /** + * Obtains a request parameter of the specified type. + * The specified type must have a single-argument constructor accepting a string to perform conversion. + * The constructor of the specified type may throw an exception on conversion failures. + * + * @param req the servlet request object + * @param clazz the class object of the expected type + * @param name the name of the parameter + * @param <T> the expected type + * @return the parameter value or an empty optional, if no parameter with the specified name was found + */ + public<T> Optional<T> getParameter(HttpServletRequest req, Class<T> clazz, String name) { + final String paramValue = req.getParameter(name); + if (paramValue == null) return Optional.empty(); + if (clazz.equals(String.class)) return Optional.of((T)paramValue); + try { + final Constructor<T> ctor = clazz.getConstructor(String.class); + return Optional.of(ctor.newInstance(paramValue)); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + + } + private void forwardToFullView(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { @@ -287,6 +324,7 @@ } // set some internal request attributes + req.setAttribute(Constants.REQ_ATTR_BASE_HREF, Functions.baseHref(req)); req.setAttribute(Constants.REQ_ATTR_PATH, Functions.fullPath(req)); Optional.ofNullable(moduleInfo).ifPresent((proxy) -> req.setAttribute(Constants.REQ_ATTR_MODULE_INFO, proxy));
--- a/src/main/java/de/uapcore/lightpit/Constants.java Wed May 13 21:46:26 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/Constants.java Thu May 14 22:48:01 2020 +0200 @@ -40,6 +40,7 @@ public static final String DYN_FRAGMENT_PATH_PREFIX = "/WEB-INF/dynamic_fragments/"; + public static final String DYN_FRAGMENT_COMMIT_SUCCESSFUL = "commit-successful"; /** * Name for the context parameter specifying the available languages. @@ -77,6 +78,11 @@ public static final String REQ_ATTR_SUB_MENU = fqn(AbstractLightPITServlet.class, "subMenu"); /** + * Key for the request attribute containing the base href. + */ + public static final String REQ_ATTR_BASE_HREF = fqn(AbstractLightPITServlet.class, "base_href"); + + /** * Key for the request attribute containing the full path information (servlet path + path info). */ public static final String REQ_ATTR_PATH = fqn(AbstractLightPITServlet.class, "path"); @@ -91,6 +97,11 @@ */ public static final String REQ_ATTR_STYLESHEET = fqn(AbstractLightPITServlet.class, "extraCss"); + /** + * Key for a location the page shall redirect to. + * Will be used in a meta element. + */ + public static final String REQ_ATTR_REDIRECT_LOCATION = fqn(AbstractLightPITServlet.class, "redirectLocation"); /** * Key for the current language selection within the session.
--- a/src/main/java/de/uapcore/lightpit/Functions.java Wed May 13 21:46:26 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/Functions.java Thu May 14 22:48:01 2020 +0200 @@ -66,15 +66,12 @@ return fqn(clazz.getName(), name); } - public static String fullPath(LightPITModule module, RequestMapping mapping) { - StringBuilder sb = new StringBuilder(); - sb.append(module.modulePath()); - sb.append('/'); - if (!mapping.requestPath().isEmpty()) { - sb.append(mapping.requestPath().isEmpty()); - sb.append('/'); - } - return sb.toString(); + public static String baseHref(HttpServletRequest req) { + return String.format("%s://%s:%d%s/", + req.getScheme(), + req.getServerName(), + req.getServerPort(), + req.getContextPath()); } public static String fullPath(HttpServletRequest req) {
--- a/src/main/java/de/uapcore/lightpit/dao/AbstractDao.java Wed May 13 21:46:26 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,96 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 2018 Mike Becker. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -package de.uapcore.lightpit.dao; - -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Types; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - -public abstract class AbstractDao<T> implements GenericDao<T> { - - private final PreparedStatement listQuery; - - protected AbstractDao(PreparedStatement listQuery) { - this.listQuery = listQuery; - } - - public final T mapColumns(ResultSet result) throws SQLException { - return mapColumns(result, ""); - } - - public abstract T mapColumns(ResultSet result, String qualifier) throws SQLException; - - /** - * Qualifies a column label if an qualifier is specified. - * - * @param qualifier an optional qualifier - * @param label the column label - * @return the label, qualified if necessary - */ - protected final String qual(String qualifier, String label) { - if (qualifier == null || qualifier.isBlank()) { - return label; - } else { - return qualifier + "." + label; - } - } - - protected final void setStringOrNull(PreparedStatement stmt, int index, String str) throws SQLException { - if (str == null || str.isBlank()) { - stmt.setNull(index, Types.VARCHAR); - } else { - stmt.setString(index, str); - } - } - - protected final <T> void setForeignKeyOrNull(PreparedStatement stmt, int index, T instance, Function<? super T, Integer> keyGetter) throws SQLException { - Integer key = Optional.ofNullable(instance).map(keyGetter).orElse(null); - if (key == null) { - stmt.setNull(index, Types.INTEGER); - } else { - stmt.setInt(index, key); - } - } - - @Override - public List<T> list() throws SQLException { - List<T> list = new ArrayList<>(); - try (ResultSet result = listQuery.executeQuery()) { - while (result.next()) { - list.add(mapColumns(result)); - } - } - return list; - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/uapcore/lightpit/dao/Functions.java Thu May 14 22:48:01 2020 +0200 @@ -0,0 +1,62 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2018 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +package de.uapcore.lightpit.dao; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Optional; +import java.util.function.Function; + +/** + * Some DAO utilities. + */ +public final class Functions { + + public static void setStringOrNull(PreparedStatement stmt, int index, String str) throws SQLException { + if (str == null || str.isBlank()) { + stmt.setNull(index, Types.VARCHAR); + } else { + stmt.setString(index, str); + } + } + + public static <T> void setForeignKeyOrNull(PreparedStatement stmt, int index, T instance, Function<? super T, Integer> keyGetter) throws SQLException { + Integer key = Optional.ofNullable(instance).map(keyGetter).orElse(null); + if (key == null) { + stmt.setNull(index, Types.INTEGER); + } else { + stmt.setInt(index, key); + } + } + + private Functions() { + + } +}
--- a/src/main/java/de/uapcore/lightpit/dao/GenericDao.java Wed May 13 21:46:26 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/GenericDao.java Thu May 14 22:48:01 2020 +0200 @@ -41,6 +41,15 @@ List<T> list() throws SQLException; /** + * Finds an entity by its integer ID. + * + * @param id the id + * @return the enity or null if there is no such entity + * @throws SQLException on any kind of SQL errors + */ + T find(int id) throws SQLException; + + /** * Inserts an instance into database. * * @param instance the instance to insert
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGDataAccessObjects.java Wed May 13 21:46:26 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGDataAccessObjects.java Thu May 14 22:48:01 2020 +0200 @@ -41,9 +41,8 @@ private final ProjectDao projectDao; public PGDataAccessObjects(Connection connection) throws SQLException { - final PGUserDao pgUserDao = new PGUserDao(connection); - userDao = pgUserDao; - projectDao = new PGProjectDao(connection, pgUserDao); + userDao = new PGUserDao(connection); + projectDao = new PGProjectDao(connection); } @Override
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java Wed May 13 21:46:26 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java Thu May 14 22:48:01 2020 +0200 @@ -28,7 +28,7 @@ */ package de.uapcore.lightpit.dao.postgres; -import de.uapcore.lightpit.dao.AbstractDao; +import de.uapcore.lightpit.dao.GenericDao; import de.uapcore.lightpit.dao.ProjectDao; import de.uapcore.lightpit.entities.Project; import de.uapcore.lightpit.entities.User; @@ -37,18 +37,31 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; -public final class PGProjectDao extends AbstractDao<Project> implements ProjectDao { +import static de.uapcore.lightpit.dao.Functions.setForeignKeyOrNull; +import static de.uapcore.lightpit.dao.Functions.setStringOrNull; - private final PGUserDao userDao; +public final class PGProjectDao implements ProjectDao, GenericDao<Project> { + + private final PreparedStatement insert, update, list, find; - private final PreparedStatement insert; - private final PreparedStatement update; + public PGProjectDao(Connection connection) throws SQLException { + list = connection.prepareStatement( + "select id, name, description, repourl, " + + "userid, username, lastname, givenname, mail " + + "from lpit_project " + + "left join lpit_user owner on lpit_project.owner = owner.userid " + + "order by name"); - public PGProjectDao(Connection connection, PGUserDao userDao) throws SQLException { - super(connection.prepareStatement( - "select * from lpit_project join lpit_user owner on lpit_project.owner = owner.userid")); + find = connection.prepareStatement( + "select id, name, description, repourl, " + + "userid, username, lastname, givenname, mail " + + "from lpit_project " + + "left join lpit_user owner on lpit_project.owner = owner.userid " + + "where id = ?"); insert = connection.prepareStatement( "insert into lpit_project (name, description, repourl, owner) values (?, ?, ?, ?)" @@ -56,17 +69,24 @@ update = connection.prepareStatement( "update lpit_project set name = ?, description = ?, repourl = ?, owner = ? where id = ?" ); - - this.userDao = userDao; } - @Override - public Project mapColumns(ResultSet result, String q) throws SQLException { - final var proj = new Project(result.getInt(qual(q, "id"))); - proj.setName(result.getString(qual(q, "name"))); - proj.setDescription(result.getString(qual(q, "description"))); - proj.setRepoUrl(result.getString(qual(q, "repourl"))); - proj.setOwner(userDao.mapColumns(result, "owner")); + public Project mapColumns(ResultSet result) throws SQLException { + final var proj = new Project(result.getInt("id")); + proj.setName(result.getString("name")); + proj.setDescription(result.getString("description")); + proj.setRepoUrl(result.getString("repourl")); + + final int id = result.getInt("userid"); + if (id != 0) { + final var user = new User(id); + user.setUsername(result.getString("username")); + user.setGivenname(result.getString("givenname")); + user.setLastname(result.getString("lastname")); + user.setMail(result.getString("mail")); + proj.setOwner(user); + } + return proj; } @@ -87,6 +107,30 @@ setStringOrNull(update, 2, instance.getDescription()); setStringOrNull(update, 3, instance.getRepoUrl()); setForeignKeyOrNull(update, 4, instance.getOwner(), User::getUserID); + update.setInt(5, instance.getId()); return update.executeUpdate() > 0; } + + @Override + public List<Project> list() throws SQLException { + List<Project> projects = new ArrayList<>(); + try (var result = list.executeQuery()) { + while (result.next()) { + projects.add(mapColumns(result)); + } + } + return projects; + } + + @Override + public Project find(int id) throws SQLException { + find.setInt(1, id); + try (var result = find.executeQuery()) { + if (result.next()) { + return mapColumns(result); + } else { + return null; + } + } + } }
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGUserDao.java Wed May 13 21:46:26 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGUserDao.java Thu May 14 22:48:01 2020 +0200 @@ -28,7 +28,7 @@ */ package de.uapcore.lightpit.dao.postgres; -import de.uapcore.lightpit.dao.AbstractDao; +import de.uapcore.lightpit.dao.GenericDao; import de.uapcore.lightpit.dao.UserDao; import de.uapcore.lightpit.entities.User; @@ -36,26 +36,41 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; -public final class PGUserDao extends AbstractDao<User> implements UserDao { +import static de.uapcore.lightpit.dao.Functions.setStringOrNull; + +public final class PGUserDao implements UserDao, GenericDao<User> { - private final PreparedStatement insert; - private final PreparedStatement update; + public static final String[] COLUMNS = { + "id", "username", "lastname", "givenname", "mail" + }; + + private final PreparedStatement insert, update, list, find; public PGUserDao(Connection connection) throws SQLException { - super(connection.prepareStatement("select * from lpit_user where userid >= 0 order by username")); + list = connection.prepareStatement( + "select userid, username, lastname, givenname, mail " + + "from lpit_user where userid >= 0 " + + "order by username"); + find = connection.prepareStatement( + "select userid, username, lastname, givenname, mail " + + "from lpit_user where userid = ? "); insert = connection.prepareStatement("insert into lpit_user (username, lastname, givenname, mail) values (?, ?, ?, ?)"); update = connection.prepareStatement("update lpit_user set lastname = ?, givenname = ?, mail = ? where userid = ?"); } - @Override - public User mapColumns(ResultSet result, String q) throws SQLException { - final var user = new User(result.getInt(qual(q, "userid"))); - user.setUsername(result.getString(qual(q, "username"))); - user.setGivenname(result.getString(qual(q, "givenname"))); - user.setLastname(result.getString(qual(q, "lastname"))); + public User mapColumns(ResultSet result) throws SQLException { + final int id = result.getInt("userid"); + if (id == 0) return null; + final var user = new User(id); + user.setUsername(result.getString("username")); + user.setGivenname(result.getString("givenname")); + user.setLastname(result.getString("lastname")); + user.setMail(result.getString("mail")); return user; } @@ -77,4 +92,27 @@ update.setInt(4, instance.getUserID()); return update.executeUpdate() > 0; } + + @Override + public List<User> list() throws SQLException { + List<User> users = new ArrayList<>(); + try (var result = list.executeQuery()) { + while (result.next()) { + users.add(mapColumns(result)); + } + } + return users; + } + + @Override + public User find(int id) throws SQLException { + find.setInt(1, id); + try (var result = find.executeQuery()) { + if (result.next()) { + return mapColumns(result); + } else { + return null; + } + } + } }
--- a/src/main/java/de/uapcore/lightpit/entities/Project.java Wed May 13 21:46:26 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/entities/Project.java Thu May 14 22:48:01 2020 +0200 @@ -28,8 +28,6 @@ */ package de.uapcore.lightpit.entities; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; public class Project { @@ -40,8 +38,6 @@ private String repoUrl; private User owner; - private final List<Version> versions = new ArrayList<>(); - public Project(int id) { this.id = id; } @@ -82,10 +78,6 @@ this.owner = owner; } - public List<Version> getVersions() { - return versions; - } - @Override public boolean equals(Object o) { if (this == o) return true;
--- a/src/main/java/de/uapcore/lightpit/entities/User.java Wed May 13 21:46:26 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/entities/User.java Thu May 14 22:48:01 2020 +0200 @@ -80,6 +80,19 @@ this.lastname = lastname; } + public String getDisplayname() { + StringBuilder dn = new StringBuilder(); + dn.append(givenname); + dn.append(' '); + dn.append(lastname); + dn.append(' '); + if (mail != null && !mail.isBlank()) { + dn.append("<"+mail+">"); + } + final var str = dn.toString().trim(); + return str.isBlank() ? username : str; + } + @Override public boolean equals(Object o) { if (this == o) return true;
--- a/src/main/java/de/uapcore/lightpit/modules/HomeModule.java Wed May 13 21:46:26 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/modules/HomeModule.java Thu May 14 22:48:01 2020 +0200 @@ -31,6 +31,7 @@ import de.uapcore.lightpit.*; import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; /** * Entry point for the application. @@ -47,7 +48,10 @@ public final class HomeModule extends AbstractLightPITServlet { @RequestMapping(method = HttpMethod.GET) - public ResponseType handle() { + public ResponseType handle(HttpServletRequest req) { + + setDynamicFragment(req, "home"); + setStylesheet(req, "home"); return ResponseType.HTML; }
--- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java Wed May 13 21:46:26 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java Thu May 14 22:48:01 2020 +0200 @@ -31,9 +31,13 @@ import de.uapcore.lightpit.*; import de.uapcore.lightpit.dao.DataAccessObjects; +import de.uapcore.lightpit.entities.Project; +import de.uapcore.lightpit.entities.User; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; +import java.sql.SQLException; +import java.util.Optional; @LightPITModule( bundleBaseName = "localization.projects", @@ -47,12 +51,59 @@ public final class ProjectsModule extends AbstractLightPITServlet { @RequestMapping(method = HttpMethod.GET) - public ResponseType index(HttpServletRequest req, DataAccessObjects dao) { + public ResponseType index(HttpServletRequest req, DataAccessObjects dao) throws SQLException { + final var projectDao = dao.getProjectDao(); + + req.setAttribute("projects", projectDao.list()); + setDynamicFragment(req, "projects"); + + return ResponseType.HTML; + } + + @RequestMapping(requestPath = "edit", method = HttpMethod.GET) + public ResponseType displayCreateForm(HttpServletRequest req, DataAccessObjects dao) throws SQLException { + final var projectDao = dao.getProjectDao(); + + Optional<Integer> id = getParameter(req, Integer.class, "id"); + if (id.isPresent()) { + req.setAttribute("project", Optional.ofNullable(projectDao.find(id.get())).orElse(new Project(-1))); + } else { + req.setAttribute("project", new Project(-1)); + } + + setDynamicFragment(req, "project-form"); return ResponseType.HTML; } - @RequestMapping(method = HttpMethod.GET, requestPath = "versions", menuKey = "menu.versions") + @RequestMapping(requestPath = "commit", method = HttpMethod.POST) + public ResponseType commit(HttpServletRequest req, DataAccessObjects dao) { + + Project project = new Project(-1); + try { + project = new Project(getParameter(req, Integer.class, "id").orElseThrow()); + project.setName(getParameter(req, String.class, "name").orElseThrow()); + getParameter(req, String.class, "description").ifPresent(project::setDescription); + getParameter(req, String.class, "repoUrl").ifPresent(project::setRepoUrl); + getParameter(req, Integer.class, "owner").map( + ownerId -> ownerId >= 0 ? new User(ownerId) : null + ).ifPresent(project::setOwner); + + dao.getProjectDao().saveOrUpdate(project); + + setRedirectLocation(req, "./projects/"); + setDynamicFragment(req, Constants.DYN_FRAGMENT_COMMIT_SUCCESSFUL); + } catch (NullPointerException | NumberFormatException | SQLException ex) { + // TODO: set request attribute with error text + req.setAttribute("project", project); + setDynamicFragment(req, "project-form"); + } + + return ResponseType.HTML; + } + + + @RequestMapping(requestPath = "versions", method = HttpMethod.GET, menuKey = "menu.versions") public ResponseType versions(HttpServletRequest req, DataAccessObjects dao) { return ResponseType.HTML;
--- a/src/main/resources/localization/home.properties Wed May 13 21:46:26 2020 +0200 +++ b/src/main/resources/localization/home.properties Thu May 14 22:48:01 2020 +0200 @@ -24,3 +24,5 @@ name = Home Page description = The default page that is displayed when visiting the site. menuLabel = Home + +version=LightPIT - Version 0.1 (Snapshot) \ No newline at end of file
--- a/src/main/resources/localization/home_de.properties Wed May 13 21:46:26 2020 +0200 +++ b/src/main/resources/localization/home_de.properties Thu May 14 22:48:01 2020 +0200 @@ -24,3 +24,5 @@ name = Startseite description = Die Seite, die dem Benutzer standardm\u00e4\u00dfig beim Besuch angezeigt wird. menuLabel = Startseite + +version=LightPIT - Version 0.1 (Entwicklungsversion)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/resources/localization/lightpit.properties Thu May 14 22:48:01 2020 +0200 @@ -0,0 +1,28 @@ +# Copyright 2018 Mike Becker. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +button.okay=OK +button.cancel=Cancel + +commit.success=Operation successful - you will be redirected in a second. +commit.redirect-link=If redirection does not work, click the following link: \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/resources/localization/lightpit_de.properties Thu May 14 22:48:01 2020 +0200 @@ -0,0 +1,28 @@ +# Copyright 2018 Mike Becker. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +button.okay=OK +button.cancel=Abbrechen + +commit.success=Operation erfolgreich - Sie werden jeden Moment weitergeleitet. +commit.redirect-link=Falls die Weiterleitung nicht klappt, klicken Sie bitte hier:
--- a/src/main/resources/localization/projects.properties Wed May 13 21:46:26 2020 +0200 +++ b/src/main/resources/localization/projects.properties Thu May 14 22:48:01 2020 +0200 @@ -20,7 +20,20 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + name=Project Management description=Allows the configuration of projects. menuLabel=Projects + menu.versions=Versions + +button.create=New Project + +no-projects=Welcome to LightPIT. Start off by creating a new project! + +thead.name=Name +thead.description=Description +thead.repoUrl=Repository +thead.owner=Project Lead + +placeholder.null-owner=Unassigned
--- a/src/main/resources/localization/projects_de.properties Wed May 13 21:46:26 2020 +0200 +++ b/src/main/resources/localization/projects_de.properties Thu May 14 22:48:01 2020 +0200 @@ -20,7 +20,20 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + name=Projektverwaltung description=Erlaubt die Konfiguration von Projekten. menuLabel=Projekte -menu.versions=Versionen \ No newline at end of file + +menu.versions=Versionen + +button.create=Neues Projekt + +no-projects=Wilkommen bei LightPIT. Beginnen Sie mit der Erstellung eines Projektes! + +thead.name=Name +thead.description=Beschreibung +thead.repoUrl=Repository +thead.owner=Projektleitung + +placeholder.null-owner=Nicht Zugewiesen
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/dynamic_fragments/commit-successful.jsp Thu May 14 22:48:01 2020 +0200 @@ -0,0 +1,34 @@ +<%-- +DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + +Copyright 2018 Mike Becker. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--%> +<%@page pageEncoding="UTF-8" %> +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + +<c:set scope="page" var="redirectLocation" value="${requestScope[Constants.REQ_ATTR_REDIRECT_LOCATION]}"/> + +<fmt:message bundle="${lightpit_bundle}" key="commit.success" /> +<fmt:message bundle="${lightpit_bundle}" key="commit.redirect-link" />
--- a/src/main/webapp/WEB-INF/dynamic_fragments/error.jsp Wed May 13 21:46:26 2020 +0200 +++ b/src/main/webapp/WEB-INF/dynamic_fragments/error.jsp Thu May 14 22:48:01 2020 +0200 @@ -25,11 +25,13 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --%> <%@page pageEncoding="UTF-8" %> +<%@page import="de.uapcore.lightpit.Constants" %> <%@page import="de.uapcore.lightpit.modules.ErrorModule" %> <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> +<c:set scope="page" var="baseHref" value="${requestScope[Constants.REQ_ATTR_BASE_HREF]}" /> <c:set scope="page" var="errorCode" value="${requestScope[ErrorModule.REQ_ATTR_ERROR_CODE]}"/> <c:set scope="page" var="returnLink" value="${requestScope[ErrorModule.REQ_ATTR_RETURN_LINK]}"/>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/dynamic_fragments/home.jsp Thu May 14 22:48:01 2020 +0200 @@ -0,0 +1,32 @@ +<%-- +DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + +Copyright 2018 Mike Becker. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--%> +<%@page pageEncoding="UTF-8" %> +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + +<div class="smalltext"> + <fmt:message key="version" /> +</div> \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/dynamic_fragments/project-form.jsp Thu May 14 22:48:01 2020 +0200 @@ -0,0 +1,75 @@ +<%-- +DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + +Copyright 2018 Mike Becker. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--%> +<%@page pageEncoding="UTF-8" %> +<%@page import="de.uapcore.lightpit.Constants" %> +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + +<c:set scope="page" var="moduleInfo" value="${requestScope[Constants.REQ_ATTR_MODULE_INFO]}"/> + +<jsp:useBean id="project" type="de.uapcore.lightpit.entities.Project" scope="request"/> + +<form action="./${moduleInfo.modulePath}/commit" method="post"> + <table class="formtable" style="width: 80ch"> + <colgroup> + <col> + <col style="width: 100%"> + </colgroup> + <tbody> + <tr> + <th><fmt:message key="thead.name"/></th> + <td><input name="name" type="text" maxlength="20" required value="${project.name}"/> </td> + </tr> + <tr> + <th class="vtop"><fmt:message key="thead.description"/></th> + <td><input type="text" name="description" maxlength="200" value="${project.description}" /> </td> + </tr> + <tr> + <th><fmt:message key="thead.repoUrl"/></th> + <td><input name="repoUrl" type="url" maxlength="50" value="${project.repoUrl}" /> </td> + </tr> + <tr> + <th><fmt:message key="thead.owner"/></th> + <td> + <select name="owner"> + <option value="-1"><fmt:message key="placeholder.null-owner" /> </option> + <!-- TODO: add user selection --> + </select> + </td> + </tr> + </tbody> + <tfoot> + <tr> + <td colspan="2"> + <input type="hidden" name="id" value="${project.id}" /> + <a href="./${moduleInfo.modulePath}" class="button"><fmt:message bundle="${lightpit_bundle}" key="button.cancel"/></a> + <button type="submit"><fmt:message bundle="${lightpit_bundle}" key="button.okay" /></button> + </td> + </tr> + </tfoot> + </table> +</form>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/dynamic_fragments/projects.jsp Thu May 14 22:48:01 2020 +0200 @@ -0,0 +1,83 @@ +<%-- +DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + +Copyright 2018 Mike Becker. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--%> +<%@page pageEncoding="UTF-8" %> +<%@page import="de.uapcore.lightpit.Constants" %> +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + +<c:set scope="page" var="moduleInfo" value="${requestScope[Constants.REQ_ATTR_MODULE_INFO]}"/> + +<jsp:useBean id="projects" type="java.util.List<de.uapcore.lightpit.entities.Project>" scope="request"/> + +<c:if test="${empty projects}"> + <div class="info-box"> + <fmt:message key="no-projects" /> + </div> +</c:if> + +<div id="tool-area"> + <a href="./${moduleInfo.modulePath}/edit" class="button"><fmt:message key="button.create" /></a> +</div> + +<c:if test="${not empty projects}"> +<table class="datatable medskip"> + <colgroup> + <col> + <col style="width: 15%"> + <col style="width: 35%"> + <col style="width: 30%"> + <col style="width: 20%"> + </colgroup> + <thead> + <tr> + <th></th> + <th><fmt:message key="thead.name"/></th> + <th><fmt:message key="thead.description"/></th> + <th><fmt:message key="thead.repoUrl"/></th> + <th><fmt:message key="thead.owner"/></th> + </tr> + </thead> + <tbody> + <c:forEach var="project" items="${projects}"> + <tr> + <td><a href="./${moduleInfo.modulePath}/edit?id=${project.id}">✎</a></td> + <td><c:out value="${project.name}"/></td> + <td><c:out value="${project.description}"/></td> + <td> + <c:if test="${not empty project.repoUrl}"> + <a target="_blank" href="<c:out value="${project.repoUrl}"/>"><c:out value="${project.repoUrl}"/></a> + </c:if> + </td> + <td> + <c:if test="${not empty project.owner}"><c:out value="${project.owner.displayname}"/></c:if> + <c:if test="${empty project.owner}"><fmt:message key="placeholder.null-owner" /></c:if> + </td> + </tr> + </c:forEach> + </tbody> +</table> +</c:if>
--- a/src/main/webapp/WEB-INF/jsp/site.jsp Wed May 13 21:46:26 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/site.jsp Thu May 14 22:48:01 2020 +0200 @@ -31,7 +31,7 @@ <%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <%-- Make the base href easily available at request scope --%> -<c:set scope="request" var="baseHref" value="${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/" /> +<c:set scope="page" var="baseHref" value="${requestScope[Constants.REQ_ATTR_BASE_HREF]}" /> <%-- Define an alias for the request path --%> <c:set scope="page" var="requestPath" value="${requestScope[Constants.REQ_ATTR_PATH]}"/> @@ -45,6 +45,9 @@ <%-- Define an alias for the fragment name --%> <c:set scope="page" var="fragment" value="${requestScope[Constants.REQ_ATTR_FRAGMENT]}"/> +<%-- Define an alias for the optional redirect location --%> +<c:set scope="page" var="redirectLocation" value="${requestScope[Constants.REQ_ATTR_REDIRECT_LOCATION]}"/> + <%-- Define an alias for the additional stylesheet --%> <c:set scope="page" var="extraCss" value="${requestScope[Constants.REQ_ATTR_STYLESHEET]}"/> @@ -66,6 +69,9 @@ </fmt:bundle> </title> <meta charset="UTF-8"> + <c:if test="${not empty redirectLocation}"> + <meta http-equiv="refresh" content="0; URL=${redirectLocation}"> + </c:if> <link rel="stylesheet" href="lightpit.css" type="text/css"> <c:if test="${not empty extraCss}"> <link rel="stylesheet" href="${extraCss}" type="text/css"> @@ -87,6 +93,7 @@ <div id="content-area"> <c:if test="${not empty fragment}"> <fmt:setBundle scope="request" basename="${moduleInfo.bundleBaseName}"/> + <fmt:setBundle scope="request" var="lightpit_bundle" basename="localization.lightpit"/> <c:import url="${fragment}" /> </c:if> </div>
--- a/src/main/webapp/lightpit.css Wed May 13 21:46:26 2020 +0200 +++ b/src/main/webapp/lightpit.css Thu May 14 22:48:01 2020 +0200 @@ -28,22 +28,17 @@ */ html { - background: #f8f8f8; + font-family: sans-serif; + background: white; + color: #1c204e; + margin: 0; + padding: 0; } body { - background: white; - font-family: serif; - - border-color: #505050; - border-style: solid; - border-width: 1pt; - - color: #1c202e; -} - -h1, h2, h3, h4, #mainMenu, #subMenu { - font-family: sans-serif; + height: 100%; + margin: 0; + padding: 0; } a { @@ -88,6 +83,33 @@ padding: 1em; } +button, a.button { + display: inline-block; + font-size: medium; + border-style: outset; + border-width: 2pt; + border-color: #6060cc; + color: inherit; + background: #f0f0f0; + + padding: .25em .5em .25em .5em; + cursor: default; + text-decoration: none; +} + +button:hover, a.button:hover { + background: #f0f0ff; +} + +button[type=submit] { + background: #20a0ff; + color: white; +} + +button[type=submit]:hover { + background: #1090cf; +} + th { text-align: left; } @@ -101,6 +123,7 @@ } table.datatable th { + white-space: nowrap; font-weight: bold; background: lightsteelblue; } @@ -116,6 +139,35 @@ background: lightblue; } +table.formtable { + border-style: none; + border-collapse: separate; + border-spacing: 1em; +} + +table.formtable th { + font-weight: bold; + text-align: left; + vertical-align: center; + white-space: nowrap; +} + +table.formtable tbody td > * { + width: 100%; +} + +table.formtable tfoot td { + text-align: right; +} + +.fullwidth { + width: 100%; +} + +.vtop { + vertical-align: top; +} + .hcenter { text-align: center; } @@ -126,4 +178,16 @@ .nowrap { white-space: nowrap; +} + +.medskip { + margin-top: .5em; +} + +.info-box { + margin: 2em; + border-style: dashed; + border-width: 1pt; + border-color: deepskyblue; + padding: 1em; } \ No newline at end of file