From 3043f8ac2b2dacf185f23f5b61695d32ee2d4cf3 Mon Sep 17 00:00:00 2001 From: kunwarj <121902036+kunwarj@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:43:01 -0400 Subject: [PATCH 1/6] support for dom element accessibility (#1250) * email templates for document creation and user registration notification * Revert "email templates for document creation and user registration notification" This reverts commit 229e3a98a61e407d4ca20c5ff5a7e2637fb8ab68. * dispatch an editDocumentWKTReceived event after document is updated with nunaliit_geom * add href to multiselect checkbox to make it accessible via keyboard * add tabindex to map overview dispaly container to make it focusable with tab key * add tabindex to help content container to make if focusable with tab key * add appropriate tag for array inputs plus/grabage button to make them interactable with tab * Fix focus handling in jQuery dialog when adding/removing form elements --------- Co-authored-by: Jagdish Kunwar --- .../main/js/nunaliit2/css/basic/n2.schema.css | 12 +++--- .../src/main/js/nunaliit2/n2.couchModule.js | 1 + nunaliit2-js/src/main/js/nunaliit2/n2.help.js | 1 + .../src/main/js/nunaliit2/n2.schema.js | 40 ++++++++++++++----- .../js/nunaliit2/n2.widgetSelectableFilter.js | 1 + 5 files changed, 38 insertions(+), 17 deletions(-) diff --git a/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.schema.css b/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.schema.css index e9d9b27d0..3cdfbcf66 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.schema.css +++ b/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.schema.css @@ -12,7 +12,9 @@ .n2schema_array_item { position: relative; - min-height: 58px; + display: flex; + align-items: center; + margin-bottom: 12px; } .n2_tag_element { @@ -108,11 +110,8 @@ } .n2schema_array_item_buttons { - position: absolute; - right: 0px; - top: 0px; - width: 16px; - z-index: 1; + display: flex; + flex-direction: column; } .n2schema_array_item_delete { @@ -144,6 +143,7 @@ .n2schema_array_item_wrapper { margin-right: 16px; + width: 100%; } .n2schema_referenceDelete { diff --git a/nunaliit2-js/src/main/js/nunaliit2/n2.couchModule.js b/nunaliit2-js/src/main/js/nunaliit2/n2.couchModule.js index e12e4f351..adf305831 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/n2.couchModule.js +++ b/nunaliit2-js/src/main/js/nunaliit2/n2.couchModule.js @@ -717,6 +717,7 @@ var ModuleDisplay = $n2.Class({ _this.sidePanelName = $n2.getUniqueId(); var $sidePanel = $('
') .attr('id',_this.sidePanelName) + .attr('tabindex', 0) .addClass('n2_content_text'); // Add side panel inside content panel or after content panel diff --git a/nunaliit2-js/src/main/js/nunaliit2/n2.help.js b/nunaliit2-js/src/main/js/nunaliit2/n2.help.js index decddf2d2..339498d1f 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/n2.help.js +++ b/nunaliit2-js/src/main/js/nunaliit2/n2.help.js @@ -110,6 +110,7 @@ var HelpDisplay = $n2.Class({ var $dialog = $('
') .attr('id',this.helpDialogId) + .attr('tabindex', 0) .addClass('n2help_content') .appendTo( $('body') ); diff --git a/nunaliit2-js/src/main/js/nunaliit2/n2.schema.js b/nunaliit2-js/src/main/js/nunaliit2/n2.schema.js index f1f2b327f..0d3f362d1 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/n2.schema.js +++ b/nunaliit2-js/src/main/js/nunaliit2/n2.schema.js @@ -531,20 +531,20 @@ function _arrayField() { r.push('
'); - r.push('
'); + r.push('
'); - r.push('
'); + r.push( options.fn(item,{data:{n2_selector:completeSelectors}}) ); - r.push('
'); + r.push('
'); - r.push('
'); - - r.push('
'); // close buttons - - r.push('
'); + r.push('
'); - r.push( options.fn(item,{data:{n2_selector:completeSelectors}}) ); + r.push(''); + r.push(''); + + r.push(''); + r.push('
'); }; }; @@ -561,11 +561,11 @@ function _arrayField() { }; if( arraySelector ){ var arrayClass = createClassStringFromSelector(arraySelector); - r.push('
'); + r.push('>'); }; r.push('
'); @@ -1928,6 +1928,24 @@ var Form = $n2.Class({ }; }); }; + + /** + * When adding or removing form elements within a jQuery dialog, the focus may + * shift to the body tag, preventing keyboard users from navigating back to + * the dialog. To handle this scenario, we check for any visible jQuery dialogs, + * search for focusable elements within the dialog, and set the focus to the first + * visible one. + */ + var $openDialog = $('.ui-dialog-content:visible'); + if ($openDialog.length) { + var $focusableElements = $openDialog + .find('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])') + .filter(':visible'); + + if ($focusableElements.length) { + $focusableElements.first().focus(); + } + } }; }, diff --git a/nunaliit2-js/src/main/js/nunaliit2/n2.widgetSelectableFilter.js b/nunaliit2-js/src/main/js/nunaliit2/n2.widgetSelectableFilter.js index 992d7b00c..643a14256 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/n2.widgetSelectableFilter.js +++ b/nunaliit2-js/src/main/js/nunaliit2/n2.widgetSelectableFilter.js @@ -1010,6 +1010,7 @@ var MultiFilterSelectionDropDownWidget = $n2.Class('MultiFilterSelectionDropDown $('') .text(_loc(label)) .attr('data-n2-choiceId',choiceId) + .attr('href', '#') .appendTo($div) .click(function(){ var $a = $(this); From 79710662ff8e3f72681f25d3f40546301d303ad9 Mon Sep 17 00:00:00 2001 From: Brendan Billingsley Date: Thu, 24 Oct 2024 14:06:25 -0600 Subject: [PATCH 2/6] 1040 enhance nunaliit to provide an atlas export (#1229) * Discover all schemas and dump data for each schema. * Add media and tar, gz export. * Add media and tar, gz export. * Refactor. Delete dir after compessing. * Thread export creation. Endpoints for create, list, get export * remove unused code, println --- .../gcrc/couch/command/AtlasProperties.java | 19 ++ .../gcrc/couch/command/CommandRun.java | 2 + nunaliit2-couch-export/pom.xml | 5 + .../gcrc/couch/export/ExportFullAtlas.java | 219 ++++++++++++++++++ .../gcrc/couch/export/ExportServlet.java | 146 +++++++++++- pom.xml | 1 + 6 files changed, 389 insertions(+), 3 deletions(-) create mode 100644 nunaliit2-couch-export/src/main/java/ca/carleton/gcrc/couch/export/ExportFullAtlas.java diff --git a/nunaliit2-couch-command/src/main/java/ca/carleton/gcrc/couch/command/AtlasProperties.java b/nunaliit2-couch-command/src/main/java/ca/carleton/gcrc/couch/command/AtlasProperties.java index 68b5a2b74..baed2ac77 100644 --- a/nunaliit2-couch-command/src/main/java/ca/carleton/gcrc/couch/command/AtlasProperties.java +++ b/nunaliit2-couch-command/src/main/java/ca/carleton/gcrc/couch/command/AtlasProperties.java @@ -37,6 +37,8 @@ static public AtlasProperties fromProperties(Properties props) throws Exception atlasProps.setCouchDbSubmissionDbName( props.getProperty("couchdb.submission.dbName") ); atlasProps.setCouchDbAdminUser( props.getProperty("couchdb.admin.user") ); atlasProps.setInReachDbName(props.getProperty("couchdb.inreach.dbName", "")); + atlasProps.setExportUser(props.getProperty("export.complete.user")); + atlasProps.setExportPassword(props.getProperty("export.complete.password")); // CouchDb password try { @@ -201,6 +203,7 @@ static public void writeProperties(File atlasDir, Properties props) throws Excep Set sensitivePropertyNames = new HashSet(); { sensitivePropertyNames.add("couchdb.admin.password"); + sensitivePropertyNames.add("export.complete.password"); sensitivePropertyNames.add("server.key"); sensitivePropertyNames.add("google.mapapi.key"); @@ -320,6 +323,8 @@ static public void writeProperties(File atlasDir, Properties props) throws Excep private String inReachDbName; private String couchDbAdminUser; private String couchDbAdminPassword; + private String exportUser; + private String exportPassword; private int serverPort = 8080; private boolean restricted = false; private byte[] serverKey = null; @@ -403,6 +408,20 @@ public void setCouchDbAdminPassword(String couchDbAdminPassword) { this.couchDbAdminPassword = couchDbAdminPassword; } + public String getExportUser() { + return exportUser; + } + public void setExportUser(String exportUser) { + this.exportUser = exportUser; + } + + public String getExportPassword() { + return exportPassword; + } + public void setExportPassword(String exportPassword) { + this.exportPassword = exportPassword; + } + public int getServerPort() { return serverPort; } diff --git a/nunaliit2-couch-command/src/main/java/ca/carleton/gcrc/couch/command/CommandRun.java b/nunaliit2-couch-command/src/main/java/ca/carleton/gcrc/couch/command/CommandRun.java index 69646f012..391ba5fab 100644 --- a/nunaliit2-couch-command/src/main/java/ca/carleton/gcrc/couch/command/CommandRun.java +++ b/nunaliit2-couch-command/src/main/java/ca/carleton/gcrc/couch/command/CommandRun.java @@ -229,6 +229,8 @@ public void runCommand( // Servlet for export { ServletHolder servletHolder = new ServletHolder(new ExportServlet()); + servletHolder.setInitParameter("exportUser", atlasProperties.getExportUser()); + servletHolder.setInitParameter("exportPassword", atlasProperties.getExportPassword()); servletHolder.setInitOrder(2); context.addServlet(servletHolder,"/servlet/export/*"); } diff --git a/nunaliit2-couch-export/pom.xml b/nunaliit2-couch-export/pom.xml index 295cb15b7..61ed3059a 100644 --- a/nunaliit2-couch-export/pom.xml +++ b/nunaliit2-couch-export/pom.xml @@ -24,6 +24,11 @@ commons-io ${commons-io.version} + + org.apache.commons + commons-compress + ${commons-compress.version} + junit junit diff --git a/nunaliit2-couch-export/src/main/java/ca/carleton/gcrc/couch/export/ExportFullAtlas.java b/nunaliit2-couch-export/src/main/java/ca/carleton/gcrc/couch/export/ExportFullAtlas.java new file mode 100644 index 000000000..432b2c1a2 --- /dev/null +++ b/nunaliit2-couch-export/src/main/java/ca/carleton/gcrc/couch/export/ExportFullAtlas.java @@ -0,0 +1,219 @@ +package ca.carleton.gcrc.couch.export; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.json.JSONObject; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; + +import ca.carleton.gcrc.couch.client.CouchDb; +import ca.carleton.gcrc.couch.client.CouchDesignDocument; +import ca.carleton.gcrc.couch.client.CouchQuery; +import ca.carleton.gcrc.couch.client.CouchQueryResults; +import ca.carleton.gcrc.couch.export.impl.DocumentRetrievalSchema; +import ca.carleton.gcrc.couch.export.impl.ExportFormatGeoJson; +import ca.carleton.gcrc.couch.export.impl.SchemaCacheCouchDb; + +public class ExportFullAtlas implements Runnable { + private static final Logger logger = LoggerFactory.getLogger(ExportFullAtlas.class); + private CouchDesignDocument dd; + private File exportDir; + private CouchDb couchDb; + private String atlasRootPath; + + public ExportFullAtlas(File exportDir, CouchDb couchDb, String atlasRootPath) throws Exception { + this.couchDb = couchDb; + dd = couchDb.getDesignDocument("atlas"); + this.atlasRootPath = atlasRootPath; + this.exportDir = exportDir; + } + + public static String createExport(String atlasRootPath, CouchDb couchDb) { + File exportDir = getExportDir(atlasRootPath); + File exportCompressFinalFile = new File(exportDir.getAbsolutePath() + ".tar.gz"); + + try { + ExportFullAtlas export = new ExportFullAtlas(exportDir, couchDb, atlasRootPath); + new Thread(export).start(); + } catch (Exception e) { + logger.error("Error setting up export", e); + } + + return exportCompressFinalFile.getName(); + } + + public static File getExport(String atlasRootPath, String filename) { + File dumpDir = new File(atlasRootPath, "dump"); + return new File(dumpDir, filename); + } + + public static List getExports(String atlasRootPath) { + File dir = new File(atlasRootPath, "dump"); + File [] files = dir.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.startsWith("full_export") && name.endsWith(".tar.gz"); + } + }); + List filesList = Arrays.asList(files); + List names = filesList.stream().map(f -> f.getName()).collect(Collectors.toList()); + return names; + } + + private static File getExportDir(String atlasRootPath) { + File exportDir = null; + { + Calendar calendar = Calendar.getInstance(); + String name = String.format( + "full_export_%04d-%02d-%02d-%02d_%02d-%02d", calendar.get(Calendar.YEAR), + (calendar.get(Calendar.MONTH) + 1), calendar.get(Calendar.DAY_OF_MONTH), + calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), calendar.get(Calendar.SECOND)); + exportDir = new File(atlasRootPath, "dump/" + name); + exportDir.mkdirs(); + } + return exportDir; + } + + public void run() { + File exportCompressTmpFile = new File(exportDir.getAbsolutePath() + ".tar.gz.tmp"); + File exportCompressFinalFile = new File(exportDir.getAbsolutePath() + ".tar.gz"); + exportData(exportDir); + exportMedia(exportDir); + try { + compress(exportCompressTmpFile.getAbsolutePath(), exportDir); + FileUtils.moveFile(FileUtils.getFile(exportCompressTmpFile), FileUtils.getFile(exportCompressFinalFile)); + } catch (IOException e) { + logger.error("Error writing export tar.gz", e); + } + try { + FileUtils.deleteDirectory(exportDir); + } catch (IOException e) { + logger.error("Error deleting export directory " + exportDir.getAbsolutePath() + " after compressing", e); + } + } + + private void exportData(File exportDir) { + // Find schemas that need to be exported + CouchQuery query = new CouchQuery(); + query.setViewName("schemas"); + CouchQueryResults results; + try { + query.setIncludeDocs(false); + results = dd.performQuery(query); + } catch (Exception e) { + logger.error("Error querying DB for schemas", e); + return; + } + + Set schemas = new HashSet(); + for (JSONObject row : results.getRows()) { + String docId = row.optString("id"); + String key = row.optString("key"); + if (null != docId && !docId.startsWith("org.nunaliit")) { + schemas.add(key); + } + } + + logger.debug("Fetching data for schemas: ", schemas); + + // Loop over schemas and export docs to GeoJSON + try { + for (String schemaName : schemas) { + // Build doc retrieval based on method + DocumentRetrieval docRetrieval = null; + docRetrieval = DocumentRetrievalSchema.create(couchDb, schemaName); + + ExportFormat outputFormat; + SchemaCache schemaCache = new SchemaCacheCouchDb(couchDb); + outputFormat = new ExportFormatGeoJson(schemaCache, docRetrieval); + + File outputFile = new File(exportDir, schemaName + ".geojson"); + outputFile.createNewFile(); + FileOutputStream fos = new FileOutputStream(outputFile); + outputFormat.outputExport(fos); + fos.close(); + } + } catch (Exception e) { + logger.error("Error fetching schema docs and writing results", e); + return; + } + } + + private void exportMedia(File exportDir) { + String mediaDir = new File(atlasRootPath, "media").getAbsolutePath(); + String exportDirPath = exportDir.getAbsolutePath() + "/media"; + try { + Files.walk(Paths.get(mediaDir)).forEach(source -> { + Path destination = Paths.get(exportDirPath, source.toString() + .substring(mediaDir.length())); + try { + Files.copy(source, destination); + } catch (IOException e) { + logger.error("Error copying file: " + source + " to dest: " + destination, e); + } + + }); + } catch (IOException e) { + logger.error("Error copying media directoy for export", e); + } + } + + private void compress(String name, File... files) throws IOException { + try (TarArchiveOutputStream out = getTarArchiveOutputStream(name)) { + for (File file : files) { + addToArchiveCompression(out, file, "."); + } + } + } + + private TarArchiveOutputStream getTarArchiveOutputStream(String name) throws IOException { + TarArchiveOutputStream taos = new TarArchiveOutputStream( + new GzipCompressorOutputStream(new FileOutputStream(name))); + // TAR has an 8 gig file limit by default, this gets around that + taos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR); + // TAR originally didn't support long file names, so enable the support for it + taos.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU); + taos.setAddPaxHeadersForNonAsciiNames(true); + return taos; + } + + private void addToArchiveCompression(TarArchiveOutputStream out, File file, String dir) throws IOException { + String entry = dir + File.separator + file.getName(); + if (file.isFile()) { + out.putArchiveEntry(new TarArchiveEntry(file, entry)); + try (FileInputStream in = new FileInputStream(file)) { + IOUtils.copy(in, out); + } + out.closeArchiveEntry(); + } else if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + addToArchiveCompression(out, child, entry); + } + } + } else { + logger.error(file.getName() + " is not supported"); + } + } +} diff --git a/nunaliit2-couch-export/src/main/java/ca/carleton/gcrc/couch/export/ExportServlet.java b/nunaliit2-couch-export/src/main/java/ca/carleton/gcrc/couch/export/ExportServlet.java index 160ee3d75..a20005fcd 100644 --- a/nunaliit2-couch-export/src/main/java/ca/carleton/gcrc/couch/export/ExportServlet.java +++ b/nunaliit2-couch-export/src/main/java/ca/carleton/gcrc/couch/export/ExportServlet.java @@ -1,5 +1,9 @@ package ca.carleton.gcrc.couch.export; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; @@ -12,9 +16,11 @@ import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.codec.binary.Base64; import org.apache.commons.fileupload.FileItemIterator; import org.apache.commons.fileupload.FileItemStream; import org.apache.commons.fileupload.servlet.ServletFileUpload; @@ -45,10 +51,16 @@ public class ExportServlet extends JsonServlet { final protected Logger logger = LoggerFactory.getLogger(this.getClass()); public static final String ConfigAttributeName_AtlasName = "ExportServlet_AtlasName"; + public static final String CONFIG_EXPORT_USER = "exportUser"; + public static final String CONFIG_EXPORT_PASS = "exportPassword"; private ExportConfiguration configuration; + private ServletConfig servletConfig; + private String atlasName = null; + private String exportUserPass; + public ExportServlet() { } @@ -78,6 +90,16 @@ public void init(ServletConfig config) throws ServletException { } else { throw new ServletException("Invalid class for configuration: "+configurationObj.getClass().getName()); } + + String exportUser = config.getInitParameter(CONFIG_EXPORT_USER); + String exportPassword = config.getInitParameter(CONFIG_EXPORT_PASS); + if(exportUser == null || exportUser.isEmpty() || exportPassword == null || exportPassword.isEmpty()) { + exportUserPass = null; + } else { + exportUserPass = exportUser + ":" + exportPassword; + } + + servletConfig = config; } public void destroy() { @@ -96,6 +118,14 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t doGetWelcome(request, response); } else if ("test".equalsIgnoreCase(path)) { doGetTest(request, response); + } else if ("complete".equalsIgnoreCase(path)) { + if(paths.size() == 1) { + doGetCompleteList(request, response); + } else if (paths.size() == 2) { + doGetCompleteFile(paths.get(1), request, response); + } else { + throw new Exception("Can't get " + paths.toString()); + } } else { throw new Exception("Unknown request"); } @@ -112,11 +142,13 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) if (paths.size() > 0) { path = paths.get(0); } - - if ("definition".equalsIgnoreCase(path)) { + + if( "definition".equalsIgnoreCase(path) ) { doPostDefinition(request, response); - } else if ("records".equalsIgnoreCase(path)) { + } else if( "records".equalsIgnoreCase(path) ) { doPostRecords(request, response); + } else if("complete".equalsIgnoreCase(path)) { + doPostComplete(request, response); } else { throw new Exception("Unknown request: " + path); } @@ -126,6 +158,45 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) } } + protected void doPostComplete(HttpServletRequest request, HttpServletResponse response) throws ServletException { + //get output folder + // Figure out root file + if(exportUserPass == null || exportUserPass.isEmpty()) { + try { + response.sendError(403); + } catch (Exception e) { + logger.error("Couldn't send 403 response", e); + } + return; + } + String authHeader = request.getHeader("Authorization"); + if (!allowUser(authHeader)) { + try { + response.sendError(401); + } catch (Exception e) { + logger.error("Couldn't send 401 response", e); + } + return; + } + + String realRootPath = servletConfig.getServletContext().getRealPath("."); + try { + String exportName = ExportFullAtlas.createExport(realRootPath, configuration.getCouchDb()); + response.setHeader("Content-Type", "text/plain"); + PrintWriter writer = response.getWriter(); + writer.write(exportName); + writer.close(); + } catch (Exception e) { + logger.error("Error writing full export"); + try { + response.sendError(500); + } catch (Exception e1) { + logger.error("Couldn't send 500 response", e1); + } + return; + } + } + protected void doPostDefinition(HttpServletRequest request, HttpServletResponse response) throws ServletException { // Ignore final path. Allows client to set any download file name @@ -429,6 +500,26 @@ protected void doPostRecords(HttpServletRequest request, HttpServletResponse res } } + private boolean allowUser(String auth) { + if (auth == null) { + return false; // no auth + } + if (!auth.toUpperCase().startsWith("BASIC ")) { + return false; // we only do BASIC + } + // Get encoded user and password, comes after "BASIC " + String userpassEncoded = auth.substring(6); + // Decode it, using any base 64 decoder + String userpassDecoded = new String(Base64.decodeBase64(userpassEncoded)); + + // Check our user list to see if that user and password are "allowed" + if (exportUserPass.equals(userpassDecoded)) { + return true; + } else { + return false; + } + } + private void doGetWelcome(HttpServletRequest request, HttpServletResponse response) throws Exception { try { // Return JSON object to acknowledge the welcome @@ -561,4 +652,53 @@ private void doGetTest(HttpServletRequest request, HttpServletResponse response) throw new Exception("Can not generate test message",e); } } + + private void doGetCompleteList(HttpServletRequest request, HttpServletResponse response) throws IOException{ + String realRootPath = servletConfig.getServletContext().getRealPath("."); + logger.error("realrootpath: " + realRootPath); + List filenames = ExportFullAtlas.getExports(realRootPath); + JSONObject obj = new JSONObject(); + obj.put("filenames", filenames); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.setDateHeader("Expires", (new Date()).getTime()); + + PrintWriter out = response.getWriter(); + out.print(obj); + out.flush(); + } + + private void doGetCompleteFile(String filename, HttpServletRequest request, HttpServletResponse response) throws ServletException { + ServletContext ctx = servletConfig.getServletContext(); + String realRootPath = ctx.getRealPath("."); + logger.error("realrootpath: " + realRootPath); + File file = ExportFullAtlas.getExport(realRootPath, filename); + if(!file.exists()){ + logger.error("Export file does not exist: " + file.getAbsolutePath()); + throw new ServletException("Export File Does Not Exist!"); + } + + + response.setContentLength((int) file.length()); + response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\""); + + try(InputStream in = new FileInputStream(file); + ServletOutputStream out = response.getOutputStream()) { + String mimeType = ctx.getMimeType(file.getAbsolutePath()); + response.setContentType(mimeType != null? mimeType:"application/octet-stream"); + + byte[] buffer = new byte[1024]; + + int numBytesRead; + while ((numBytesRead = in.read(buffer)) > 0) { + out.write(buffer, 0, numBytesRead); + } + } catch (FileNotFoundException e) { + logger.error("Export file not found: " + file.getAbsolutePath(), e); + throw new ServletException("Export File Not Found!"); + } catch (IOException e) { + logger.error("Exception with export file handling: " + file.getAbsolutePath(), e); + throw new ServletException("Error retrieving the file"); + } + } } diff --git a/pom.xml b/pom.xml index 1fa196af6..9d80735f6 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,7 @@ 1.10 1.3.3 2.5 + 1.21 3.9 9.4.46.v20220331 20230618 From aa0271d0347a950c1e6da79417d461392b6aec7c Mon Sep 17 00:00:00 2001 From: Brendan Billingsley Date: Mon, 4 Nov 2024 13:31:29 -0700 Subject: [PATCH 3/6] Add table sorting classes and css (#1252) --- .../js/nunaliit2/css/basic/n2.canvasTable.css | 9 +++++++++ .../src/main/js/nunaliit2/n2.canvasTable.js | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.canvasTable.css b/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.canvasTable.css index b3ad6972e..3a12cdc33 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.canvasTable.css +++ b/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.canvasTable.css @@ -21,3 +21,12 @@ margin: 9px 5px 9px 0px; padding: 2px 5px; } + +.n2TableCanvas .nunaliit-sort-asc-1 a::after { + content: " \25bc"; +} + +.n2TableCanvas .nunaliit-sort-desc-1 a::after { + content: " \25b2"; +} + diff --git a/nunaliit2-js/src/main/js/nunaliit2/n2.canvasTable.js b/nunaliit2-js/src/main/js/nunaliit2/n2.canvasTable.js index b273ac1f6..b0467e81c 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/n2.canvasTable.js +++ b/nunaliit2-js/src/main/js/nunaliit2/n2.canvasTable.js @@ -896,6 +896,26 @@ var TableCanvas = $n2.Class({ ,direction: 1 }); }; + + const headerElements = $('th'); + for(let i = 0; i < headerElements.length; i++){ + $(headerElements[i]).removeClass(); + } + + //remove sort info + for(let i = 0; i < this.sortOrder.length; i++) { + const s = this.sortOrder[i]; + const order = i+1 + //add sort order info to th + const thElement = $(($(`a[data-sort-name=${s.name}]`).parent())[0]) + if(s.direction > 0) { + thElement.addClass('nunaliit-sort-asc') + thElement.addClass(`nunaliit-sort-asc-${order}`) + } else { + thElement.addClass('nunaliit-sort-desc') + thElement.addClass(`nunaliit-sort-desc-${order}`) + } + } this._sortRows(this.sortedRows); From c313040b2a89d531cb019fd0873f06acaf0d6c08 Mon Sep 17 00:00:00 2001 From: alexgao1 Date: Tue, 3 Dec 2024 13:25:20 +0000 Subject: [PATCH 4/6] Add classes to edit, delete, add related buttons if user owned document (#1254) * Add check if document owned by session user, add classes for Edit and Delete button if true * Missing functionality of customService option for hiding Add Related if not logged in * Set classes for add related item button * Add Related button pointer cursor, missing French translations --- .../main/js/nunaliit2/css/basic/n2.theme.css | 6 ++ .../src/main/js/nunaliit2/n2.couchDisplay.js | 78 +++++++++++++------ .../src/main/js/nunaliit2/n2.couchMap.js | 19 +++-- .../src/main/js/nunaliit2/n2.jquery.js | 12 ++- .../src/main/js/nunaliit2/nunaliit2.fr.js | 3 + 5 files changed, 85 insertions(+), 33 deletions(-) diff --git a/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.theme.css b/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.theme.css index 7a68e8608..d038d667b 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.theme.css +++ b/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.theme.css @@ -493,6 +493,12 @@ CreateDocument widget display: inline-block; } +.nunaliit_form_link_add_related_item_wrapper +.nunaliit_form_link.nunaliit_form_link_add_related_item +{ + cursor: pointer; +} + .intro b { font-size: 26px; font-weight: normal; diff --git a/nunaliit2-js/src/main/js/nunaliit2/n2.couchDisplay.js b/nunaliit2-js/src/main/js/nunaliit2/n2.couchDisplay.js index 67ffc40ae..76bb0a446 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/n2.couchDisplay.js +++ b/nunaliit2-js/src/main/js/nunaliit2/n2.couchDisplay.js @@ -524,39 +524,63 @@ var Display = $n2.Class({ // Show 'edit' button if( opt.edit && $n2.couchMap.canEditDoc(data) ) { - buttonDisplay.drawButton({ - elem: $buttons - ,name: 'edit' - ,label: _loc('Edit') - ,click: function(){ - _this._performDocumentEdit(data, opt); - } - }); + const editButtonCfg = { + elem: $buttons + ,name: 'edit' + ,label: _loc('Edit') + ,click: function(){ + _this._performDocumentEdit(data, opt); + } + } + if ($n2.couchMap.documentOwnedBySessionUser(data)) { + editButtonCfg.classNames = ['n2_document_user_owned_editable'] + } + buttonDisplay.drawButton(editButtonCfg); }; // Show 'delete' button if( opt['delete'] && $n2.couchMap.canDeleteDoc(data) ) { - buttonDisplay.drawButton({ - elem: $buttons - ,name: 'delete' - ,label: _loc('Delete') - ,click: function(){ - _this._performDocumentDelete(data, opt); - } - }); + const deleteButtonCfg = { + elem: $buttons + ,name: 'delete' + ,label: _loc('Delete') + ,click: function(){ + _this._performDocumentDelete(data, opt); + } + } + if ($n2.couchMap.documentOwnedBySessionUser(data)) { + deleteButtonCfg.classNames = ['n2_document_user_owned_deletable'] + } + buttonDisplay.drawButton(deleteButtonCfg); }; // Show 'add related' button if( opt.related && this.displayRelatedInfoProcess ) { - this.displayRelatedInfoProcess.addButton({ - display: this - ,div: $buttons[0] - ,doc: data - ,schema: opt.schema - ,buttonDisplay: buttonDisplay - }); + let showAddRelatedButton = true + if (this.restrictAddRelatedButtonToLoggedIn) { + var isLoggedInMsg = { + type: 'authIsLoggedIn' + , isLoggedIn: false + } + if (dispatcher) { + dispatcher.synchronousCall(DH, isLoggedInMsg); + } + if (!isLoggedInMsg.isLoggedIn) { + showAddRelatedButton = false; + } + } + if (showAddRelatedButton) { + this.displayRelatedInfoProcess.addButton({ + display: this + ,div: $buttons[0] + ,doc: data + ,schema: opt.schema + ,buttonDisplay: buttonDisplay + }); + } + }; // Show 'reply' button @@ -1466,8 +1490,12 @@ var LegacyDisplayRelatedFunctionAdapter = $n2.Class({ ,onElementCreated: function($addRelatedButton){ $addRelatedButton.addClass('nunaliit_form_link'); $addRelatedButton.addClass('nunaliit_form_link_add_related_item'); - - $addRelatedButton.menuselector(); + const options = {} + if ($n2.couchMap.documentOwnedBySessionUser(this.doc)) { + options.wrapperSpanClass = 'n2_document_user_owned_can_add_related' + } + + $addRelatedButton.menuselector(options); } ,onRelatedDocumentCreated: function(docId){} }); diff --git a/nunaliit2-js/src/main/js/nunaliit2/n2.couchMap.js b/nunaliit2-js/src/main/js/nunaliit2/n2.couchMap.js index 62a058e57..6bfc0042b 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/n2.couchMap.js +++ b/nunaliit2-js/src/main/js/nunaliit2/n2.couchMap.js @@ -159,11 +159,7 @@ function canEditDoc(data) { // If a document is not on a controlled layer, then the creator of the // document can edit it - if( data.nunaliit_created - && data.nunaliit_created.nunaliit_type - && data.nunaliit_created.nunaliit_type === 'actionstamp' - && data.nunaliit_created.name === userName - ) { + if (documentOwnedBySessionUser(data)) { return true; }; @@ -177,6 +173,18 @@ function canDeleteDoc(data) { return canEditDoc(data); }; +function documentOwnedBySessionUser(doc) { + const sessionContext = getCurrentContext() + if (sessionContext) { + const username = sessionContext.name + return (doc + && doc.nunaliit_created + && doc.nunaliit_created.nunaliit_type === 'actionstamp' + && doc.nunaliit_created.name === username) + } + return false +}; + function documentContainsMedia(doc){ var containsMedia = false; @@ -238,6 +246,7 @@ $n2.couchMap = { ,documentContainsMedia: documentContainsMedia ,documentContainsApprovedMedia: documentContainsApprovedMedia ,documentContainsDeniedMedia: documentContainsDeniedMedia + ,documentOwnedBySessionUser }; })(nunaliit2); diff --git a/nunaliit2-js/src/main/js/nunaliit2/n2.jquery.js b/nunaliit2-js/src/main/js/nunaliit2/n2.jquery.js index 305fda1c6..b8af04412 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/n2.jquery.js +++ b/nunaliit2-js/src/main/js/nunaliit2/n2.jquery.js @@ -91,7 +91,8 @@ if( typeof $.widget === 'function' ){ $.widget( 'nunaliit.menuselector', { options: { - menuClass: null + menuClass: null, + spanClass: null } ,_create: function() { @@ -99,8 +100,13 @@ if( typeof $.widget === 'function' ){ this.wrapper = $('') .addClass('nunaliit-menuselector') + .addClass('nunaliit_form_link_add_related_item_wrapper') .insertAfter(this.element); - + + if (this.options.wrapperSpanClass) { + this.wrapper.addClass(this.options.wrapperSpanClass) + } + var classes = this.element.attr('class'); var text = this.element.find('option').first().text(); @@ -126,7 +132,7 @@ if( typeof $.widget === 'function' ){ .css('z-index',1000) .hide() .appendTo(this.wrapper); - + this.element.hide(); } diff --git a/nunaliit2-js/src/main/js/nunaliit2/nunaliit2.fr.js b/nunaliit2-js/src/main/js/nunaliit2/nunaliit2.fr.js index e4de8d379..02ae5759c 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/nunaliit2.fr.js +++ b/nunaliit2-js/src/main/js/nunaliit2/nunaliit2.fr.js @@ -681,6 +681,9 @@ $n2.l10n.addLocalizedStrings('fr',{ ,"{index}/{count}":"{index}/{count}" ,"{label} definition ({key}) already exists - not loaded or updated":"Définition {label} ({key}) existe déjà - ignorée" ,"No documents found.": "Aucun document n'a été trouvé." + ,"File Upload": "Téléchargement de fichiers" + ,"Record Audio": "Enregistrer l'audio" + ,"Record Video": "Enregistrer une vidéo" }); if( $ && $.datepicker ) { From c402b8c9bc21a7b7c1d0a2829f82e6624010a292 Mon Sep 17 00:00:00 2001 From: alexgao1 Date: Tue, 7 Jan 2025 15:50:19 -0500 Subject: [PATCH 5/6] Feature/email on submission approval (#1255) * approve add dialog * approval function * set submission doc approval properties * init WIP todo generate approval mail function in implementation * reference * Add submission_accepted email template and mail generator * Set submission accept generator in mail notification implementation * typo * note --- .../_attachments/tools/css/submission.css | 10 ++ .../_attachments/tools/js/submission.js | 158 ++++++++++++++---- .../couch/command/servlet/ConfigServlet.java | 10 ++ .../_id.txt | 1 + .../nunaliit_email_template/body.txt | 12 ++ .../nunaliit_email_template/subject.txt | 1 + .../nunaliit_schema.txt | 1 + .../impl/SubmissionRobotThread.java | 91 ++++++++++ .../mail/SubmissionAcceptedGenerator.java | 37 ++++ .../mail/SubmissionMailNotifier.java | 4 +- .../mail/SubmissionMailNotifierImpl.java | 61 +++++++ .../mail/SubmissionMailNotifierNull.java | 6 + 12 files changed, 354 insertions(+), 38 deletions(-) create mode 100644 nunaliit2-couch-sdk/src/main/content/docs/org.nunaliit.email_template.submission_accepted/_id.txt create mode 100644 nunaliit2-couch-sdk/src/main/content/docs/org.nunaliit.email_template.submission_accepted/nunaliit_email_template/body.txt create mode 100644 nunaliit2-couch-sdk/src/main/content/docs/org.nunaliit.email_template.submission_accepted/nunaliit_email_template/subject.txt create mode 100644 nunaliit2-couch-sdk/src/main/content/docs/org.nunaliit.email_template.submission_accepted/nunaliit_schema.txt create mode 100644 nunaliit2-couch-submission/src/main/java/ca/carleton/gcrc/couch/submission/mail/SubmissionAcceptedGenerator.java diff --git a/nunaliit2-couch-application/src/main/atlas_couchapp/_attachments/tools/css/submission.css b/nunaliit2-couch-application/src/main/atlas_couchapp/_attachments/tools/css/submission.css index f3b3647f6..dff367088 100644 --- a/nunaliit2-couch-application/src/main/atlas_couchapp/_attachments/tools/css/submission.css +++ b/nunaliit2-couch-application/src/main/atlas_couchapp/_attachments/tools/css/submission.css @@ -91,6 +91,16 @@ textarea.submission_deny_dialog_reason { margin-bottom: 5px; } +textarea.submission_approve_dialog_message { + width: 460px; + height: 125px; +} + +.submission_approve_dialog_options { + margin-top: 5px; + margin-bottom: 5px; +} + span.patchSelected, div.patchSelected { font-weight: bold; color: #00ff00; diff --git a/nunaliit2-couch-application/src/main/atlas_couchapp/_attachments/tools/js/submission.js b/nunaliit2-couch-application/src/main/atlas_couchapp/_attachments/tools/js/submission.js index bb5df6bfd..fcbb12731 100644 --- a/nunaliit2-couch-application/src/main/atlas_couchapp/_attachments/tools/js/submission.js +++ b/nunaliit2-couch-application/src/main/atlas_couchapp/_attachments/tools/js/submission.js @@ -278,45 +278,128 @@ }); } - ,_approve: function(subDocId, approvedDoc){ + ,_approve: function(subDocId, approvedDoc, approveFn){ var _this = this; - this._getSubmissionDocument({ - subDocId: subDocId - ,onSuccess: function(subDoc){ - subDoc.nunaliit_submission.state = 'approved'; - $n2.couchDocument.adjustDocument(subDoc); - - if( approvedDoc ){ - subDoc.nunaliit_submission.approved_doc = {}; - subDoc.nunaliit_submission.approved_reserved = {}; - for(var key in approvedDoc){ - if( key.length > 0 && key[0] === '_' ) { - var effectiveKey = key.substr(1); - subDoc.nunaliit_submission.approved_reserved[effectiveKey] = - approvedDoc[key]; - } else { - subDoc.nunaliit_submission.approved_doc[key] = - approvedDoc[key]; + collectApprovalMessage(function(message, sendEmail) { + _this._getSubmissionDocument({ + subDocId: subDocId + ,onSuccess: function(subDoc){ + subDoc.nunaliit_submission.state = 'approved'; + $n2.couchDocument.adjustDocument(subDoc); + + if( approvedDoc ){ + + if (message && message !== '') { + subDoc.nunaliit_submission.approval_message = message; + } + + if (sendEmail) { + subDoc.nunaliit_submission.approval_email = { + requested: true + } + } + + subDoc.nunaliit_submission.approved_doc = {}; + subDoc.nunaliit_submission.approved_reserved = {}; + for(var key in approvedDoc){ + if( key.length > 0 && key[0] === '_' ) { + var effectiveKey = key.substr(1); + subDoc.nunaliit_submission.approved_reserved[effectiveKey] = + approvedDoc[key]; + } else { + subDoc.nunaliit_submission.approved_doc[key] = + approvedDoc[key]; + }; }; }; - }; - - _this.submissionDb.updateDocument({ - data: subDoc - ,onSuccess: function(docInfo){ - _this.logger.log( _loc('Submission approved') ); - _this._refreshSubmissions(); - } - ,onError: function(err){ - _this.logger.error( _loc('Unable to update submission document: {err}',{err:err}) ); - } + + _this.submissionDb.updateDocument({ + data: subDoc + ,onSuccess: function(docInfo){ + _this.logger.log( _loc('Submission approved') ); + if( typeof approveFn === 'function' ){ + approveFn(); + }; + _this._refreshSubmissions(); + } + ,onError: function(err){ + _this.logger.error( _loc('Unable to update submission document: {err}',{err:err}) ); + } + }); + } + ,onError: function(err){ + _this.logger.error( _loc('Unable to obtain submission document: {err}',{err:err}) ); + } + }); + }) + + function collectApprovalMessage(callback, sendEmail){ + var diagId = $n2.getUniqueId(); + var $diag = $('
') + .attr('id',diagId) + .addClass('submission_approve_dialog') + .appendTo( $('body') ); + + $('