1
0
mirror of https://github.com/sismics/docs.git synced 2025-12-14 10:16:21 +00:00

62 Commits
v1.6 ... v1.8

Author SHA1 Message Date
bgamard
6367a1fd15 v1.8 2020-03-26 19:57:10 +01:00
bgamard
2c5ff64d42 Closes #387: validation username and group name in UI 2020-03-25 19:02:50 +01:00
bgamard
e614cb41d8 update feedback api url 2020-03-25 18:13:49 +01:00
bgamard
82737e2280 Closes #334: highlight previously opened file 2020-03-07 17:56:01 +01:00
bgamard
3b5c27096b Closes #350: better relations widget 2020-03-07 17:46:40 +01:00
bgamard
8a85830bd3 #381: date fields manually editable 2020-03-07 00:36:03 +01:00
bgamard
19ac90688e upgrade guava 2020-02-19 20:47:26 +01:00
bgamard
5f4a6bc462 #336: fix search by type 2020-02-19 18:00:13 +01:00
bgamard
4c7f3166d4 Closes #332: tag text color legibility 2020-02-15 23:00:35 +01:00
bgamard
4233f4dd88 Closes #317: edit tag color hex code manually 2020-02-15 22:38:06 +01:00
bgamard
bd09312418 Closes #336: search document by file mime type 2020-02-15 22:05:04 +01:00
bgamard
11ab07b238 Closes #333: fix overflow in document table with a lot of tags 2020-02-15 15:44:32 +01:00
bgamard
d2e2f089fb update README for build deps 2020-02-14 21:48:45 +01:00
bgamard
d619f98de7 Closes #379: spaces and colons not allowed in tag name 2020-02-14 21:40:13 +01:00
bgamard
89228a52dc Closes #378: silence useless log from Jersey 2020-02-14 21:33:34 +01:00
bgamard
90a49efa4a Closes #373: high quality PDF to image conversion before OCR 2020-02-13 17:43:07 +01:00
junpet
a7423caeb1 Add Hungarian Language Support (#369)
Add Hungarian language support
2020-01-29 15:44:44 +01:00
bgamard
6f31a2c228 Closes #366: get the private key from the right user when processing files 2020-01-21 12:54:50 +01:00
Benjamin Gamard
fc98b0882f Merge remote-tracking branch 'origin/master' 2019-12-28 13:54:45 +01:00
Benjamin Gamard
dff05967ea Closes #363: pgsql compatibility for table alias in update queries 2019-12-28 13:53:42 +01:00
Benjamin Gamard
ec836a2f9d Update README.md 2019-10-13 01:24:10 +02:00
Benjamin Gamard
737c85cf00 Update README.md 2019-10-13 01:23:03 +02:00
Benjamin Gamard
ff7b07f464 Create FUNDING.yml 2019-10-12 23:05:35 +02:00
Mario Voigt
19422b5afa Fixed some language issue (#352) 2019-08-26 14:19:19 +02:00
Benjamin Gamard
6b93e413b6 #321: remove duplicate contributors 2019-06-03 11:45:38 +02:00
Mario Voigt
ab72736bcc Update de.json (#322)
Added missing german translation strings
2019-05-25 18:53:02 +02:00
Benjamin Gamard
38939e5d05 #314: force file content in utf8 2019-05-22 15:36:50 +02:00
Benjamin Gamard
1a90a0e0ad next dev iteration 2019-05-21 16:17:54 +02:00
Benjamin Gamard
7aa9fa4646 v1.7 2019-05-21 15:55:41 +02:00
Benjamin Gamard
82d788c8d3 Closes #300: adjust tests 2019-05-21 15:53:54 +02:00
Benjamin Gamard
ab8176efcb #300: custom metadata fields: UI read/write 2019-05-21 15:44:23 +02:00
Benjamin Gamard
b4c3e7a928 update Docker image infos 2019-05-20 15:18:08 +02:00
Benjamin Gamard
2db263fb68 #300: custom metadata fields: API read 2019-05-20 15:08:16 +02:00
Benjamin Gamard
5fd4d37972 #300: custom metadata fields: API write 2019-05-17 16:00:03 +02:00
Benjamin Gamard
9b1dbf351a #300: custom metadata fields: UI admin 2019-05-15 14:15:55 +02:00
Benjamin Gamard
4c7c058e0d Merge remote-tracking branch 'origin/master' 2019-05-15 13:34:21 +02:00
Benjamin Gamard
f8dc08b02b #300: custom metadata fields: API admin 2019-05-15 13:34:01 +02:00
yosef langer
0e6bc3ce54 Hebrew Language Support (#320) 2019-05-11 16:26:01 +02:00
Benjamin Gamard
fcb018406d Merge remote-tracking branch 'origin/master' 2019-05-07 14:38:15 +02:00
Benjamin Gamard
40756a5e4b Fix for https://bugs.openjdk.java.net/browse/JDK-8216039 2019-05-07 14:38:05 +02:00
Benjamin Gamard
61b12bdebd Closes #309: store onboarding status server side 2019-05-06 18:12:44 +02:00
Benjamin Gamard
8b1c41ae1e Closes #305: exclude tags from search 2019-05-03 15:35:47 +02:00
Benjamin Gamard
d654564f6b Closes #318: Sort file list on drag & drop before sending to server 2019-05-03 15:09:43 +02:00
Benjamin Gamard
8bd22ebafa Closes #301: download link in overflow menu 2019-05-03 14:09:35 +02:00
Benjamin Gamard
647e66d57b update api doc 2019-05-03 13:55:18 +02:00
Benjamin Gamard
67c8ac1aa3 Closes #307: log workflow create/delete in document logs 2019-05-03 13:54:10 +02:00
Benjamin Gamard
f336c7ae53 Closes #313: remove administrators from ACL targets search 2019-05-03 13:27:23 +02:00
Benjamin Gamard
9ea1dad62d Closes #296: firefox hack to prevent file open on drag & drop 2019-05-03 13:19:21 +02:00
Benjamin Gamard
58bc374e64 Closes #306: Prevent deleting/renaming users/groups used in route models 2019-05-02 16:19:50 +02:00
Benjamin Gamard
cea0d4887d Closes #302: increase version history modal size 2019-05-02 11:19:08 +02:00
Benjamin Gamard
d5e73ecd8b Closes #308: update user quota when deleting a document 2019-05-02 11:13:16 +02:00
Benjamin Gamard
2235a0498b Merge remote-tracking branch 'origin/master' 2019-05-02 10:54:28 +02:00
Benjamin Gamard
3f9b92831c Closes #312: fix file importer description 2019-05-02 10:54:19 +02:00
Benjamin Gamard
5680750c82 Merge remote-tracking branch 'origin/master' 2019-05-01 17:36:27 +02:00
Benjamin Gamard
298e3efe49 Spanish translation 2019-05-01 17:35:35 +02:00
Burak Sormageç
7b2bd6f9eb Turkish Language Support (#304)
Add turkish language support
2019-03-07 20:34:02 +01:00
Benjamin Gamard
d935e07990 pgsql compatibility 2019-02-12 14:55:12 +01:00
Benjamin Gamard
868a74c184 sismics docs -> teedy 2019-02-12 13:57:54 +01:00
Benjamin Gamard
a86af9736b fix test 2019-02-07 16:55:58 +01:00
Benjamin Gamard
8bd4d27d2f increase h2 lock timeout 2019-02-07 15:17:38 +01:00
Benjamin Gamard
94951c59f3 quick test 2019-02-07 15:08:17 +01:00
Benjamin Gamard
e39c83a5a6 update the file ID on a document with a native query 2019-02-07 14:50:47 +01:00
151 changed files with 3579 additions and 794 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [jendib]

View File

@@ -4,7 +4,7 @@ language: java
before_install:
- sudo add-apt-repository -y ppa:mc3man/trusty-media
- sudo apt-get -qq update
- sudo apt-get -y -q install ffmpeg mediainfo tesseract-ocr tesseract-ocr-fra tesseract-ocr-ita tesseract-ocr-kor tesseract-ocr-rus tesseract-ocr-ukr tesseract-ocr-spa tesseract-ocr-ara tesseract-ocr-hin tesseract-ocr-deu tesseract-ocr-pol tesseract-ocr-jpn tesseract-ocr-por tesseract-ocr-tha tesseract-ocr-jpn tesseract-ocr-chi-sim tesseract-ocr-chi-tra tesseract-ocr-nld
- sudo apt-get -y -q install ffmpeg mediainfo tesseract-ocr tesseract-ocr-fra tesseract-ocr-ita tesseract-ocr-kor tesseract-ocr-rus tesseract-ocr-ukr tesseract-ocr-spa tesseract-ocr-ara tesseract-ocr-hin tesseract-ocr-deu tesseract-ocr-pol tesseract-ocr-jpn tesseract-ocr-por tesseract-ocr-tha tesseract-ocr-jpn tesseract-ocr-chi-sim tesseract-ocr-chi-tra tesseract-ocr-nld tesseract-ocr-tur tesseract-ocr-heb tesseract-ocr-hun
- sudo apt-get -y -q install haveged && sudo service haveged start
after_success:
- |

View File

@@ -1,7 +1,7 @@
FROM sismics/ubuntu-jetty:9.4.12
MAINTAINER b.gamard@sismics.com
RUN apt-get update && apt-get -y -q install ffmpeg mediainfo tesseract-ocr tesseract-ocr-fra tesseract-ocr-ita tesseract-ocr-kor tesseract-ocr-rus tesseract-ocr-ukr tesseract-ocr-spa tesseract-ocr-ara tesseract-ocr-hin tesseract-ocr-deu tesseract-ocr-pol tesseract-ocr-jpn tesseract-ocr-por tesseract-ocr-tha tesseract-ocr-jpn tesseract-ocr-chi-sim tesseract-ocr-chi-tra tesseract-ocr-nld && \
RUN apt-get update && apt-get -y -q install ffmpeg mediainfo tesseract-ocr tesseract-ocr-fra tesseract-ocr-ita tesseract-ocr-kor tesseract-ocr-rus tesseract-ocr-ukr tesseract-ocr-spa tesseract-ocr-ara tesseract-ocr-hin tesseract-ocr-deu tesseract-ocr-pol tesseract-ocr-jpn tesseract-ocr-por tesseract-ocr-tha tesseract-ocr-jpn tesseract-ocr-chi-sim tesseract-ocr-chi-tra tesseract-ocr-nld tesseract-ocr-tur tesseract-ocr-heb tesseract-ocr-hun && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Remove the embedded javax.mail jar from Jetty

View File

@@ -1,27 +1,25 @@
<h3 align="center">
<img src="https://www.sismicsdocs.com/img/github-title.png" alt="Sismics Docs" width=500 />
<img src="https://teedy.io/img/github-title.png" alt="Teedy" width=500 />
</h3>
[![Twitter: @sismicsdocs](https://img.shields.io/badge/contact-@sismicsdocs-blue.svg?style=flat)](https://twitter.com/sismicsdocs)
[![Twitter: @teedyio](https://img.shields.io/badge/contact-@teedyio-blue.svg?style=flat)](https://twitter.com/teedyio)
[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-blue.svg)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
[![Build Status](https://secure.travis-ci.org/sismics/docs.png)](http://travis-ci.org/sismics/docs)
Docs is an open source, lightweight document management system for individuals and businesses.
**Discuss it on [Product Hunt](https://www.producthunt.com/posts/sismics-docs) 🦄**
Teedy is an open source, lightweight document management system for individuals and businesses.
<hr />
<h2 align="center">
We just launched a Cloud version of Sismics Docs! Head to <a href="https://www.sismicsdocs.com/">sismicsdocs.com</a> for more informations
<a href="https://github.com/users/jendib/sponsorship">Sponsor this project if you use and appreciate it!</a>
</h2>
<hr />
![New!](https://www.sismicsdocs.com/img/laptop-demo.png?20180301)
![New!](https://teedy.io/img/laptop-demo.png?20180301)
Demo
----
A demo is available at [demo.sismicsdocs.com](https://demo.sismicsdocs.com)
A demo is available at [demo.teedy.io](https://demo.teedy.io)
- Guest login is enabled with read access on all documents
- "admin" login with "admin" password
- "demo" login with "password" password
@@ -36,6 +34,7 @@ Features
- Flexible search engine with suggestions and highlighting
- Full text search in all supported files
- All [Dublin Core](http://dublincore.org/) metadata
- Custom user-defined metadata ![New!](https://www.sismics.com/public/img/new.png)
- Workflow system ![New!](https://www.sismics.com/public/img/new.png)
- 256-bit AES encryption of stored files
- File versioning ![New!](https://www.sismics.com/public/img/new.png)
@@ -58,14 +57,15 @@ Features
Install with Docker
-------------------
From a Docker host, run this command to download and install Sismics Docs. The server will run on <http://[your-docker-host-ip]:8100>.
A preconfigured Docker image is available, including OCR and media conversion tools, listening on port 8080. The database is an embedded H2 database but PostgreSQL is also supported for more performance.
**The default admin password is "admin". Don't forget to change it before going to production.**
- Master branch, can be unstable. Not recommended for production use: `sismics/docs:latest`
- Latest stable version: `sismics/docs:v1.7`
docker run --rm --name sismics_docs_latest -d -e DOCS_BASE_URL='http://[your-docker-host-ip]:8100' -p 8100:8080 -v sismics_docs_latest:/data sismics/docs:latest
<img src="http://www.newdesignfile.com/postpic/2011/01/green-info-icon_206509.png" width="16px" height="16px"> **Note:** You will need to change [your-docker-host-ip] with the IP address or FQDN of your docker host e.g.
The data directory is `/data`. Don't forget to mount a volume on it.
FQDN: http://docs.sismics.com
IP: http://192.168.100.10
To build external URL, the server is expecting a `DOCS_BASE_URL` environment variable (for example https://teedy.mycompany.com)
Manual installation
-------------------
@@ -81,12 +81,12 @@ Manual installation
The latest release is downloadable here: <https://github.com/sismics/docs/releases> in WAR format.
**The default admin password is "admin". Don't forget to change it before going to production.**
How to build Docs from the sources
How to build Teedy from the sources
----------------------------------
Prerequisites: JDK 8 with JCE, Maven 3, Tesseract 3 or 4
Prerequisites: JDK 8 with JCE, Maven 3, NPM, Grunt, Tesseract 3 or 4
Docs is organized in several Maven modules:
Teedy is organized in several Maven modules:
- docs-core
- docs-web
@@ -122,19 +122,8 @@ All contributions are more than welcomed. Contributions may close an issue, fix
The `master` branch is the default and base branch for the project. It is used for development and all Pull Requests should go there.
Community
---------
Get updates on Sismics Docs' development and chat with the project maintainers:
- Follow [@sismicsdocs on Twitter](https://twitter.com/sismicsdocs)
- Read and subscribe to [The Official Sismics Docs Blog](https://blog.sismicsdocs.com/)
- Check the [Official Website](https://www.sismicsdocs.com)
- Join us [on Facebook](https://www.facebook.com/sismicsdocs)
License
-------
Docs is released under the terms of the GPL license. See `COPYING` for more
Teedy is released under the terms of the GPL license. See `COPYING` for more
information or see <http://opensource.org/licenses/GPL-2.0>.

View File

@@ -4,7 +4,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.0'
classpath 'com.android.tools.build:gradle:3.4.0'
}
}
apply plugin: 'com.android.application'

View File

@@ -15,7 +15,7 @@
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
android:theme="@style/AppTheme">
<activity
android:name=".activity.LoginActivity"
android:label="@string/app_name"

View File

@@ -1,6 +1,7 @@
package com.sismics.docs;
import android.app.Application;
import android.support.v7.app.AppCompatDelegate;
import com.sismics.docs.model.application.ApplicationContext;
import com.sismics.docs.util.PreferenceUtil;
@@ -22,5 +23,7 @@ public class MainApplication extends Application {
// TODO Provide documents to intent action get content
super.onCreate();
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
}
}

View File

@@ -63,14 +63,13 @@ public class DocListFragment extends Fragment {
recyclerView.setAdapter(adapter);
recyclerView.setHasFixedSize(true);
recyclerView.setLongClickable(true);
recyclerView.addItemDecoration(new DividerItemDecoration(getResources().getDrawable(R.drawable.abc_list_divider_mtrl_alpha)));
// Configure the LayoutManager
final LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
// Configure the swipe refresh layout
swipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.swipeRefreshLayout);
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout);
swipeRefreshLayout.setColorSchemeResources(android.R.color.holo_blue_bright,
android.R.color.holo_green_light,
android.R.color.holo_orange_light,
@@ -194,7 +193,7 @@ public class DocListFragment extends Fragment {
private void loadDocuments(final View view, final boolean reset) {
if (view == null) return;
final View progressBar = view.findViewById(R.id.progressBar);
final TextView documentsEmptyView = (TextView) view.findViewById(R.id.documentsEmptyView);
final TextView documentsEmptyView = view.findViewById(R.id.documentsEmptyView);
if (reset) {
loading = true;

View File

@@ -156,7 +156,7 @@ public class OkHttpUtil {
public static OkHttpClient buildClient(final Context context) {
// One-time header computation
if (userAgent == null) {
userAgent = "Sismics Docs Android " + ApplicationUtil.getVersionName(context) + "/Android " + Build.VERSION.RELEASE + "/" + Build.MODEL;
userAgent = "Teedy Android " + ApplicationUtil.getVersionName(context) + "/Android " + Build.VERSION.RELEASE + "/" + Build.MODEL;
}
if (acceptLanguage == null) {

View File

@@ -29,7 +29,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"
android:textColor="#212121"
android:textColor="?android:attr/textColorPrimary"
android:text="Test"
android:textSize="16sp"
android:ellipsize="end"
@@ -46,7 +46,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"
android:textColor="#777777"
android:textColor="?android:attr/textColorPrimary"
android:text="test2"
android:textSize="16sp"
android:maxLines="1"
@@ -69,7 +69,7 @@
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:textColor="#777777"
android:textColor="?android:attr/textColorPrimary"
android:fontFamily="sans-serif-light"/>
</RelativeLayout>

View File

@@ -9,23 +9,22 @@
<android.support.design.widget.CoordinatorLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/overview_coordinator_layout"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:layout_scrollFlags="enterAlways|scroll|snap" />
android:layout_height="wrap_content">
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/AppTheme"
app:layout_scrollFlags="enterAlways|scroll|snap" />
</android.support.design.widget.AppBarLayout>
<fragment
android:id="@+id/main_fragment"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -11,7 +11,7 @@
<!-- App -->
<string name="drawer_open">Navigationsleiste öffnen</string>
<string name="drawer_close">Navigationsleiste schließen</string>
<string name="login_explain"><![CDATA[Um zu beginnen, müssen Sie Sismics Docs Server herunterladen und installieren <a href="https://github.com/sismics/docs">github.com/sismics/docs</a>, sowie die Login-Daten unten eingeben]]></string>
<string name="login_explain"><![CDATA[Um zu beginnen, müssen Sie Teedy Server herunterladen und installieren <a href="https://github.com/sismics/docs">github.com/sismics/docs</a>, sowie die Login-Daten unten eingeben]]></string>
<string name="server">Server</string>
<string name="username">Username</string>
<string name="password">Password</string>
@@ -83,7 +83,7 @@
<string name="file_delete_failure">Netzwerkfehler beim Löschen der Datei</string>
<string name="file_deleting_message">Lösche Datei</string>
<string name="error_reading_file">Fehler beim Lesen der Datei</string>
<string name="upload_notification_title">Sismics Docs</string>
<string name="upload_notification_title">Teedy</string>
<string name="upload_notification_message">Neue Datei in das Dokument hochladen</string>
<string name="upload_notification_error">Fehler beim Hochladen der neuen Datei</string>
<string name="delete_file">Aktuelle Datei löschen</string>
@@ -119,9 +119,9 @@
<string name="export_comments">Kommentare exportieren</string>
<string name="export_metadata">Metadaten exportieren</string>
<string name="mm">mm</string>
<string name="download_file_title">Sismics Docs Datei Export</string>
<string name="download_document_title">Sismics Docs Dokumentenexport</string>
<string name="download_pdf_title">Sismics Docs PDF Export</string>
<string name="download_file_title">Teedy Datei Export</string>
<string name="download_document_title">Teedy Dokumentenexport</string>
<string name="download_pdf_title">Teedy PDF Export</string>
<string name="latest_activity">Letzte Aktivität</string>
<string name="activity">Aktivitäten</string>
<string name="email">E-Mail</string>

View File

@@ -11,7 +11,7 @@
<!-- App -->
<string name="drawer_open">Ouvrir le menu de navigation</string>
<string name="drawer_close">Fermer le menu de navigation</string>
<string name="login_explain"><![CDATA[Pour commencer, vous devez télécharger et installer le serveur Sismics Docs sur <a href="https://github.com/sismics/docs">github.com/sismics/docs</a> et entrer son URL ci-dessous]]></string>
<string name="login_explain"><![CDATA[Pour commencer, vous devez télécharger et installer le serveur Teedy sur <a href="https://github.com/sismics/docs">github.com/sismics/docs</a> et entrer son URL ci-dessous]]></string>
<string name="server">Serveur</string>
<string name="username">Nom d\'utilisateur</string>
<string name="password">Mot de passe</string>
@@ -83,7 +83,7 @@
<string name="file_delete_failure">Erreur réseau lors de la suppression du fichier</string>
<string name="file_deleting_message">Suppression du fichier</string>
<string name="error_reading_file">Erreur lors de la lecture du fichier</string>
<string name="upload_notification_title">Sismics Docs</string>
<string name="upload_notification_title">Teedy</string>
<string name="upload_notification_message">Envoi du nouveau fichier</string>
<string name="upload_notification_error">Erreur lors de l\'envoi du nouveau fichier</string>
<string name="delete_file">Supprimer ce fichier</string>
@@ -119,9 +119,9 @@
<string name="export_comments">Exporter les commentaires</string>
<string name="export_metadata">Exporter les métadonnées</string>
<string name="mm">mm</string>
<string name="download_file_title">Export de fichier Sismics Docs</string>
<string name="download_document_title">Export de document Sismics Docs</string>
<string name="download_pdf_title">Export PDF Sismics Docs</string>
<string name="download_file_title">Export de fichier Teedy</string>
<string name="download_document_title">Export de document Teedy</string>
<string name="download_pdf_title">Export PDF Teedy</string>
<string name="latest_activity">Activité récente</string>
<string name="activity">Activité</string>
<string name="email">E-mail</string>

View File

@@ -9,10 +9,10 @@
<string name="validate_error_alphanumeric">Only letters and numbers</string>
<!-- App -->
<string name="app_name" translatable="false">Sismics Docs</string>
<string name="app_name" translatable="false">Teedy</string>
<string name="drawer_open">Open navigation drawer</string>
<string name="drawer_close">Close navigation drawer</string>
<string name="login_explain"><![CDATA[To start, you must download and install Sismics Docs Server on <a href="https://github.com/sismics/docs">github.com/sismics/docs</a> and enter its below]]></string>
<string name="login_explain"><![CDATA[To start, you must download and install Teedy Server on <a href="https://github.com/sismics/docs">github.com/sismics/docs</a> and enter its below]]></string>
<string name="server">Server</string>
<string name="username">Username</string>
<string name="password">Password</string>
@@ -87,7 +87,7 @@
<string name="file_delete_failure">Network error while deleting the current file</string>
<string name="file_deleting_message">Deleting file</string>
<string name="error_reading_file">Error while reading the file</string>
<string name="upload_notification_title">Sismics Docs</string>
<string name="upload_notification_title">Teedy</string>
<string name="upload_notification_message">Uploading the new file to the document</string>
<string name="upload_notification_error">Error uploading the new file</string>
<string name="delete_file">Delete current file</string>
@@ -123,9 +123,9 @@
<string name="export_comments">Export comments</string>
<string name="export_metadata">Export metadata</string>
<string name="mm">mm</string>
<string name="download_file_title">Sismics Docs file export</string>
<string name="download_document_title">Sismics Docs document export</string>
<string name="download_pdf_title">Sismics Docs PDF export</string>
<string name="download_file_title">Teedy file export</string>
<string name="download_document_title">Teedy document export</string>
<string name="download_pdf_title">Teedy PDF export</string>
<string name="latest_activity">Latest activity</string>
<string name="activity">Activity</string>
<string name="email">E-mail</string>

View File

@@ -1,12 +1,12 @@
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<style name="AppTheme" parent="Theme.AppCompat.DayNight">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.Light.DarkActionBar">
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="colorPrimary">@color/colorPrimary</item>
@@ -14,7 +14,7 @@
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppThemeDark" parent="Theme.AppCompat.NoActionBar">
<style name="AppThemeDark" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>

View File

@@ -1,6 +1,6 @@
#Wed Jan 30 16:31:31 CET 2019
#Tue May 07 11:49:13 CEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.sismics.docs</groupId>
<artifactId>docs-parent</artifactId>
<version>1.6-SNAPSHOT</version>
<version>1.8</version>
<relativePath>..</relativePath>
</parent>
@@ -190,6 +190,25 @@
<artifactId>postgresql</artifactId>
</dependency>
<!-- JDK 11 JAXB dependencies -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>junit</groupId>

View File

@@ -38,7 +38,7 @@ public class Constants {
/**
* Supported document languages.
*/
public static final List<String> SUPPORTED_LANGUAGES = Lists.newArrayList("eng", "fra", "ita", "deu", "spa", "por", "pol", "rus", "ukr", "ara", "hin", "chi_sim", "chi_tra", "jpn", "tha", "kor", "nld");
public static final List<String> SUPPORTED_LANGUAGES = Lists.newArrayList("eng", "fra", "ita", "deu", "spa", "por", "pol", "rus", "ukr", "ara", "hin", "chi_sim", "chi_tra", "jpn", "tha", "kor", "nld", "tur", "heb", "hun");
/**
* Base URL environment variable.

View File

@@ -0,0 +1,14 @@
package com.sismics.docs.core.constant;
/**
* Metadata type.
*
* @author bgamard
*/
public enum MetadataType {
STRING,
INTEGER,
FLOAT,
DATE,
BOOLEAN
}

View File

@@ -61,6 +61,7 @@ public class AuditLogDao {
queries.add(baseQuery + " where l.LOG_IDENTITY_C in (select f.FIL_ID_C from T_FILE f where f.FIL_IDDOC_C = :documentId) ");
queries.add(baseQuery + " where l.LOG_IDENTITY_C in (select c.COM_ID_C from T_COMMENT c where c.COM_IDDOC_C = :documentId) ");
queries.add(baseQuery + " where l.LOG_IDENTITY_C in (select a.ACL_ID_C from T_ACL a where a.ACL_SOURCEID_C = :documentId) ");
queries.add(baseQuery + " where l.LOG_IDENTITY_C in (select r.RTE_ID_C from T_ROUTE r where r.RTE_IDDOCUMENT_C = :documentId) ");
parameterMap.put("documentId", criteria.getDocumentId());
}

View File

@@ -27,7 +27,6 @@ public class CommentDao {
* @param comment Comment
* @param userId User ID
* @return New ID
* @throws Exception
*/
public String create(Comment comment, String userId) {
// Create the UUID
@@ -99,7 +98,7 @@ public class CommentDao {
@SuppressWarnings("unchecked")
List<Object[]> l = q.getResultList();
List<CommentDto> commentDtoList = new ArrayList<CommentDto>();
List<CommentDto> commentDtoList = new ArrayList<>();
for (Object[] o : l) {
int i = 0;
CommentDto commentDto = new CommentDto();
@@ -107,7 +106,7 @@ public class CommentDao {
commentDto.setContent((String) o[i++]);
commentDto.setCreateTimestamp(((Timestamp) o[i++]).getTime());
commentDto.setCreatorName((String) o[i++]);
commentDto.setCreatorEmail((String) o[i++]);
commentDto.setCreatorEmail((String) o[i]);
commentDtoList.add(commentDto);
}
return commentDtoList;

View File

@@ -56,7 +56,7 @@ public class ContributorDao {
@SuppressWarnings("unchecked")
public List<ContributorDto> getByDocumentId(String documentId) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
StringBuilder sb = new StringBuilder("select u.USE_USERNAME_C, u.USE_EMAIL_C from T_CONTRIBUTOR c ");
StringBuilder sb = new StringBuilder("select distinct u.USE_USERNAME_C, u.USE_EMAIL_C from T_CONTRIBUTOR c ");
sb.append(" join T_USER u on u.USE_ID_C = c.CTR_IDUSER_C ");
sb.append(" where c.CTR_IDDOC_C = :documentId ");
Query q = em.createNativeQuery(sb.toString());

View File

@@ -196,21 +196,6 @@ public class DocumentDao {
* @return Updated document
*/
public Document update(Document document, String userId) {
Document documentDb = updateSilently(document);
// Create audit log
AuditLogUtil.create(documentDb, AuditLogType.UPDATE, userId);
return documentDb;
}
/**
* Update a document without audit log.
*
* @param document Document to update
* @return Updated document
*/
public Document updateSilently(Document document) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
// Get the document
@@ -234,9 +219,27 @@ public class DocumentDao {
documentDb.setFileId(document.getFileId());
documentDb.setUpdateDate(new Date());
// Create audit log
AuditLogUtil.create(documentDb, AuditLogType.UPDATE, userId);
return documentDb;
}
/**
* Update the file ID on a document.
*
* @param document Document
*/
public void updateFileId(Document document) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
Query query = em.createNativeQuery("update T_DOCUMENT d set DOC_IDFILE_C = :fileId, DOC_UPDATEDATE_D = :updateDate where d.DOC_ID_C = :id");
query.setParameter("updateDate", new Date());
query.setParameter("fileId", document.getFileId());
query.setParameter("id", document.getId());
query.executeUpdate();
}
/**
* Returns the number of documents.
*

View File

@@ -0,0 +1,89 @@
package com.sismics.docs.core.dao;
import com.sismics.docs.core.constant.MetadataType;
import com.sismics.docs.core.dao.dto.DocumentMetadataDto;
import com.sismics.docs.core.model.jpa.DocumentMetadata;
import com.sismics.util.context.ThreadLocalContext;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Document metadata DAO.
*
* @author bgamard
*/
public class DocumentMetadataDao {
/**
* Creates a new document metadata.
*
* @param documentMetadata Document metadata
* @return New ID
*/
public String create(DocumentMetadata documentMetadata) {
// Create the UUID
documentMetadata.setId(UUID.randomUUID().toString());
// Create the document metadata
EntityManager em = ThreadLocalContext.get().getEntityManager();
em.persist(documentMetadata);
return documentMetadata.getId();
}
/**
* Updates a document metadata.
*
* @param documentMetadata Document metadata
* @return Updated document metadata
*/
public DocumentMetadata update(DocumentMetadata documentMetadata) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
// Get the document metadata
Query q = em.createQuery("select u from DocumentMetadata u where u.id = :id");
q.setParameter("id", documentMetadata.getId());
DocumentMetadata documentMetadataDb = (DocumentMetadata) q.getSingleResult();
// Update the document metadata
documentMetadataDb.setValue(documentMetadata.getValue());
return documentMetadata;
}
/**
* Returns the list of all metadata values on a document.
*
* @param documentId Document ID
* @return List of metadata
*/
@SuppressWarnings("unchecked")
public List<DocumentMetadataDto> getByDocumentId(String documentId) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
StringBuilder sb = new StringBuilder("select dm.DME_ID_C, dm.DME_IDDOCUMENT_C, dm.DME_IDMETADATA_C, dm.DME_VALUE_C, m.MET_TYPE_C");
sb.append(" from T_DOCUMENT_METADATA dm, T_METADATA m ");
sb.append(" where dm.DME_IDMETADATA_C = m.MET_ID_C and dm.DME_IDDOCUMENT_C = :documentId and m.MET_DELETEDATE_D is null");
// Perform the search
Query q = em.createNativeQuery(sb.toString());
q.setParameter("documentId", documentId);
List<Object[]> l = q.getResultList();
// Assemble results
List<DocumentMetadataDto> dtoList = new ArrayList<>();
for (Object[] o : l) {
int i = 0;
DocumentMetadataDto dto = new DocumentMetadataDto();
dto.setId((String) o[i++]);
dto.setDocumentId((String) o[i++]);
dto.setMetadataId((String) o[i++]);
dto.setValue((String) o[i++]);
dto.setType(MetadataType.valueOf((String) o[i]));
dtoList.add(dto);
}
return dtoList;
}
}

View File

@@ -0,0 +1,148 @@
package com.sismics.docs.core.dao;
import com.google.common.base.Joiner;
import com.sismics.docs.core.constant.AuditLogType;
import com.sismics.docs.core.constant.MetadataType;
import com.sismics.docs.core.dao.criteria.MetadataCriteria;
import com.sismics.docs.core.dao.dto.MetadataDto;
import com.sismics.docs.core.model.jpa.Metadata;
import com.sismics.docs.core.util.AuditLogUtil;
import com.sismics.docs.core.util.jpa.QueryParam;
import com.sismics.docs.core.util.jpa.QueryUtil;
import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.util.context.ThreadLocalContext;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import java.util.*;
/**
* Metadata DAO.
*
* @author bgamard
*/
public class MetadataDao {
/**
* Creates a new metdata.
*
* @param metadata Metadata
* @param userId User ID
* @return New ID
*/
public String create(Metadata metadata, String userId) {
// Create the UUID
metadata.setId(UUID.randomUUID().toString());
// Create the metadata
EntityManager em = ThreadLocalContext.get().getEntityManager();
em.persist(metadata);
// Create audit log
AuditLogUtil.create(metadata, AuditLogType.CREATE, userId);
return metadata.getId();
}
/**
* Update a metadata.
*
* @param metadata Metadata to update
* @param userId User ID
* @return Updated metadata
*/
public Metadata update(Metadata metadata, String userId) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
// Get the metadata
Query q = em.createQuery("select r from Metadata r where r.id = :id and r.deleteDate is null");
q.setParameter("id", metadata.getId());
Metadata metadataDb = (Metadata) q.getSingleResult();
// Update the metadata
metadataDb.setName(metadata.getName());
// Create audit log
AuditLogUtil.create(metadataDb, AuditLogType.UPDATE, userId);
return metadataDb;
}
/**
* Gets an active metadata by its ID.
*
* @param id Metadata ID
* @return Metadata
*/
public Metadata getActiveById(String id) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
try {
Query q = em.createQuery("select r from Metadata r where r.id = :id and r.deleteDate is null");
q.setParameter("id", id);
return (Metadata) q.getSingleResult();
} catch (NoResultException e) {
return null;
}
}
/**
* Deletes a metadata.
*
* @param id Metadata ID
* @param userId User ID
*/
public void delete(String id, String userId) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
// Get the metadata
Query q = em.createQuery("select r from Metadata r where r.id = :id and r.deleteDate is null");
q.setParameter("id", id);
Metadata metadataDb = (Metadata) q.getSingleResult();
// Delete the metadata
Date dateNow = new Date();
metadataDb.setDeleteDate(dateNow);
// Create audit log
AuditLogUtil.create(metadataDb, AuditLogType.DELETE, userId);
}
/**
* Returns the list of all metadata.
*
* @param criteria Search criteria
* @param sortCriteria Sort criteria
* @return List of metadata
*/
public List<MetadataDto> findByCriteria(MetadataCriteria criteria, SortCriteria sortCriteria) {
Map<String, Object> parameterMap = new HashMap<>();
List<String> criteriaList = new ArrayList<>();
StringBuilder sb = new StringBuilder("select m.MET_ID_C c0, m.MET_NAME_C c1, m.MET_TYPE_C c2");
sb.append(" from T_METADATA m ");
criteriaList.add("m.MET_DELETEDATE_D is null");
if (!criteriaList.isEmpty()) {
sb.append(" where ");
sb.append(Joiner.on(" and ").join(criteriaList));
}
// Perform the search
QueryParam queryParam = QueryUtil.getSortedQueryParam(new QueryParam(sb.toString(), parameterMap), sortCriteria);
@SuppressWarnings("unchecked")
List<Object[]> l = QueryUtil.getNativeQuery(queryParam).getResultList();
// Assemble results
List<MetadataDto> dtoList = new ArrayList<>();
for (Object[] o : l) {
int i = 0;
MetadataDto dto = new MetadataDto();
dto.setId((String) o[i++]);
dto.setName((String) o[i++]);
dto.setType(MetadataType.valueOf((String) o[i]));
dtoList.add(dto);
}
return dtoList;
}
}

View File

@@ -36,13 +36,13 @@ public class RelationDao {
List<Object[]> l = q.getResultList();
// Assemble results
List<RelationDto> relationDtoList = new ArrayList<RelationDto>();
List<RelationDto> relationDtoList = new ArrayList<>();
for (Object[] o : l) {
int i = 0;
RelationDto relationDto = new RelationDto();
relationDto.setId((String) o[i++]);
relationDto.setTitle((String) o[i++]);
String fromDocId = (String) o[i++];
String fromDocId = (String) o[i];
relationDto.setSource(documentId.equals(fromDocId));
relationDtoList.add(relationDto);
}

View File

@@ -91,10 +91,15 @@ public class RouteDao {
* Deletes a route and the associated steps.
*
* @param routeId Route ID
* @param userId User ID
*/
public void deleteRoute(String routeId) {
public void deleteRoute(String routeId, String userId) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
// Create audit log
Route route = em.find(Route.class, routeId);
AuditLogUtil.create(route, AuditLogType.DELETE, userId);
em.createNativeQuery("update T_ROUTE_STEP rs set RTP_DELETEDATE_D = :dateNow where rs.RTP_IDROUTE_C = :routeId and rs.RTP_DELETEDATE_D is null")
.setParameter("routeId", routeId)
.setParameter("dateNow", new Date())

View File

@@ -61,7 +61,7 @@ public class RouteModelDao {
q.setParameter("id", routeModel.getId());
RouteModel routeModelDb = (RouteModel) q.getSingleResult();
// Update the group
// Update the route model
routeModelDb.setName(routeModel.getName());
routeModelDb.setSteps(routeModel.getSteps());
@@ -88,6 +88,18 @@ public class RouteModelDao {
}
}
/**
* Returns the list of all route models.
*
* @return List of route models
*/
@SuppressWarnings("unchecked")
public List<RouteModel> findAll() {
EntityManager em = ThreadLocalContext.get().getEntityManager();
Query q = em.createQuery("select r from RouteModel r where r.deleteDate is null");
return q.getResultList();
}
/**
* Deletes a route model.
*

View File

@@ -171,6 +171,26 @@ public class UserDao {
return user;
}
/**
* Update the onboarding status.
*
* @param user User to update
* @return Updated user
*/
public User updateOnboarding(User user) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
// Get the user
Query q = em.createQuery("select u from User u where u.id = :id and u.deleteDate is null");
q.setParameter("id", user.getId());
User userDb = (User) q.getSingleResult();
// Update the user
userDb.setOnboarding(user.isOnboarding());
return user;
}
/**
* Gets a user by its ID.
*

View File

@@ -51,6 +51,12 @@ public class DocumentCriteria {
*/
private List<List<String>> tagIdList;
/**
* Tag IDs to excluded.
* The first and second level list will be excluded.
*/
private List<List<String>> excludedTagIdList;
/**
* Shared status.
*/
@@ -71,6 +77,11 @@ public class DocumentCriteria {
*/
private Boolean activeRoute;
/**
* MIME type of a file.
*/
private String mimeType;
public List<String> getTargetIdList() {
return targetIdList;
}
@@ -119,6 +130,15 @@ public class DocumentCriteria {
this.tagIdList = tagIdList;
}
public List<List<String>> getExcludedTagIdList() {
return excludedTagIdList;
}
public DocumentCriteria setExcludedTagIdList(List<List<String>> excludedTagIdList) {
this.excludedTagIdList = excludedTagIdList;
return this;
}
public Boolean getShared() {
return shared;
}
@@ -166,4 +186,12 @@ public class DocumentCriteria {
public void setActiveRoute(Boolean activeRoute) {
this.activeRoute = activeRoute;
}
public String getMimeType() {
return mimeType;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
}

View File

@@ -0,0 +1,9 @@
package com.sismics.docs.core.dao.criteria;
/**
* Metadata criteria.
*
* @author bgamard
*/
public class MetadataCriteria {
}

View File

@@ -0,0 +1,94 @@
package com.sismics.docs.core.dao.dto;
import com.sismics.docs.core.constant.MetadataType;
/**
* Document metadata DTO.
*
* @author bgamard
*/
public class DocumentMetadataDto {
/**
* Document metadata ID.
*/
private String id;
/**
* Document ID.
*/
private String documentId;
/**
* Metadata ID.
*/
private String metadataId;
/**
* Name.
*/
private String name;
/**
* Value.
*/
private String value;
/**
* Type.
*/
private MetadataType type;
public String getId() {
return id;
}
public DocumentMetadataDto setId(String id) {
this.id = id;
return this;
}
public String getName() {
return name;
}
public DocumentMetadataDto setName(String name) {
this.name = name;
return this;
}
public MetadataType getType() {
return type;
}
public DocumentMetadataDto setType(MetadataType type) {
this.type = type;
return this;
}
public String getDocumentId() {
return documentId;
}
public DocumentMetadataDto setDocumentId(String documentId) {
this.documentId = documentId;
return this;
}
public String getMetadataId() {
return metadataId;
}
public DocumentMetadataDto setMetadataId(String metadataId) {
this.metadataId = metadataId;
return this;
}
public String getValue() {
return value;
}
public DocumentMetadataDto setValue(String value) {
this.value = value;
return this;
}
}

View File

@@ -0,0 +1,52 @@
package com.sismics.docs.core.dao.dto;
import com.sismics.docs.core.constant.MetadataType;
/**
* Metadata DTO.
*
* @author bgamard
*/
public class MetadataDto {
/**
* Metadata ID.
*/
private String id;
/**
* Name.
*/
private String name;
/**
* Type.
*/
private MetadataType type;
public String getId() {
return id;
}
public MetadataDto setId(String id) {
this.id = id;
return this;
}
public String getName() {
return name;
}
public MetadataDto setName(String name) {
this.name = name;
return this;
}
public MetadataType getType() {
return type;
}
public MetadataDto setType(MetadataType type) {
this.type = type;
return this;
}
}

View File

@@ -58,7 +58,7 @@ public class DocumentUpdatedAsyncListener {
}
// Update database and index
documentDao.updateSilently(document);
documentDao.updateFileId(document);
AppContext.getInstance().getIndexingHandler().updateDocument(document);
// Update contributors list

View File

@@ -96,14 +96,14 @@ public class FileProcessingAsyncListener {
final File file = event.getFile();
FormatHandler formatHandler = FormatHandlerUtil.find(file.getMimeType());
if (formatHandler == null) {
log.error("Format unhandled: " + file.getMimeType());
log.info("Format unhandled: " + file.getMimeType());
FileUtil.endProcessingFile(file.getId());
return;
}
// Get the user from the database
// Get the creating user from the database for its private key
UserDao userDao = new UserDao();
User user = userDao.getById(event.getUserId());
User user = userDao.getById(file.getUserId());
if (user == null) {
// The user has been deleted meanwhile
FileUtil.endProcessingFile(file.getId());

View File

@@ -0,0 +1,91 @@
package com.sismics.docs.core.model.jpa;
import com.google.common.base.MoreObjects;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
/**
* Link between a document and a metadata, holding the value.
*
* @author bgamard
*/
@Entity
@Table(name = "T_DOCUMENT_METADATA")
public class DocumentMetadata implements Serializable {
/**
* Serial version UID.
*/
private static final long serialVersionUID = 1L;
/**
* Document metadata ID.
*/
@Id
@Column(name = "DME_ID_C", length = 36)
private String id;
/**
* Document ID.
*/
@Column(name = "DME_IDDOCUMENT_C", nullable = false, length = 36)
private String documentId;
/**
* Metadata ID.
*/
@Column(name = "DME_IDMETADATA_C", nullable = false, length = 36)
private String metadataId;
/**
* Value.
*/
@Column(name = "DME_VALUE_C", length = 4000)
private String value;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getDocumentId() {
return documentId;
}
public void setDocumentId(String documentId) {
this.documentId = documentId;
}
public String getMetadataId() {
return metadataId;
}
public DocumentMetadata setMetadataId(String metadataId) {
this.metadataId = metadataId;
return this;
}
public String getValue() {
return value;
}
public DocumentMetadata setValue(String value) {
this.value = value;
return this;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("documentId", documentId)
.add("metadataId", metadataId)
.toString();
}
}

View File

@@ -0,0 +1,92 @@
package com.sismics.docs.core.model.jpa;
import com.google.common.base.MoreObjects;
import com.sismics.docs.core.constant.MetadataType;
import javax.persistence.*;
import java.util.Date;
/**
* Metadata entity.
*
* @author bgamard
*/
@Entity
@Table(name = "T_METADATA")
public class Metadata implements Loggable {
/**
* Metadata ID.
*/
@Id
@Column(name = "MET_ID_C", length = 36)
private String id;
/**
* Name.
*/
@Column(name = "MET_NAME_C", length = 50, nullable = false)
private String name;
/**
* Type.
*/
@Column(name = "MET_TYPE_C", length = 20, nullable = false)
@Enumerated(EnumType.STRING)
private MetadataType type;
/**
* Deletion date.
*/
@Column(name = "MET_DELETEDATE_D")
private Date deleteDate;
public String getId() {
return id;
}
public Metadata setId(String id) {
this.id = id;
return this;
}
public String getName() {
return name;
}
public Metadata setName(String name) {
this.name = name;
return this;
}
public MetadataType getType() {
return type;
}
public Metadata setType(MetadataType type) {
this.type = type;
return this;
}
@Override
public Date getDeleteDate() {
return deleteDate;
}
public void setDeleteDate(Date deleteDate) {
this.deleteDate = deleteDate;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("name", name)
.add("type", type)
.toString();
}
@Override
public String toMessage() {
return name;
}
}

View File

@@ -47,6 +47,12 @@ public class User implements Loggable {
@Column(name = "USE_PRIVATEKEY_C", nullable = false, length = 100)
private String privateKey;
/**
* False when the user passed the onboarding.
*/
@Column(name = "USE_ONBOARDING_B", nullable = false)
private boolean onboarding;
/**
* TOTP secret key.
*/
@@ -198,6 +204,15 @@ public class User implements Loggable {
return this;
}
public boolean isOnboarding() {
return onboarding;
}
public User setOnboarding(boolean onboarding) {
this.onboarding = onboarding;
return this;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)

View File

@@ -85,7 +85,7 @@ public class FileService extends AbstractScheduledService {
*
* @author bgamard
*/
class TemporaryPathReference extends PhantomReference<Path> {
static class TemporaryPathReference extends PhantomReference<Path> {
String path;
TemporaryPathReference(Path referent, ReferenceQueue<? super Path> q) {
super(referent, q);

View File

@@ -22,9 +22,9 @@ public class DirectoryUtil {
*/
public static Path getBaseDataDirectory() {
Path baseDataDir = null;
if (StringUtils.isNotBlank(EnvironmentUtil.getDocsHome())) {
if (StringUtils.isNotBlank(EnvironmentUtil.getTeedyHome())) {
// If the docs.home property is set then use it
baseDataDir = Paths.get(EnvironmentUtil.getDocsHome());
baseDataDir = Paths.get(EnvironmentUtil.getTeedyHome());
} else if (EnvironmentUtil.isUnitTest()) {
// For unit testing, use a temporary directory
baseDataDir = Paths.get(System.getProperty("java.io.tmpdir"));

View File

@@ -32,6 +32,7 @@ public class EncryptionUtil {
static {
// Initialize Bouncy Castle provider
Security.insertProviderAt(new BouncyCastleProvider(), 1);
Security.removeProvider("SunRsaSign");
}
/**

View File

@@ -0,0 +1,196 @@
package com.sismics.docs.core.util;
import com.google.common.collect.Maps;
import com.sismics.docs.core.constant.MetadataType;
import com.sismics.docs.core.dao.DocumentMetadataDao;
import com.sismics.docs.core.dao.MetadataDao;
import com.sismics.docs.core.dao.criteria.MetadataCriteria;
import com.sismics.docs.core.dao.dto.DocumentMetadataDto;
import com.sismics.docs.core.dao.dto.MetadataDto;
import com.sismics.docs.core.model.jpa.DocumentMetadata;
import com.sismics.docs.core.util.jpa.SortCriteria;
import javax.json.Json;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObjectBuilder;
import java.text.MessageFormat;
import java.util.List;
import java.util.Map;
/**
* Metadata utilities.
*
* @author bgamard
*/
public class MetadataUtil {
/**
* Update custom metadata on a document.
*
* @param documentId Document ID
* @param metadataIdList Metadata ID list
* @param metadataValueList Metadata value list
*/
public static void updateMetadata(String documentId, List<String> metadataIdList, List<String> metadataValueList) throws Exception {
if (metadataIdList == null || metadataValueList == null || metadataIdList.isEmpty()) {
return;
}
if (metadataIdList.size() != metadataValueList.size()) {
throw new Exception("metadata_id and metadata_value must have the same length");
}
Map<String, String> newValues = Maps.newHashMap();
for (int i = 0; i < metadataIdList.size(); i++) {
newValues.put(metadataIdList.get(i), metadataValueList.get(i));
}
MetadataDao metadataDao = new MetadataDao();
DocumentMetadataDao documentMetadataDao = new DocumentMetadataDao();
List<MetadataDto> metadataDtoList = metadataDao.findByCriteria(new MetadataCriteria(), null);
List<DocumentMetadataDto> documentMetadataDtoList = documentMetadataDao.getByDocumentId(documentId);
// Update existing values
for (DocumentMetadataDto documentMetadataDto : documentMetadataDtoList) {
if (newValues.containsKey(documentMetadataDto.getMetadataId())) {
// Update the value
String value = newValues.get(documentMetadataDto.getMetadataId());
validateValue(documentMetadataDto.getType(), value);
updateValue(documentMetadataDto.getId(), value);
newValues.remove(documentMetadataDto.getMetadataId());
} else {
// Remove the value
updateValue(documentMetadataDto.getId(), null);
}
}
// Create new values
for (Map.Entry<String, String> entry : newValues.entrySet()) {
// Search the metadata definition
MetadataDto metadata = null;
for (MetadataDto metadataDto : metadataDtoList) {
if (metadataDto.getId().equals(entry.getKey())) {
metadata = metadataDto;
break;
}
}
if (metadata == null) {
throw new Exception(MessageFormat.format("Metadata not found: {0}", entry.getKey()));
}
// Add the value
validateValue(metadata.getType(), entry.getValue());
createValue(documentId, entry.getKey(), entry.getValue());
}
}
/**
* Validate a custom metadata value.
*
* @param type Metadata type
* @param value Value
* @throws Exception In case of validation error
*/
private static void validateValue(MetadataType type, String value) throws Exception {
switch (type) {
case STRING:
case BOOLEAN:
return;
case DATE:
try {
Long.parseLong(value);
} catch (NumberFormatException e) {
throw new Exception("Date value not parsable as timestamp");
}
break;
case FLOAT:
try {
Double.parseDouble(value);
} catch (NumberFormatException e) {
throw new Exception("Float value not parsable");
}
break;
case INTEGER:
try {
Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new Exception("Integer value not parsable");
}
break;
}
}
/**
* Create a custom metadata value on a document.
*
* @param documentId Document ID
* @param metadataId Metadata ID
* @param value Value
*/
private static void createValue(String documentId, String metadataId, String value) {
DocumentMetadataDao documentMetadataDao = new DocumentMetadataDao();
DocumentMetadata documentMetadata = new DocumentMetadata();
documentMetadata.setDocumentId(documentId);
documentMetadata.setMetadataId(metadataId);
documentMetadata.setValue(value);
documentMetadataDao.create(documentMetadata);
}
/**
* Update a custom metadata value.
*
* @param documentMetadataId Document metadata ID
* @param value Value
*/
private static void updateValue(String documentMetadataId, String value) {
DocumentMetadataDao documentMetadataDao = new DocumentMetadataDao();
DocumentMetadata documentMetadata = new DocumentMetadata();
documentMetadata.setId(documentMetadataId);
documentMetadata.setValue(value);
documentMetadataDao.update(documentMetadata);
}
/**
* Add custom metadata to a JSON response.
*
* @param json JSON
* @param documentId Document ID
*/
public static void addMetadata(JsonObjectBuilder json, String documentId) {
DocumentMetadataDao documentMetadataDao = new DocumentMetadataDao();
MetadataDao metadataDao = new MetadataDao();
List<MetadataDto> metadataDtoList = metadataDao.findByCriteria(new MetadataCriteria(), new SortCriteria(1, true));
List<DocumentMetadataDto> documentMetadataDtoList = documentMetadataDao.getByDocumentId(documentId);
JsonArrayBuilder metadata = Json.createArrayBuilder();
for (MetadataDto metadataDto : metadataDtoList) {
JsonObjectBuilder meta = Json.createObjectBuilder()
.add("id", metadataDto.getId())
.add("name", metadataDto.getName())
.add("type", metadataDto.getType().name());
for (DocumentMetadataDto documentMetadataDto : documentMetadataDtoList) {
if (documentMetadataDto.getMetadataId().equals(metadataDto.getId())) {
if (documentMetadataDto.getValue() != null) {
switch (metadataDto.getType()) {
case STRING:
meta.add("value", documentMetadataDto.getValue());
break;
case BOOLEAN:
meta.add("value", Boolean.parseBoolean(documentMetadataDto.getValue()));
break;
case DATE:
meta.add("value", Long.parseLong(documentMetadataDto.getValue()));
break;
case FLOAT:
meta.add("value", Double.parseDouble(documentMetadataDto.getValue()));
break;
case INTEGER:
meta.add("value", Integer.parseInt(documentMetadataDto.getValue()));
break;
}
}
}
}
metadata.add(meta);
}
json.add("metadata", metadata);
}
}

View File

@@ -6,6 +6,7 @@ import com.sismics.docs.core.constant.AclType;
import com.sismics.docs.core.constant.PermType;
import com.sismics.docs.core.dao.AclDao;
import com.sismics.docs.core.dao.DocumentDao;
import com.sismics.docs.core.dao.RouteModelDao;
import com.sismics.docs.core.dao.UserDao;
import com.sismics.docs.core.dao.criteria.UserCriteria;
import com.sismics.docs.core.dao.dto.RouteStepDto;
@@ -15,8 +16,14 @@ import com.sismics.docs.core.event.RouteStepValidateEvent;
import com.sismics.docs.core.model.context.AppContext;
import com.sismics.docs.core.model.jpa.Acl;
import com.sismics.docs.core.model.jpa.Document;
import com.sismics.docs.core.model.jpa.RouteModel;
import com.sismics.util.context.ThreadLocalContext;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonReader;
import java.io.StringReader;
import java.util.List;
/**
@@ -87,4 +94,31 @@ public class RoutingUtil {
AppContext.getInstance().getMailEventBus().post(routeStepValidateEvent);
}
}
/**
* Find the first route model name matching a target type and name.
*
* @param targetType Target type
* @param targetName Target name
* @return Route model name or null if none is matching
*/
public static String findRouteModelNameByTargetName(AclTargetType targetType, String targetName) {
RouteModelDao routeModelDao = new RouteModelDao();
List<RouteModel> routeModelList = routeModelDao.findAll();
for (RouteModel routeModel : routeModelList) {
try (JsonReader reader = Json.createReader(new StringReader(routeModel.getSteps()))) {
JsonArray stepsJson = reader.readArray();
for (int order = 0; order < stepsJson.size(); order++) {
JsonObject step = stepsJson.getJsonObject(order);
JsonObject target = step.getJsonObject("target");
AclTargetType routeTargetType = AclTargetType.valueOf(target.getString("type"));
String routeTargetName = target.getString("name");
if (targetType == routeTargetType && targetName.equals(routeTargetName)) {
return routeModel.getName();
}
}
}
}
return null;
}
}

View File

@@ -6,6 +6,7 @@ import com.sismics.util.mime.MimeType;
import org.apache.pdfbox.io.MemoryUsageSetting;
import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.text.PDFTextStripper;
import org.slf4j.Logger;
@@ -60,7 +61,7 @@ public class PdfFormatHandler implements FormatHandler {
for (int pageIndex = 0; pageIndex < pdfDocument.getNumberOfPages(); pageIndex++) {
log.info("OCR page " + (pageIndex + 1) + "/" + pdfDocument.getNumberOfPages() + " of PDF file containing only images");
sb.append(" ");
sb.append(FileUtil.ocrFile(language, renderer.renderImage(pageIndex)));
sb.append(FileUtil.ocrFile(language, renderer.renderImageWithDPI(pageIndex, 300, ImageType.GRAY)));
}
return sb.toString();
} catch (Exception e) {

View File

@@ -46,7 +46,6 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Timestamp;
import java.util.*;
@@ -252,7 +251,7 @@ public class LuceneIndexingHandler implements IndexingHandler {
" s.SHA_DELETEDATE_D IS NULL group by ac.ACL_SOURCEID_C) s on s.ACL_SOURCEID_C = d.DOC_ID_C " +
" left join (SELECT count(f.FIL_ID_C) count, f.FIL_IDDOC_C " +
" FROM T_FILE f " +
" WHERE f.FIL_DELETEDATE_D IS NULL group by f.FIL_IDDOC_C) f on f.FIL_IDDOC_C = d.DOC_ID_C ");
" WHERE f.FIL_DELETEDATE_D is null group by f.FIL_IDDOC_C) f on f.FIL_IDDOC_C = d.DOC_ID_C ");
sb.append(" left join (select rs.*, rs3.idDocument " +
"from T_ROUTE_STEP rs " +
"join (select r.RTE_IDDOCUMENT_C idDocument, rs.RTP_IDROUTE_C idRoute, min(rs.RTP_ORDER_N) minOrder from T_ROUTE_STEP rs join T_ROUTE r on r.RTE_ID_C = rs.RTP_IDROUTE_C and r.RTE_DELETEDATE_D is null where rs.RTP_DELETEDATE_D is null and rs.RTP_ENDDATE_D is null group by rs.RTP_IDROUTE_C, r.RTE_IDDOCUMENT_C) rs3 on rs.RTP_IDROUTE_C = rs3.idRoute and rs.RTP_ORDER_N = rs3.minOrder " +
@@ -309,9 +308,27 @@ public class LuceneIndexingHandler implements IndexingHandler {
criteriaList.add("(" + Joiner.on(" OR ").join(tagCriteriaList) + ")");
}
}
if (criteria.getExcludedTagIdList() != null && !criteria.getExcludedTagIdList().isEmpty()) {
int index = 0;
for (List<String> tagIdList : criteria.getExcludedTagIdList()) {
List<String> tagCriteriaList = Lists.newArrayList();
for (String tagId : tagIdList) {
sb.append(String.format("left join T_DOCUMENT_TAG dtex%d on dtex%d.DOT_IDDOCUMENT_C = d.DOC_ID_C and dtex%d.DOT_IDTAG_C = :tagIdEx%d and dtex%d.DOT_DELETEDATE_D is null ", index, index, index, index, index));
parameterMap.put("tagIdEx" + index, tagId);
tagCriteriaList.add(String.format("dtex%d.DOT_ID_C is null", index));
index++;
}
criteriaList.add("(" + Joiner.on(" AND ").join(tagCriteriaList) + ")");
}
}
if (criteria.getShared() != null && criteria.getShared()) {
criteriaList.add("s.count > 0");
}
if (criteria.getMimeType() != null) {
sb.append("left join T_FILE f0 on f0.FIL_IDDOC_C = d.DOC_ID_C and f0.FIL_MIMETYPE_C = :mimeType and f0.FIL_DELETEDATE_D is null");
parameterMap.put("mimeType", criteria.getMimeType());
criteriaList.add("f0.FIL_ID_C is not null");
}
if (criteria.getLanguage() != null) {
criteriaList.add("d.DOC_LANGUAGE_C = :language");
parameterMap.put("language", criteria.getLanguage());
@@ -377,7 +394,7 @@ public class LuceneIndexingHandler implements IndexingHandler {
LuceneDictionary dictionary = new LuceneDictionary(directoryReader, "title");
suggester.build(dictionary);
int lastIndex = search.lastIndexOf(' ');
String suggestQuery = search.substring(lastIndex < 0 ? 0 : lastIndex);
String suggestQuery = search.substring(Math.max(lastIndex, 0));
List<Lookup.LookupResult> lookupResultList = suggester.lookup(suggestQuery, false, 10);
for (Lookup.LookupResult lookupResult : lookupResultList) {
suggestionList.add(lookupResult.key.toString());

View File

@@ -124,11 +124,11 @@ public class EmailUtil {
// Application name
Config themeConfig = configDao.getById(ConfigType.THEME);
String appName = "Sismics Docs";
String appName = "Teedy";
if (themeConfig != null) {
try (JsonReader reader = Json.createReader(new StringReader(themeConfig.getValue()))) {
JsonObject themeJson = reader.readObject();
appName = themeJson.getString("name", "Sismics Docs");
appName = themeJson.getString("name", "Teedy");
}
}

View File

@@ -15,7 +15,7 @@ public class EnvironmentUtil {
private static String MAC_OS_USER_HOME = System.getProperty("user.home");
private static String DOCS_HOME = System.getProperty("docs.home");
private static String TEEDY_HOME = System.getProperty("docs.home");
/**
* In a web application context.
@@ -90,8 +90,8 @@ public class EnvironmentUtil {
*
* @return Home directory
*/
public static String getDocsHome() {
return DOCS_HOME;
public static String getTeedyHome() {
return TEEDY_HOME;
}
/**

View File

@@ -48,8 +48,11 @@ public class DialectUtil {
sql = sql.replaceAll("(cached|memory) table", "table");
sql = sql.replaceAll("datetime", "timestamp");
sql = sql.replaceAll("longvarchar", "text");
sql = sql.replaceAll("bit not null", "bool not null");
sql = sql.replaceAll("bit default 1", "bool default true");
sql = sql.replaceAll("bit default 0", "bool default false");
sql = sql.replaceAll("bit not null default 1", "bool not null default true");
sql = sql.replaceAll("bit not null default 0", "bool not null default false");
sql = sql.replaceAll("bit not null", "bool not null");
return sql;
}
}

View File

@@ -88,7 +88,7 @@ public final class EMF {
if (databaseUrl == null) {
props.put("hibernate.connection.driver_class", "org.h2.Driver");
props.put("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
props.put("hibernate.connection.url", "jdbc:h2:file:" + dbFile + ";CACHE_SIZE=65536");
props.put("hibernate.connection.url", "jdbc:h2:file:" + dbFile + ";CACHE_SIZE=65536;LOCK_TIMEOUT=10000");
props.put("hibernate.connection.username", "sa");
} else {
props.put("hibernate.connection.driver_class", "org.postgresql.Driver");

View File

@@ -1 +1 @@
db.version=22
db.version=24

View File

@@ -0,0 +1,2 @@
alter table T_USER add column USE_ONBOARDING_B bit not null default 1;
update T_CONFIG set CFG_VALUE_C = '23' where CFG_ID_C = 'DB_VERSION';

View File

@@ -0,0 +1,5 @@
create cached table T_METADATA ( MET_ID_C varchar(36) not null, MET_NAME_C varchar(50) not null, MET_TYPE_C varchar(20) not null, MET_DELETEDATE_D datetime, primary key (MET_ID_C) );
create cached table T_DOCUMENT_METADATA ( DME_ID_C varchar(36) not null, DME_IDDOCUMENT_C varchar(36) not null, DME_IDMETADATA_C varchar(36) not null, DME_VALUE_C varchar(4000) null, primary key (DME_ID_C) );
alter table T_DOCUMENT_METADATA add constraint FK_DME_IDDOCUMENT_C foreign key (DME_IDDOCUMENT_C) references T_DOCUMENT (DOC_ID_C) on delete restrict on update restrict;
alter table T_DOCUMENT_METADATA add constraint FK_DME_IDMETADATA_C foreign key (DME_IDMETADATA_C) references T_METADATA (MET_ID_C) on delete restrict on update restrict;
update T_CONFIG set CFG_VALUE_C = '24' where CFG_ID_C = 'DB_VERSION';

View File

@@ -0,0 +1,27 @@
package com.sismics.util.format;
import com.sismics.docs.core.util.format.PdfFormatHandler;
import org.junit.Assert;
import org.junit.Test;
import java.nio.file.Paths;
/**
* Test of {@link PdfFormatHandler}
*
* @author bgamard
*/
public class TestPdfFormatHandler {
/**
* Test related to https://github.com/sismics/docs/issues/373.
*/
@Test
public void testIssue373() throws Exception {
PdfFormatHandler formatHandler = new PdfFormatHandler();
String content = formatHandler.extractContent("deu", Paths.get(ClassLoader.getSystemResource("file/issue373.pdf").toURI()));
Assert.assertTrue(content.contains("Aufrechterhaltung"));
Assert.assertTrue(content.contains("Außentemperatur"));
Assert.assertTrue(content.contains("Grundumsatzmessungen"));
Assert.assertTrue(content.contains("ermitteln"));
}
}

Binary file not shown.

View File

@@ -22,9 +22,9 @@ const prefs = new preferences('com.sismics.docs.importer',{
});
// Welcome message
console.log('Sismics Docs Importer 1.0.0, https://www.sismicsdocs.com' +
console.log('Teedy Importer 1.0.0, https://teedy.io' +
'\n\n' +
'This program let you import files from your system to Sismics Docs' +
'This program let you import files from your system to Teedy' +
'\n');
// Ask for the base URL
@@ -33,7 +33,7 @@ const askBaseUrl = () => {
{
type: 'input',
name: 'baseUrl',
message: 'What is the base URL of your Docs? (eg. https://docs.mycompany.com)',
message: 'What is the base URL of your Teedy? (eg. https://teedy.mycompany.com)',
default: prefs.importer.baseUrl
}
]).then(answers => {
@@ -42,12 +42,12 @@ const askBaseUrl = () => {
// Test base URL
const spinner = ora({
text: 'Checking connection to Docs',
text: 'Checking connection to Teedy',
spinner: 'flips'
}).start();
request(answers.baseUrl + '/api/app', function (error, response) {
if (!response || response.statusCode !== 200) {
spinner.fail('Connection to Docs failed: ' + error);
spinner.fail('Connection to Teedy failed: ' + error);
askBaseUrl();
return;
}
@@ -82,7 +82,7 @@ const askCredentials = () => {
// Test credentials
const spinner = ora({
text: 'Checking connection to Docs',
text: 'Checking connection to Teedy',
spinner: 'flips'
}).start();
request.post({

View File

@@ -1,7 +1,7 @@
{
"name": "docs-importer",
"name": "teedy-importer",
"version": "1.5.1",
"description": "Import files to Sismics Docs",
"description": "Import files to Teedy",
"bin": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.sismics.docs</groupId>
<artifactId>docs-parent</artifactId>
<version>1.6-SNAPSHOT</version>
<version>1.8</version>
<relativePath>..</relativePath>
</parent>

View File

@@ -26,7 +26,7 @@ import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
/**
* Stress app for Sismics Docs.
* Stress app for Teedy.
*
* @author bgamard
*/

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.sismics.docs</groupId>
<artifactId>docs-parent</artifactId>
<version>1.6-SNAPSHOT</version>
<version>1.8</version>
<relativePath>..</relativePath>
</parent>
@@ -69,6 +69,11 @@
<artifactId>joda-time</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>junit</groupId>

View File

@@ -112,6 +112,17 @@ public class ValidationUtil {
ValidationUtil.validateLength(s, name, 7, 7, nullable);
}
/**
* Validate a tag name.
*
* @param name Name of the tag
*/
public static void validateTagName(String name) throws ClientException {
if (name.contains(" ") || name.contains(":")) {
throw new ClientException("IllegalTagName", "Spaces and colons are not allowed in tag name");
}
}
/**
* Validates that the provided string matches an URL with HTTP or HTTPS scheme.
*

View File

@@ -11,6 +11,7 @@ import org.apache.log4j.PatternLayout;
import org.apache.log4j.RollingFileAppender;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
@@ -57,6 +58,8 @@ public class RequestContextFilter implements Filter {
fileAppender.setMaxBackupIndex(5);
fileAppender.activateOptions();
org.apache.log4j.Logger.getRootLogger().addAppender(fileAppender);
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
// Initialize the application context
TransactionUtil.handle(AppContext::getInstance);

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.sismics.docs</groupId>
<artifactId>docs-parent</artifactId>
<version>1.6-SNAPSHOT</version>
<version>1.8</version>
<relativePath>..</relativePath>
</parent>
@@ -26,25 +26,6 @@
<artifactId>docs-web-common</artifactId>
</dependency>
<!-- JDK 11 JAXB dependencies -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<!-- Dependencies to Jersey -->
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>

View File

@@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=22
db.version=24

View File

@@ -6,3 +6,5 @@ log4j.appender.MEMORY=com.sismics.util.log4j.MemoryAppender
log4j.appender.MEMORY.size=1000
log4j.logger.com.sismics=DEBUG
log4j.logger.org.apache.pdfbox=ERROR
log4j.logger.org.glassfish.jersey.servlet.WebComponent=ERROR

View File

@@ -228,8 +228,11 @@ public class AclResource extends BaseResource {
SortCriteria sortCriteria = new SortCriteria(1, true);
List<UserDto> userDtoList = userDao.findByCriteria(new UserCriteria().setSearch(search), sortCriteria);
for (UserDto userDto : userDtoList) {
users.add(Json.createObjectBuilder()
.add("name", userDto.getUsername()));
// No need to add users who will skip ACL check anyways
if (!SecurityUtil.skipAclCheck(Lists.newArrayList(userDto.getId()))) {
users.add(Json.createObjectBuilder()
.add("name", userDto.getUsername()));
}
}
// Search groups
@@ -237,8 +240,11 @@ public class AclResource extends BaseResource {
JsonArrayBuilder groups = Json.createArrayBuilder();
List<GroupDto> groupDtoList = groupDao.findByCriteria(new GroupCriteria().setSearch(search), sortCriteria);
for (GroupDto groupDto : groupDtoList) {
groups.add(Json.createObjectBuilder()
.add("name", groupDto.getName()));
// No need to add users who will skip ACL check anyways
if (!SecurityUtil.skipAclCheck(Lists.newArrayList(groupDto.getId()))) {
groups.add(Json.createObjectBuilder()
.add("name", groupDto.getName()));
}
}
JsonObjectBuilder response = Json.createObjectBuilder()

View File

@@ -235,6 +235,9 @@ public class DocumentResource extends BaseResource {
document.add("route_step", step);
}
// Add custom metadata
MetadataUtil.addMetadata(document, documentId);
return Response.ok().entity(document.build()).build();
}
@@ -459,11 +462,15 @@ public class DocumentResource extends BaseResource {
switch (params[0]) {
case "tag":
case "!tag":
// New tag criteria
List<TagDto> tagDtoList = TagUtil.findByName(params[1], allTagDtoList);
if (documentCriteria.getTagIdList() == null) {
documentCriteria.setTagIdList(new ArrayList<>());
}
if (documentCriteria.getExcludedTagIdList() == null) {
documentCriteria.setExcludedTagIdList(new ArrayList<>());
}
if (tagDtoList.isEmpty()) {
// No tag found, the request must returns nothing
documentCriteria.getTagIdList().add(Lists.newArrayList(UUID.randomUUID().toString()));
@@ -476,7 +483,11 @@ public class DocumentResource extends BaseResource {
tagIdList.add(childrenTagDto.getId());
}
}
documentCriteria.getTagIdList().add(tagIdList);
if (params[0].startsWith("!")) {
documentCriteria.getExcludedTagIdList().add(tagIdList);
} else {
documentCriteria.getTagIdList().add(tagIdList);
}
}
break;
case "after":
@@ -558,6 +569,10 @@ public class DocumentResource extends BaseResource {
documentCriteria.setLanguage(UUID.randomUUID().toString());
}
break;
case "mime":
// New mime type criteria
documentCriteria.setMimeType(params[1]);
break;
case "by":
// New creator criteria
User user = userDao.getActiveByUsername(params[1]);
@@ -606,6 +621,8 @@ public class DocumentResource extends BaseResource {
* @apiParam {String} [rights] Rights
* @apiParam {String[]} [tags] List of tags ID
* @apiParam {String[]} [relations] List of related documents ID
* @apiParam {String[]} [metadata_id] List of metadata ID
* @apiParam {String[]} [metadata_value] List of metadata values
* @apiParam {String} language Language
* @apiParam {Number} [create_date] Create date (timestamp)
* @apiSuccess {String} id Document ID
@@ -626,6 +643,8 @@ public class DocumentResource extends BaseResource {
* @param rights Rights
* @param tagList Tags
* @param relationList Relations
* @param metadataIdList Metadata ID list
* @param metadataValueList Metadata value list
* @param language Language
* @param createDateStr Creation date
* @return Response
@@ -644,6 +663,8 @@ public class DocumentResource extends BaseResource {
@FormParam("rights") String rights,
@FormParam("tags") List<String> tagList,
@FormParam("relations") List<String> relationList,
@FormParam("metadata_id") List<String> metadataIdList,
@FormParam("metadata_value") List<String> metadataValueList,
@FormParam("language") String language,
@FormParam("create_date") String createDateStr) {
if (!authenticate()) {
@@ -696,6 +717,13 @@ public class DocumentResource extends BaseResource {
// Update relations
updateRelationList(document.getId(), relationList);
// Update custom metadata
try {
MetadataUtil.updateMetadata(document.getId(), metadataIdList, metadataValueList);
} catch (Exception e) {
throw new ClientException("ValidationError", e.getMessage());
}
// Raise a document created event
DocumentCreatedAsyncEvent documentCreatedAsyncEvent = new DocumentCreatedAsyncEvent();
documentCreatedAsyncEvent.setUserId(principal.getId());
@@ -726,6 +754,8 @@ public class DocumentResource extends BaseResource {
* @apiParam {String} [rights] Rights
* @apiParam {String[]} [tags] List of tags ID
* @apiParam {String[]} [relations] List of related documents ID
* @apiParam {String[]} [metadata_id] List of metadata ID
* @apiParam {String[]} [metadata_value] List of metadata values
* @apiParam {String} language Language
* @apiParam {Number} [create_date] Create date (timestamp)
* @apiSuccess {String} id Document ID
@@ -755,6 +785,8 @@ public class DocumentResource extends BaseResource {
@FormParam("rights") String rights,
@FormParam("tags") List<String> tagList,
@FormParam("relations") List<String> relationList,
@FormParam("metadata_id") List<String> metadataIdList,
@FormParam("metadata_value") List<String> metadataValueList,
@FormParam("language") String language,
@FormParam("create_date") String createDateStr) {
if (!authenticate()) {
@@ -817,6 +849,13 @@ public class DocumentResource extends BaseResource {
// Update relations
updateRelationList(id, relationList);
// Update custom metadata
try {
MetadataUtil.updateMetadata(document.getId(), metadataIdList, metadataValueList);
} catch (Exception e) {
throw new ClientException("ValidationError", e.getMessage());
}
// Raise a document updated event
DocumentUpdatedAsyncEvent documentUpdatedAsyncEvent = new DocumentUpdatedAsyncEvent();
documentUpdatedAsyncEvent.setUserId(principal.getId());
@@ -961,14 +1000,29 @@ public class DocumentResource extends BaseResource {
// Delete the document
documentDao.delete(id, principal.getId());
// Raise file deleted events (don't bother sending document updated event)
long totalSize = 0L;
for (File file : fileList) {
// Store the file size to update the quota
java.nio.file.Path storedFile = DirectoryUtil.getStorageDirectory().resolve(file.getId());
try {
totalSize += Files.size(storedFile);
} catch (IOException e) {
// The file doesn't exists on disk, which is weird, but not fatal
}
// Raise file deleted event
FileDeletedAsyncEvent fileDeletedAsyncEvent = new FileDeletedAsyncEvent();
fileDeletedAsyncEvent.setUserId(principal.getId());
fileDeletedAsyncEvent.setFile(file);
ThreadLocalContext.get().addAsyncEvent(fileDeletedAsyncEvent);
}
// Update the user quota
UserDao userDao = new UserDao();
User user = userDao.getById(principal.getId());
user.setStorageCurrent(user.getStorageCurrent() - totalSize);
userDao.updateQuota(user);
// Raise a document deleted event
DocumentDeletedAsyncEvent documentDeletedAsyncEvent = new DocumentDeletedAsyncEvent();
documentDeletedAsyncEvent.setUserId(principal.getId());

View File

@@ -608,7 +608,7 @@ public class FileResource extends BaseResource {
if (size != null) {
if (size.equals("content")) {
return Response.ok(Strings.nullToEmpty(file.getContent()))
.header(HttpHeaders.CONTENT_TYPE, "text/plain")
.header(HttpHeaders.CONTENT_TYPE, "text/plain; charset=utf-8")
.build();
}

View File

@@ -2,6 +2,7 @@ package com.sismics.docs.rest.resource;
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import com.sismics.docs.core.constant.AclTargetType;
import com.sismics.docs.core.dao.GroupDao;
import com.sismics.docs.core.dao.RoleBaseFunctionDao;
import com.sismics.docs.core.dao.UserDao;
@@ -12,6 +13,7 @@ import com.sismics.docs.core.dao.dto.UserDto;
import com.sismics.docs.core.model.jpa.Group;
import com.sismics.docs.core.model.jpa.User;
import com.sismics.docs.core.model.jpa.UserGroup;
import com.sismics.docs.core.util.RoutingUtil;
import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.docs.rest.constant.BaseFunction;
import com.sismics.rest.exception.ClientException;
@@ -149,6 +151,14 @@ public class GroupResource extends BaseResource {
parentId = parentGroup.getId();
}
// Check that this group is not used in any workflow in case of renaming
if (!name.equals(groupName)) {
String routeModelName = RoutingUtil.findRouteModelNameByTargetName(AclTargetType.GROUP, groupName);
if (routeModelName != null) {
throw new ClientException("GroupUsedInRouteModel", routeModelName);
}
}
// Update the group
groupDao.update(group.setName(name)
.setParentId(parentId), principal.getId());
@@ -169,6 +179,7 @@ public class GroupResource extends BaseResource {
* @apiSuccess {String} status Status OK
* @apiError (client) ForbiddenError Access denied
* @apiError (client) NotFound Group not found
* @apiError (client) GroupUsedInRouteModel The group is used in a route model
* @apiPermission admin
* @apiVersion 1.5.0
*
@@ -198,6 +209,12 @@ public class GroupResource extends BaseResource {
}
}
// Check that this group is not used in any workflow
String routeModelName = RoutingUtil.findRouteModelNameByTargetName(AclTargetType.GROUP, groupName);
if (routeModelName != null) {
throw new ClientException("GroupUsedInRouteModel", routeModelName);
}
// Delete the group
groupDao.delete(group.getId(), principal.getId());

View File

@@ -0,0 +1,208 @@
package com.sismics.docs.rest.resource;
import com.sismics.docs.core.constant.MetadataType;
import com.sismics.docs.core.dao.MetadataDao;
import com.sismics.docs.core.dao.criteria.MetadataCriteria;
import com.sismics.docs.core.dao.dto.MetadataDto;
import com.sismics.docs.core.model.jpa.Metadata;
import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.docs.rest.constant.BaseFunction;
import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.rest.util.ValidationUtil;
import javax.json.Json;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObjectBuilder;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;
import java.util.List;
/**
* Metadata REST resources.
*
* @author bgamard
*/
@Path("/metadata")
public class MetadataResource extends BaseResource {
/**
* Returns the list of all configured metadata.
*
* @api {get} /metadata Get configured metadata
* @apiName GetMetadata
* @apiGroup Metadata
* @apiParam {Number} sort_column Column index to sort on
* @apiParam {Boolean} asc If true, sort in ascending order
* @apiSuccess {Object[]} metadata List of metadata
* @apiSuccess {String} metadata.id ID
* @apiSuccess {String} metadata.name Name
* @apiSuccess {String="STRING","INTEGER","FLOAT","DATE","BOOLEAN"} metadata.type Type
* @apiError (client) ForbiddenError Access denied
* @apiPermission user
* @apiVersion 1.7.0
*
* @return Response
*/
@GET
public Response list(
@QueryParam("sort_column") Integer sortColumn,
@QueryParam("asc") Boolean asc) {
if (!authenticate()) {
throw new ForbiddenClientException();
}
JsonArrayBuilder metadata = Json.createArrayBuilder();
SortCriteria sortCriteria = new SortCriteria(sortColumn, asc);
MetadataDao metadataDao = new MetadataDao();
List<MetadataDto> metadataDtoList = metadataDao.findByCriteria(new MetadataCriteria(), sortCriteria);
for (MetadataDto metadataDto : metadataDtoList) {
metadata.add(Json.createObjectBuilder()
.add("id", metadataDto.getId())
.add("name", metadataDto.getName())
.add("type", metadataDto.getType().name()));
}
JsonObjectBuilder response = Json.createObjectBuilder()
.add("metadata", metadata);
return Response.ok().entity(response.build()).build();
}
/**
* Add a metadata.
*
* @api {put} /metadata Add a custom metadata
* @apiName PutMetadata
* @apiGroup Metadata
* @apiParam {String{1..50}} name Name
* @apiParam {String="STRING","INTEGER","FLOAT","DATE","BOOLEAN"} type Type
* @apiSuccess {String} id ID
* @apiSuccess {String} name Name
* @apiSuccess {String="STRING","INTEGER","FLOAT","DATE","BOOLEAN"} type Type
* @apiError (client) ForbiddenError Access denied
* @apiError (client) ValidationError Validation error
* @apiPermission admin
* @apiVersion 1.7.0
*
* @param name Name
* @param typeStr Type
* @return Response
*/
@PUT
public Response add(@FormParam("name") String name,
@FormParam("type") String typeStr) {
if (!authenticate()) {
throw new ForbiddenClientException();
}
checkBaseFunction(BaseFunction.ADMIN);
// Validate input data
name = ValidationUtil.validateLength(name, "name", 1, 50, false);
MetadataType type = MetadataType.valueOf(ValidationUtil.validateLength(typeStr, "type", 1, 20, false));
// Create the metadata
MetadataDao metadataDao = new MetadataDao();
Metadata metadata = new Metadata();
metadata.setName(name);
metadata.setType(type);
metadataDao.create(metadata, principal.getId());
// Returns the metadata
JsonObjectBuilder response = Json.createObjectBuilder()
.add("id", metadata.getId())
.add("name", metadata.getName())
.add("type", metadata.getType().name());
return Response.ok().entity(response.build()).build();
}
/**
* Update a metadata.
*
* @api {post} /metadata/:id Update a custom metadata
* @apiName PostMetadataId
* @apiGroup Metadata
* @apiParam {String} id Metadata ID
* @apiParam {String{1..50}} name Name
* @apiSuccess {String} id ID
* @apiSuccess {String} name Name
* @apiSuccess {String="STRING","INTEGER","FLOAT","DATE","BOOLEAN"} type Type
* @apiError (client) ForbiddenError Access denied
* @apiError (client) ValidationError Validation error
* @apiError (client) NotFound Metadata not found
* @apiPermission admin
* @apiVersion 1.7.0
*
* @param id ID
* @param name Name
* @return Response
*/
@POST
@Path("{id: [a-z0-9\\-]+}")
public Response update(@PathParam("id") String id,
@FormParam("name") String name) {
if (!authenticate()) {
throw new ForbiddenClientException();
}
checkBaseFunction(BaseFunction.ADMIN);
// Validate input data
name = ValidationUtil.validateLength(name, "name", 1, 50, false);
// Get the metadata
MetadataDao metadataDao = new MetadataDao();
Metadata metadata = metadataDao.getActiveById(id);
if (metadata == null) {
throw new NotFoundException();
}
// Update the metadata
metadata.setName(name);
metadataDao.update(metadata, principal.getId());
// Returns the metadata
JsonObjectBuilder response = Json.createObjectBuilder()
.add("id", metadata.getId())
.add("name", metadata.getName())
.add("type", metadata.getType().name());
return Response.ok().entity(response.build()).build();
}
/**
* Delete a metadata.
*
* @api {delete} /metadata/:id Delete a custom metadata
* @apiName DeleteMetadataId
* @apiGroup Metadata
* @apiParam {String} id Metadata ID
* @apiSuccess {String} status Status OK
* @apiError (client) ForbiddenError Access denied
* @apiError (client) NotFound Metadata not found
* @apiPermission admin
* @apiVersion 1.7.0
*
* @param id ID
* @return Response
*/
@DELETE
@Path("{id: [a-z0-9\\-]+}")
public Response delete(@PathParam("id") String id) {
if (!authenticate()) {
throw new ForbiddenClientException();
}
checkBaseFunction(BaseFunction.ADMIN);
// Get the metadata
MetadataDao metadataDao = new MetadataDao();
Metadata metadata = metadataDao.getActiveById(id);
if (metadata == null) {
throw new NotFoundException();
}
// Delete the metadata
metadataDao.delete(id, principal.getId());
// Always return OK
JsonObjectBuilder response = Json.createObjectBuilder()
.add("status", "ok");
return Response.ok().entity(response.build()).build();
}
}

View File

@@ -326,7 +326,7 @@ public class RouteResource extends BaseResource {
// Delete the route and the steps
RouteDao routeDao = new RouteDao();
routeDao.deleteRoute(routeStepDto.getRouteId());
routeDao.deleteRoute(routeStepDto.getRouteId(), principal.getId());
// Always return OK
JsonObjectBuilder response = Json.createObjectBuilder()

View File

@@ -155,7 +155,7 @@ public class TagResource extends BaseResource {
* @apiSuccess {String} id Tag ID
* @apiError (client) ForbiddenError Access denied
* @apiError (client) ValidationError Validation error
* @apiError (client) SpacesNotAllowed Spaces are not allowed in tag name
* @apiError (client) IllegalTagName Spaces and colons are not allowed in tag name
* @apiError (client) ParentNotFound Parent not found
* @apiPermission user
* @apiVersion 1.5.0
@@ -177,11 +177,7 @@ public class TagResource extends BaseResource {
// Validate input data
name = ValidationUtil.validateLength(name, "name", 1, 36, false);
ValidationUtil.validateHexColor(color, "color", true);
// Don't allow spaces
if (name.contains(" ")) {
throw new ClientException("SpacesNotAllowed", "Spaces are not allowed in tag name");
}
ValidationUtil.validateTagName(name);
// Check the parent
if (StringUtils.isEmpty(parentId)) {
@@ -237,7 +233,7 @@ public class TagResource extends BaseResource {
* @apiSuccess {String} id Tag ID
* @apiError (client) ForbiddenError Access denied
* @apiError (client) ValidationError Validation error
* @apiError (client) SpacesNotAllowed Spaces are not allowed in tag name
* @apiError (client) IllegalTagName Spaces and colons are not allowed in tag name
* @apiError (client) ParentNotFound Parent not found
* @apiError (client) CircularReference Circular reference in parent tag
* @apiError (client) NotFound Tag not found
@@ -263,11 +259,7 @@ public class TagResource extends BaseResource {
// Validate input data
name = ValidationUtil.validateLength(name, "name", 1, 36, true);
ValidationUtil.validateHexColor(color, "color", true);
// Don't allow spaces
if (name.contains(" ")) {
throw new ClientException("SpacesNotAllowed", "Spaces are not allowed in tag name");
}
ValidationUtil.validateTagName(name);
// Check permission
AclDao aclDao = new AclDao();

View File

@@ -82,7 +82,7 @@ public class ThemeResource extends BaseResource {
public Response get() {
JsonObject themeConfig = getThemeConfig();
JsonObjectBuilder json = Json.createObjectBuilder();
json.add("name", themeConfig.getString("name", "Sismics Docs"));
json.add("name", themeConfig.getString("name", "Teedy"));
json.add("color", themeConfig.getString("color", "#ffffff"));
json.add("css", themeConfig.getString("css", ""));
return Response.ok().entity(json.build()).build();

View File

@@ -2,6 +2,7 @@ package com.sismics.docs.rest.resource;
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import com.sismics.docs.core.constant.AclTargetType;
import com.sismics.docs.core.constant.ConfigType;
import com.sismics.docs.core.constant.Constants;
import com.sismics.docs.core.dao.*;
@@ -15,6 +16,7 @@ import com.sismics.docs.core.event.PasswordLostEvent;
import com.sismics.docs.core.model.context.AppContext;
import com.sismics.docs.core.model.jpa.*;
import com.sismics.docs.core.util.ConfigUtil;
import com.sismics.docs.core.util.RoutingUtil;
import com.sismics.docs.core.util.authentication.AuthenticationUtil;
import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.docs.rest.constant.BaseFunction;
@@ -99,6 +101,7 @@ public class UserResource extends BaseResource {
user.setPassword(password);
user.setEmail(email);
user.setStorageQuota(storageQuota);
user.setOnboarding(true);
// Create the user
UserDao userDao = new UserDao();
@@ -434,6 +437,7 @@ public class UserResource extends BaseResource {
* @apiGroup User
* @apiSuccess {String} status Status OK
* @apiError (client) ForbiddenError Access denied or the user cannot be deleted
* @apiError (client) UserUsedInRouteModel The user is used in a route model
* @apiPermission user
* @apiVersion 1.5.0
*
@@ -450,6 +454,12 @@ public class UserResource extends BaseResource {
throw new ClientException("ForbiddenError", "This user cannot be deleted");
}
// Check that this user is not used in any workflow
String routeModelName = RoutingUtil.findRouteModelNameByTargetName(AclTargetType.USER, principal.getName());
if (routeModelName != null) {
throw new ClientException("UserUsedInRouteModel", routeModelName);
}
// Find linked data
DocumentDao documentDao = new DocumentDao();
List<Document> documentList = documentDao.findByUserId(principal.getId());
@@ -493,6 +503,7 @@ public class UserResource extends BaseResource {
* @apiSuccess {String} status Status OK
* @apiError (client) ForbiddenError Access denied or the user cannot be deleted
* @apiError (client) UserNotFound The user does not exist
* @apiError (client) UserUsedInRouteModel The user is used in a route model
* @apiPermission admin
* @apiVersion 1.5.0
*
@@ -512,7 +523,7 @@ public class UserResource extends BaseResource {
throw new ClientException("ForbiddenError", "The guest user cannot be deleted");
}
// Check if the user exists
// Check that the user exists
UserDao userDao = new UserDao();
User user = userDao.getActiveByUsername(username);
if (user == null) {
@@ -526,6 +537,12 @@ public class UserResource extends BaseResource {
throw new ClientException("ForbiddenError", "The admin user cannot be deleted");
}
// Check that this user is not used in any workflow
String routeModelName = RoutingUtil.findRouteModelNameByTargetName(AclTargetType.USER, username);
if (routeModelName != null) {
throw new ClientException("UserUsedInRouteModel", routeModelName);
}
// Find linked data
DocumentDao documentDao = new DocumentDao();
List<Document> documentList = documentDao.findByUserId(user.getId());
@@ -606,6 +623,7 @@ public class UserResource extends BaseResource {
* @apiGroup User
* @apiSuccess {Boolean} anonymous True if no user is connected
* @apiSuccess {Boolean} is_default_password True if the admin has the default password
* @apiSuccess {Boolean} onboarding True if the UI needs to display the onboarding
* @apiSuccess {String} username Username
* @apiSuccess {String} email E-mail
* @apiSuccess {Number} storage_quota Storage quota (in bytes)
@@ -649,7 +667,8 @@ public class UserResource extends BaseResource {
.add("email", user.getEmail())
.add("storage_quota", user.getStorageQuota())
.add("storage_current", user.getStorageCurrent())
.add("totp_enabled", user.getTotpKey() != null);
.add("totp_enabled", user.getTotpKey() != null)
.add("onboarding", user.isOnboarding());
// Base functions
JsonArrayBuilder baseFunctions = Json.createArrayBuilder();
@@ -883,6 +902,39 @@ public class UserResource extends BaseResource {
return Response.ok().entity(response.build()).build();
}
/**
* Mark the onboarding experience as passed.
*
* @api {post} /user/onboarded Mark the onboarding experience as passed
* @apiDescription Once the onboarding experience has been passed by the user, this resource prevent it from being displayed again.
* @apiName PostUserOnboarded
* @apiGroup User
* @apiSuccess {String} status Status OK
* @apiError (client) ForbiddenError Access denied
* @apiPermission user
* @apiVersion 1.7.0
*
* @return Response
*/
@POST
@Path("onboarded")
public Response onboarded() {
if (!authenticate()) {
throw new ForbiddenClientException();
}
// Save it
UserDao userDao = new UserDao();
User user = userDao.getActiveByUsername(principal.getName());
user.setOnboarding(false);
userDao.updateOnboarding(user);
// Always return OK
JsonObjectBuilder response = Json.createObjectBuilder()
.add("status", "ok");
return Response.ok().entity(response.build()).build();
}
/**
* Enable time-based one-time password.
*

View File

@@ -198,7 +198,7 @@ public class VocabularyResource extends BaseResource {
/**
* Delete a vocabulary entry.
*
* @api {delete} /vocabulary/:id Delete vocabulary entry
* @api {delete} /vocabulary/:id Delete a vocabulary entry
* @apiName DeleteVocabularyId
* @apiGroup Vocabulary
* @apiParam {String} id Entry ID

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -5,7 +5,7 @@
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0"
metadata-complete="true">
<display-name>Docs</display-name>
<display-name>Teedy</display-name>
<!-- Proper loader/unloader of ImageIO plugins -->
<listener>

View File

@@ -1,4 +1,4 @@
The web client and Android application for **Sismics Docs** are only examples
The web client and Android application for **Teedy** are only examples
of what is possible with the provided REST API. Everything you see in those apps are
accessible using the API.
@@ -6,8 +6,8 @@ This documentation is divided in two parts. The first will get you started on es
steps like authentication and the second part is a full reference of every endpoints.
## API URL
The base URL depends on your server. If your instance of Docs is accessible through
`https://docs.mycompany.com`, then the base API URL is `https://docs.mycompany.com/api`.
The base URL depends on your server. If your instance of Teedy is accessible through
`https://teedy.mycompany.com`, then the base API URL is `https://teedy.mycompany.com/api`.
## Verbs and status codes
The API uses restful verbs.

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,8 @@
"url": "git://github.com/sismics/docs.git"
},
"apidoc": {
"name": "Sismics Docs API",
"title": "Sismics Docs API",
"name": "Teedy API",
"title": "Teedy API",
"url": "/api",
"template": {
"withCompare": false,

View File

@@ -1,7 +1,7 @@
'use strict';
/**
* Sismics Docs application.
* Teedy application.
*/
angular.module('docs',
// Dependencies
@@ -145,6 +145,15 @@ angular.module('docs',
}
}
})
.state('settings.metadata', {
url: '/metadata',
views: {
'settings': {
templateUrl: 'partial/docs/settings.metadata.html',
controller: 'SettingsMetadata'
}
}
})
.state('settings.user', {
url: '/user',
views: {
@@ -411,9 +420,10 @@ angular.module('docs',
prefix: 'locale/',
suffix: '.json?@build.date@'
})
.registerAvailableLanguageKeys(['en', 'fr', 'de', 'ru', 'zh_CN', 'zh_TW'], {
.registerAvailableLanguageKeys(['en', 'es', 'fr', 'de', 'ru', 'zh_CN', 'zh_TW'], {
'ru_*': 'ru',
'en_*': 'en',
'es_*': 'es',
'fr_*': 'fr',
'de_*': 'de',
'*': 'en'
@@ -426,6 +436,9 @@ angular.module('docs',
} else {
// Or else determine the language based on the user's browser
$translateProvider.determinePreferredLanguage();
if (!$translateProvider.use()) {
$translateProvider.use('en');
}
}
// Configuring Timago
@@ -508,7 +521,10 @@ angular.module('docs',
{ key: 'jpn', label: '日本語' },
{ key: 'tha', label: 'ภาษาไทย' },
{ key: 'kor', label: '한국어' },
{ key: 'nld', label: 'Nederlands' }
{ key: 'nld', label: 'Nederlands' },
{ key: 'tur', label: 'Türkçe' },
{ key: 'heb', label: 'עברית' },
{ key: 'hun', label: 'Magyar' }
];
})
/**

View File

@@ -319,9 +319,9 @@ angular.module('docs').controller('Document', function ($scope, $rootScope, $tim
*/
$scope.extractNavigatedTag = function () {
// Find the current tag in the search query
var tagFound = /tag:([^ ]*)/.exec($scope.search);
var tagFound = /(^| )tag:([^ ]*)/.exec($scope.search);
if (tagFound) {
tagFound = tagFound[1];
tagFound = tagFound[2];
// We search only for exact match
$scope.navigatedTag = _.findWhere($scope.tags, { name: tagFound });
} else {

View File

@@ -3,7 +3,7 @@
/**
* Document default controller.
*/
angular.module('docs').controller('DocumentDefault', function ($scope, $rootScope, $state, Restangular, Upload, $translate, $uibModal, $dialog) {
angular.module('docs').controller('DocumentDefault', function ($scope, $rootScope, $state, Restangular, Upload, $translate, $uibModal, $dialog, User) {
// Load user audit log
Restangular.one('auditlog').get().then(function (data) {
$scope.logs = data.logs;
@@ -13,7 +13,6 @@ angular.module('docs').controller('DocumentDefault', function ($scope, $rootScop
$scope.loadFiles = function () {
Restangular.one('file/list').get().then(function (data) {
$scope.files = data.files;
// TODO Keep currently uploading files
});
};
$scope.loadFiles();
@@ -121,7 +120,7 @@ angular.module('docs').controller('DocumentDefault', function ($scope, $rootScop
}
Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setBaseUrl('https://api.sismicsdocs.com');
RestangularConfigurer.setBaseUrl('https://api.teedy.io');
}).one('api').post('feedback', {
content: content
}).then(function () {
@@ -145,48 +144,51 @@ angular.module('docs').controller('DocumentDefault', function ($scope, $rootScop
// Onboarding
$translate('onboarding.step1.title').then(function () {
if (localStorage.onboardingDisplayed || $(window).width() < 1000) {
return;
}
localStorage.onboardingDisplayed = true;
$rootScope.onboardingEnabled = true;
$rootScope.onboardingSteps = [
{
title: $translate.instant('onboarding.step1.title'),
description: $translate.instant('onboarding.step1.description'),
position: 'centered',
width: 300
},
{
title: $translate.instant('onboarding.step2.title'),
description: $translate.instant('onboarding.step2.description'),
attachTo: '#document-add-btn',
position: 'right',
width: 300
},
{
title: $translate.instant('onboarding.step3.title'),
description: $translate.instant('onboarding.step3.description'),
attachTo: '#quick-upload-zone',
position: 'left',
width: 300
},
{
title: $translate.instant('onboarding.step4.title'),
description: $translate.instant('onboarding.step4.description'),
attachTo: '#search-box',
position: 'right',
width: 300
},
{
title: $translate.instant('onboarding.step5.title'),
description: $translate.instant('onboarding.step5.description'),
attachTo: '#navigation-tag',
position: "right",
width: 300
User.userInfo().then(function(userData) {
if (!userData.onboarding || $(window).width() < 1000) {
return;
}
];
Restangular.one('user').post('onboarded');
$rootScope.userInfo.onboarding = false;
$rootScope.onboardingEnabled = true;
$rootScope.onboardingSteps = [
{
title: $translate.instant('onboarding.step1.title'),
description: $translate.instant('onboarding.step1.description'),
position: 'centered',
width: 300
},
{
title: $translate.instant('onboarding.step2.title'),
description: $translate.instant('onboarding.step2.description'),
attachTo: '#document-add-btn',
position: 'right',
width: 300
},
{
title: $translate.instant('onboarding.step3.title'),
description: $translate.instant('onboarding.step3.description'),
attachTo: '#quick-upload-zone',
position: 'left',
width: 300
},
{
title: $translate.instant('onboarding.step4.title'),
description: $translate.instant('onboarding.step4.description'),
attachTo: '#search-box',
position: 'right',
width: 300
},
{
title: $translate.instant('onboarding.step5.title'),
description: $translate.instant('onboarding.step5.description'),
attachTo: '#navigation-tag',
position: "right",
width: 300
}
];
});
});
});

View File

@@ -59,9 +59,18 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $
$scope.document = {
tags: [],
relations: [],
language: language
language: language,
metadata: []
};
// Get custom metadata list
Restangular.one('metadata').get({
sort_column: 1,
asc: true
}).then(function(data) {
$scope.document.metadata = data.metadata;
});
if ($scope.navigatedTag) {
$scope.document.tags.push($scope.navigatedTag);
}
@@ -93,6 +102,20 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $
// Extract ids from relations (only when our document is the source)
document.relations = _.pluck(_.where(document.relations, { source: true }), 'id');
// Extract custom metadata values
var metadata = _.reject(document.metadata, function (meta) {
return _.isUndefined(meta.value) || meta.value === '' || meta.value == null;
});
document.metadata_id = _.pluck(metadata, 'id');
document.metadata_value = _.pluck(metadata, 'value');
document.metadata_value = _.map(document.metadata_value, function (val) {
if (val instanceof Date) {
return val.getTime();
}
return val;
});
// Send to server
if ($scope.isEdit()) {
promise = Restangular.one('document', $stateParams.id).post('', document);
} else {

View File

@@ -5,6 +5,7 @@
*/
angular.module('docs').controller('DocumentViewContent', function ($scope, $rootScope, $stateParams, Restangular, $dialog, $state, Upload, $translate, $uibModal) {
$scope.displayMode = _.isUndefined(localStorage.fileDisplayMode) ? 'grid' : localStorage.fileDisplayMode;
$scope.openedFile = undefined;
/**
* Watch for display mode change.
@@ -20,7 +21,15 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root
forceHelperSize: true,
forcePlaceholderSize: true,
tolerance: 'pointer',
start: function() {
$(this).addClass('currently-dragging');
},
stop: function () {
var _this = this;
setTimeout(function(){
$(_this).removeClass('currently-dragging');
}, 300);
// Send new positions to server
$scope.$apply(function () {
Restangular.one('file').post('reorder', {
@@ -37,7 +46,6 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root
$scope.loadFiles = function () {
Restangular.one('file/list').get({ id: $stateParams.id }).then(function (data) {
$scope.files = data.files;
// TODO Keep currently uploading files
});
};
$scope.loadFiles();
@@ -45,8 +53,11 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root
/**
* Navigate to the selected file.
*/
$scope.openFile = function (file) {
$state.go('document.view.content.file', { id: $stateParams.id, fileId: file.id })
$scope.openFile = function (file, $event) {
if ($($event.target).parents('.currently-dragging').length === 0) {
$scope.openedFile = file;
$state.go('document.view.content.file', { id: $stateParams.id, fileId: file.id });
}
};
/**
@@ -101,6 +112,9 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root
}
if (files && files.length) {
// Sort by filename
files = _.sortBy(files, 'name');
// Adding files to the UI
var newfiles = [];
_.each(files, function (file) {
@@ -195,10 +209,14 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root
});
};
/**
* Open versions history.
*/
$scope.openVersions = function (file) {
$uibModal.open({
templateUrl: 'partial/docs/file.versions.html',
controller: 'ModalFileVersions',
size: 'lg',
resolve: {
file: function () {
return file;

View File

@@ -29,7 +29,7 @@ angular.module('docs').controller('SettingsConfig', function($scope, $rootScope,
// Update the theme
$scope.update = function () {
$scope.theme.name = $scope.theme.name.length === 0 ? 'Sismics Docs' : $scope.theme.name;
$scope.theme.name = $scope.theme.name.length === 0 ? 'Teedy' : $scope.theme.name;
Restangular.one('theme').post('', $scope.theme).then(function () {
var stylesheet = $('#theme-stylesheet')[0];
stylesheet.href = stylesheet.href.replace(/\?.*|$/, '?' + new Date().getTime());

View File

@@ -46,12 +46,17 @@ angular.module('docs').controller('SettingsGroupEdit', function($scope, $dialog,
$state.go('settings.group.edit', { name: group.name });
}
}, function (e) {
if (e.data.type === 'GroupAlreadyExists') {
var title = $translate.instant('settings.group.edit.edit_group_failed_title');
var msg = $translate.instant('settings.group.edit.edit_group_failed_message');
var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}];
$dialog.messageBox(title, msg, btns);
}
if (e.data.type === 'GroupAlreadyExists') {
var title = $translate.instant('settings.group.edit.edit_group_failed_title');
var msg = $translate.instant('settings.group.edit.edit_group_failed_message');
var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}];
$dialog.messageBox(title, msg, btns);
} else if (e.data.type === 'GroupUsedInRouteModel') {
var title = $translate.instant('settings.group.edit.group_used_title');
var msg = $translate.instant('settings.group.edit.group_used_message', { name: e.data.message });
var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}];
$dialog.messageBox(title, msg, btns);
}
});
};
@@ -71,8 +76,13 @@ angular.module('docs').controller('SettingsGroupEdit', function($scope, $dialog,
Restangular.one('group', $stateParams.name).remove().then(function() {
$scope.loadGroups();
$state.go('settings.group');
}, function() {
$state.go('settings.group');
}, function(e) {
if (e.data.type === 'GroupUsedInRouteModel') {
var title = $translate.instant('settings.group.edit.group_used_title');
var msg = $translate.instant('settings.group.edit.group_used_message', { name: e.data.message });
var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}];
$dialog.messageBox(title, msg, btns);
}
});
}
});

View File

@@ -0,0 +1,34 @@
'use strict';
/**
* Settings metadata page controller.
*/
angular.module('docs').controller('SettingsMetadata', function($scope, Restangular) {
// Load metadata
Restangular.one('metadata').get({
sort_column: 1,
asc: true
}).then(function(data) {
$scope.metadata = data.metadata;
});
// Add a metadata
$scope.addMetadata = function() {
Restangular.one('metadata').put($scope.newmetadata).then(function(data) {
$scope.metadata.push(data);
$scope.newmetadata = {};
});
};
// Delete a metadata
$scope.deleteMetadata = function(meta) {
Restangular.one('metadata', meta.id).remove().then(function() {
$scope.metadata.splice($scope.metadata.indexOf(meta), 1);
});
};
// Update a metadata
$scope.updateMetadata = function(meta) {
Restangular.one('metadata', meta.id).post('', meta);
};
});

View File

@@ -70,8 +70,13 @@ angular.module('docs').controller('SettingsUserEdit', function($scope, $dialog,
Restangular.one('user', $stateParams.username).remove().then(function () {
$scope.loadUsers();
$state.go('settings.user');
}, function() {
$state.go('settings.user');
}, function(e) {
if (e.data.type === 'UserUsedInRouteModel') {
var title = $translate.instant('settings.user.edit.user_used_title');
var msg = $translate.instant('settings.user.edit.user_used_message', { name: e.data.message });
var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}];
$dialog.messageBox(title, msg, btns);
}
});
}
});

View File

@@ -0,0 +1,12 @@
'use strict';
/**
* Add space between element directive.
*/
angular.module('docs').directive('addSpaceBetween', function () {
return function (scope, element) {
if(!scope.$last) {
element.after('&#32;');
}
}
});

View File

@@ -0,0 +1,25 @@
'use strict';
/**
* Invert text color for more legibility directive.
*/
angular.module('docs').directive('invertTextColor', function () {
return {
restrict: 'A',
link: function(scope, element, attrs) {
attrs.$observe('invertTextColor', function(hex) {
if (!hex || hex.length !== 7) {
return;
}
hex = hex.slice(1);
var r = parseInt(hex.slice(0, 2), 16),
g = parseInt(hex.slice(2, 4), 16),
b = parseInt(hex.slice(4, 6), 16);
element.css('color', (r * 0.299 + g * 0.587 + b * 0.114) > 186
? '#000000'
: '#FFFFFF');
});
}
}
});

View File

@@ -9,6 +9,7 @@ angular.module('docs').directive('selectRelation', function() {
templateUrl: 'partial/docs/directive.selectrelation.html',
replace: true,
scope: {
id: '=',
relations: '=',
ref: '@',
ngDisabled: '='
@@ -18,21 +19,12 @@ angular.module('docs').directive('selectRelation', function() {
* Add a relation.
*/
$scope.addRelation = function($item) {
// Does the new relation is already in the model
var duplicate = _.find($scope.relations, function(relation) {
if ($item.id === relation.id) {
return relation;
}
});
// Add the new relation
if (!duplicate) {
$scope.relations.push({
id: $item.id,
title: $item.title,
source: true
});
}
$scope.relations.push({
id: $item.id,
title: $item.title,
source: true
});
$scope.input = '';
};
@@ -42,11 +34,11 @@ angular.module('docs').directive('selectRelation', function() {
$scope.deleteRelation = function(deleteRelation) {
$scope.relations = _.reject($scope.relations, function(relation) {
return relation.id === deleteRelation.id;
})
});
};
/**
* Returns a promise for typeahead title.
* Returns a promise for typeahead document.
*/
$scope.getDocumentTypeahead = function($viewValue) {
var deferred = $q.defer();
@@ -57,8 +49,16 @@ angular.module('docs').directive('selectRelation', function() {
asc: true,
search: $viewValue
}).then(function(data) {
deferred.resolve(data.documents);
});
deferred.resolve(_.reject(data.documents, function(document) {
var duplicate = _.find($scope.relations, function(relation) {
if (document.id === relation.id) {
return relation;
}
});
return document.id === $scope.id || duplicate;
}));
});
return deferred.promise;
};
},

View File

@@ -61,9 +61,10 @@ angular.module('share',
prefix: 'locale/',
suffix: '.json?@build.date@'
})
.registerAvailableLanguageKeys(['en', 'fr', 'de', 'ru', 'zh_CN', 'zh_TW'], {
.registerAvailableLanguageKeys(['en', 'es', 'fr', 'de', 'ru', 'zh_CN', 'zh_TW'], {
'ru_*': 'ru',
'en_*': 'en',
'es_*': 'es',
'fr_*': 'fr',
'de_*': 'de',
'*': 'en'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Some files were not shown because too many files have changed in this diff Show More