├── README.md ├── app ├── views │ ├── tags │ │ ├── block │ │ │ ├── time.html │ │ │ ├── top-bar-user.html │ │ │ └── menu.html │ │ ├── user │ │ │ ├── name.html │ │ │ └── picture.html │ │ ├── document │ │ │ ├── list.html │ │ │ ├── description.html │ │ │ ├── title.html │ │ │ ├── preview.html │ │ │ ├── list-item.html │ │ │ └── stats.html │ │ ├── discussion │ │ │ ├── list.html │ │ │ ├── title.html │ │ │ ├── list-item.html │ │ │ └── stats.html │ │ ├── form │ │ │ ├── select.html │ │ │ ├── textarea.html │ │ │ ├── input.html │ │ │ └── field.html │ │ ├── vote │ │ │ ├── button.html │ │ │ └── scripts.html │ │ └── comment │ │ │ ├── list-item.html │ │ │ └── scripts.html │ ├── admin │ │ ├── Comments │ │ │ ├── blank.html │ │ │ ├── list.html │ │ │ └── show.html │ │ ├── Users │ │ │ ├── blank.html │ │ │ ├── list.html │ │ │ └── show.html │ │ ├── Documents │ │ │ ├── blank.html │ │ │ ├── list.html │ │ │ └── show.html │ │ ├── Discussions │ │ │ ├── blank.html │ │ │ ├── list.html │ │ │ └── show.html │ │ └── Categories │ │ │ ├── list.html │ │ │ ├── show.html │ │ │ └── blank.html │ ├── errors │ │ ├── 404.html │ │ └── 500.html │ ├── web │ │ ├── Documents │ │ │ ├── ajax_list.html │ │ │ ├── edit.html │ │ │ └── list.html │ │ ├── Discussions │ │ │ ├── ajax_list.html │ │ │ ├── edit.html │ │ │ ├── add.html │ │ │ └── list.html │ │ ├── Comments │ │ │ └── list.html │ │ ├── Users │ │ │ └── read.html │ │ └── WebController │ │ │ └── index.html │ ├── CRUD │ │ └── layout.html │ ├── web.html.bak │ └── web.html ├── jobs │ ├── Bootstrap.java │ ├── DeleteBackgroundJobStatusJob.java │ ├── IncrementDocumentCopyCountJob.java │ ├── IncrementDocumentReadCountJob.java │ ├── IncrementDocumentDownloadCountJob.java │ ├── UpdateDocumentCommentCountJob.java │ ├── FetchDocumentThumbnailJob.java │ └── CopyDocumentJob.java ├── models │ ├── BackgroundJobStatus.java │ ├── Thumbnail.java │ ├── DocumentJobStatus.java │ ├── UserRole.java │ ├── ExportLink.java │ ├── DiscussionDocument.java │ ├── dialects │ │ └── FullTextSearchPostgreSQLDialect.java │ ├── BaseModel.java │ ├── Category.java │ ├── enums │ │ └── Mime.java │ ├── User.java │ ├── Vote.java │ └── Comment.java ├── controllers │ ├── admin │ │ ├── Users.java │ │ ├── Comments.java │ │ ├── Documents.java │ │ ├── Categories.java │ │ └── Discussions.java │ ├── AdminController.java │ ├── api │ │ ├── DocumentJobsStatus.java │ │ ├── Categories.java │ │ ├── Comments.java │ │ ├── Votes.java │ │ ├── ApiController.java │ │ ├── DiscussionDocuments.java │ │ ├── Discussions.java │ │ └── Documents.java │ ├── web │ │ ├── Users.java │ │ ├── Comments.java │ │ ├── Documents.java │ │ ├── Discussions.java │ │ ├── WebController.java │ │ └── Auth.java │ ├── RolesHandler.java │ └── AppController.java ├── utils │ └── Labels.java └── services │ └── googleoauth │ ├── GoogleOAuthTokens.java │ ├── GoogleOAuthCredentialListener.java │ ├── GoogleUserInfo.java │ ├── GoogleOAuthConfig.java │ └── GoogleOAuth.java ├── public ├── images │ ├── doc.png │ ├── logo.png │ ├── user.png │ ├── favicon.png │ ├── logo-large.png │ ├── logo-medium.png │ └── hero-graphic.png ├── stylesheets │ ├── fonts │ │ ├── icomoon.eot │ │ ├── icomoon.ttf │ │ └── icomoon.woff │ └── icons.css └── javascripts │ ├── jquery.timeago.fr.js │ ├── jquery.timeago.en.js │ ├── app.js │ ├── jquery.cookie.js │ ├── bootstrap.button.js │ ├── jquery.placeholder.js │ └── jquery.timeago.js ├── test ├── data.yml ├── Application.test.html ├── BasicTest.java └── ApplicationTest.java ├── db └── evolutions │ ├── 5.sql │ ├── 7.sql │ ├── 2.sql │ ├── 3.sql │ ├── 4.sql │ ├── 8.sql │ ├── 6.sql │ └── 1.sql ├── conf ├── application.override.conf ├── dependencies.yml ├── routes └── messages.en └── .gitignore /README.md: -------------------------------------------------------------------------------- 1 | See http://www.opendocshub.org for help -------------------------------------------------------------------------------- /app/views/tags/block/time.html: -------------------------------------------------------------------------------- 1 | 2 | ${_time} 3 | -------------------------------------------------------------------------------- /app/views/tags/user/name.html: -------------------------------------------------------------------------------- 1 | 2 | ${_user} 3 | -------------------------------------------------------------------------------- /public/images/doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angebagui/odh-web/master/public/images/doc.png -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angebagui/odh-web/master/public/images/logo.png -------------------------------------------------------------------------------- /public/images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angebagui/odh-web/master/public/images/user.png -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angebagui/odh-web/master/public/images/favicon.png -------------------------------------------------------------------------------- /app/views/tags/document/list.html: -------------------------------------------------------------------------------- 1 | #{list _documents} 2 | #{document.list-item document : _ /} 3 | #{/list} -------------------------------------------------------------------------------- /public/images/logo-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angebagui/odh-web/master/public/images/logo-large.png -------------------------------------------------------------------------------- /public/images/logo-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angebagui/odh-web/master/public/images/logo-medium.png -------------------------------------------------------------------------------- /public/images/hero-graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angebagui/odh-web/master/public/images/hero-graphic.png -------------------------------------------------------------------------------- /app/views/tags/discussion/list.html: -------------------------------------------------------------------------------- 1 | #{list _discussions} 2 | #{discussion.list-item discussion : _ /} 3 | #{/list} 4 | 5 | -------------------------------------------------------------------------------- /public/stylesheets/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angebagui/odh-web/master/public/stylesheets/fonts/icomoon.eot -------------------------------------------------------------------------------- /public/stylesheets/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angebagui/odh-web/master/public/stylesheets/fonts/icomoon.ttf -------------------------------------------------------------------------------- /public/stylesheets/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angebagui/odh-web/master/public/stylesheets/fonts/icomoon.woff -------------------------------------------------------------------------------- /app/views/tags/discussion/title.html: -------------------------------------------------------------------------------- 1 | ${_discussion.title.toLowerCase()} -------------------------------------------------------------------------------- /test/data.yml: -------------------------------------------------------------------------------- 1 | # you describe your data using the YAML notation here 2 | # and then load them using Fixtures.load("data.yml") 3 | 4 | # User(bob): 5 | # email: bob@gmail.com 6 | # password: secret 7 | # fullname: Bob -------------------------------------------------------------------------------- /app/views/tags/document/description.html: -------------------------------------------------------------------------------- 1 |
2 | ${_document.description?.length() > 200 ? _document.description.substring(0, 200) + '...' : _document.description} 3 |   4 |
-------------------------------------------------------------------------------- /app/views/tags/form/select.html: -------------------------------------------------------------------------------- 1 | #{form.field field : _field, label : _label, help : _help} 2 | #{select _field.name, items:_items, valueProperty : _valueProperty, labelProperty : _labelProperty, value:_value, id: _field.id /} 3 | #{/form.field} -------------------------------------------------------------------------------- /app/views/tags/form/textarea.html: -------------------------------------------------------------------------------- 1 | #{form.field field : _field, label : _label, help : _help} 2 | 3 | #{/form.field} -------------------------------------------------------------------------------- /test/Application.test.html: -------------------------------------------------------------------------------- 1 | *{ You can use plain selenium command using the selenium tag }* 2 | 3 | #{selenium} 4 | // Open the home page, and check that no error occured 5 | open('/') 6 | assertNotTitle('Application error') 7 | #{/selenium} -------------------------------------------------------------------------------- /test/BasicTest.java: -------------------------------------------------------------------------------- 1 | import org.junit.*; 2 | import java.util.*; 3 | import play.test.*; 4 | import models.*; 5 | 6 | public class BasicTest extends UnitTest { 7 | 8 | @Test 9 | public void aVeryImportantThingToTest() { 10 | assertEquals(2, 1 + 1); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/views/tags/form/input.html: -------------------------------------------------------------------------------- 1 | #{form.field field : _field, label : _label, help : _help} 2 | 3 | #{/form.field} -------------------------------------------------------------------------------- /app/jobs/Bootstrap.java: -------------------------------------------------------------------------------- 1 | package jobs; 2 | 3 | import play.Logger; 4 | import play.jobs.Job; 5 | import play.jobs.OnApplicationStart; 6 | 7 | @OnApplicationStart 8 | public class Bootstrap extends Job { 9 | 10 | @Override 11 | public void doJob() { 12 | Logger.info("Application started"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /db/evolutions/5.sql: -------------------------------------------------------------------------------- 1 | # --- !Ups 2 | 3 | ALTER TABLE "document" ALTER "description" TYPE character varying(5000); 4 | ALTER TABLE "comment" ALTER "content" TYPE character varying(5000); 5 | 6 | # --- !Downs 7 | 8 | ALTER TABLE "document" ALTER "description" TYPE character varying(255); 9 | ALTER TABLE "comment" ALTER "content" TYPE character varying(255); -------------------------------------------------------------------------------- /app/views/admin/Comments/blank.html: -------------------------------------------------------------------------------- 1 | #{extends 'CRUD/layout.html' /} 2 | #{set title:messages.get('crud.blank.title', type.modelName) /} 3 | 4 |
5 | 6 |

&{'crud.blank.title', type.modelName}

7 | 8 |
9 |

Please add a comment through the frontend.

10 |
11 | 12 |
-------------------------------------------------------------------------------- /app/views/admin/Users/blank.html: -------------------------------------------------------------------------------- 1 | #{extends 'CRUD/layout.html' /} 2 | #{set title:messages.get('crud.blank.title', type.modelName) /} 3 | 4 |
5 | 6 |

&{'crud.blank.title', type.modelName}

7 | 8 |
9 |

Users have to register through the frontend.

10 |
11 | 12 |
-------------------------------------------------------------------------------- /db/evolutions/7.sql: -------------------------------------------------------------------------------- 1 | # --- !Ups 2 | ALTER TABLE thumbnail ADD COLUMN document_id bigint; 3 | ALTER TABLE thumbnail 4 | ADD CONSTRAINT fk83434d34d0b1afe FOREIGN KEY (document_id) REFERENCES "document" (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE; 5 | 6 | # --- !Downs 7 | ALTER TABLE thumbnail 8 | DROP CONSTRAINT fk83434d34d0b1afe; 9 | 10 | ALTER TABLE thumbnail DROP COLUMN document_id; -------------------------------------------------------------------------------- /db/evolutions/2.sql: -------------------------------------------------------------------------------- 1 | # --- !Ups 2 | CREATE TABLE documentjobstatus 3 | ( 4 | id bigint NOT NULL, 5 | status integer, 6 | resultdocumentid bigint NOT NULL, 7 | created timestamp with time zone, 8 | updated timestamp with time zone, 9 | CONSTRAINT documentjobstatus_pkey PRIMARY KEY (id) 10 | ) 11 | WITH ( 12 | OIDS=FALSE 13 | ); 14 | DROP TABLE clonedocumentjobstatus; 15 | 16 | # --- !Downs 17 | DROP TABLE documentjobstatus; -------------------------------------------------------------------------------- /conf/application.override.conf: -------------------------------------------------------------------------------- 1 | # Uncomment the following lines if you want to use this file to override the default configuration file 2 | 3 | #siteName=YOUR SITE NAME 4 | #siteDescription = YOUR SITE DESCRIPTION 5 | #lang=en 6 | #google.oAuth.client_id=YOUR GOOGLE OAUTH CLIENT ID 7 | #google.oAuth.client_secret=YOUR GOOGLE OAUTH CLIENT SECRET 8 | 9 | #application.secret=YOUR APPLICATION SECRET 10 | #db=postgres://user:pass@host/database -------------------------------------------------------------------------------- /app/models/BackgroundJobStatus.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import javax.persistence.MappedSuperclass; 4 | 5 | import play.db.jpa.Model; 6 | 7 | import com.fasterxml.jackson.annotation.JsonProperty; 8 | 9 | @MappedSuperclass 10 | public class BackgroundJobStatus extends BaseModel { 11 | 12 | @JsonProperty 13 | public Status status; 14 | 15 | public static enum Status { 16 | PENDING, FAIL, SUCCESS 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/views/admin/Documents/blank.html: -------------------------------------------------------------------------------- 1 | #{extends 'CRUD/layout.html' /} 2 | #{set title:messages.get('crud.blank.title', type.modelName) /} 3 | 4 |
5 | 6 |

&{'crud.blank.title', type.modelName}

7 | 8 |
9 |

Please use the document upload form on the frontend.

10 |
11 | 12 |
-------------------------------------------------------------------------------- /app/views/tags/user/picture.html: -------------------------------------------------------------------------------- 1 | 2 | #{if _user?.picture} 3 | ${_user.name} 4 | #{/if} 5 | #{else} 6 | ${_user.name} 7 | #{/else} 8 | #{if _showName} 9 | ${_user} 10 | #{/if} 11 | -------------------------------------------------------------------------------- /app/jobs/DeleteBackgroundJobStatusJob.java: -------------------------------------------------------------------------------- 1 | package jobs; 2 | 3 | import java.util.Date; 4 | 5 | import models.DocumentJobStatus; 6 | import play.jobs.Every; 7 | import play.jobs.Job; 8 | 9 | @Every("1h") 10 | public class DeleteBackgroundJobStatusJob extends Job { 11 | 12 | @Override 13 | public void doJob() { 14 | DocumentJobStatus.delete("created < ?", new Date(new Date().getTime() - (60 * 60 * 1000))); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/views/admin/Discussions/blank.html: -------------------------------------------------------------------------------- 1 | #{extends 'CRUD/layout.html' /} 2 | #{set title:messages.get('crud.blank.title', type.modelName) /} 3 | 4 |
5 | 6 |

&{'crud.blank.title', type.modelName}

7 | 8 |
9 |

Please use the "Start a discussion" form on the frontend.

10 |
11 | 12 |
-------------------------------------------------------------------------------- /app/controllers/admin/Users.java: -------------------------------------------------------------------------------- 1 | package controllers.admin; 2 | 3 | import models.User; 4 | import models.UserRole; 5 | import play.mvc.With; 6 | import controllers.CRUD; 7 | import controllers.deadbolt.Deadbolt; 8 | import controllers.deadbolt.Restrict; 9 | import controllers.deadbolt.Restrictions; 10 | 11 | @With({ Deadbolt.class }) 12 | @CRUD.For(User.class) 13 | @Restrictions(@Restrict(UserRole.ADMIN)) 14 | public class Users extends CRUD { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/controllers/admin/Comments.java: -------------------------------------------------------------------------------- 1 | package controllers.admin; 2 | 3 | import models.Comment; 4 | import models.UserRole; 5 | import play.mvc.With; 6 | import controllers.CRUD; 7 | import controllers.deadbolt.Deadbolt; 8 | import controllers.deadbolt.Restrict; 9 | import controllers.deadbolt.Restrictions; 10 | 11 | @With({ Deadbolt.class }) 12 | @Restrictions(@Restrict(UserRole.ADMIN)) 13 | @CRUD.For(Comment.class) 14 | public class Comments extends CRUD { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/controllers/admin/Documents.java: -------------------------------------------------------------------------------- 1 | package controllers.admin; 2 | 3 | import models.Document; 4 | import models.UserRole; 5 | import play.mvc.With; 6 | import controllers.CRUD; 7 | import controllers.deadbolt.Deadbolt; 8 | import controllers.deadbolt.Restrict; 9 | import controllers.deadbolt.Restrictions; 10 | 11 | @With({ Deadbolt.class }) 12 | @Restrictions(@Restrict(UserRole.ADMIN)) 13 | @CRUD.For(Document.class) 14 | public class Documents extends CRUD { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/models/Thumbnail.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import javax.persistence.CascadeType; 4 | import javax.persistence.Entity; 5 | import javax.persistence.Lob; 6 | import javax.persistence.OneToOne; 7 | 8 | import play.data.validation.Required; 9 | import play.db.jpa.Model; 10 | 11 | @Entity 12 | public class Thumbnail extends Model { 13 | 14 | @Required 15 | @Lob 16 | public byte[] image; 17 | 18 | @Required 19 | public String mimeType; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/controllers/admin/Categories.java: -------------------------------------------------------------------------------- 1 | package controllers.admin; 2 | 3 | import models.Category; 4 | import models.UserRole; 5 | import play.mvc.With; 6 | import controllers.CRUD; 7 | import controllers.deadbolt.Deadbolt; 8 | import controllers.deadbolt.Restrict; 9 | import controllers.deadbolt.Restrictions; 10 | 11 | @With({ Deadbolt.class }) 12 | @Restrictions(@Restrict(UserRole.ADMIN)) 13 | @CRUD.For(Category.class) 14 | public class Categories extends CRUD { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Extracted from https://github.com/ulrich/macaron-factory/blob/master/.gitignore 2 | # Ignore all dotfiles... 3 | .* 4 | # except for .gitignore 5 | !.gitignore 6 | 7 | # Ignore Play! working directory # 8 | .classpath 9 | .project 10 | .settings 11 | eclipse 12 | javadoc 13 | lib 14 | log 15 | logs 16 | modules 17 | nbproject 18 | precompiled 19 | tmp 20 | test-result 21 | eclipse 22 | server.pid 23 | *.db 24 | *.iws 25 | *.ipr 26 | *.iml 27 | *.log -------------------------------------------------------------------------------- /app/controllers/admin/Discussions.java: -------------------------------------------------------------------------------- 1 | package controllers.admin; 2 | 3 | import models.Discussion; 4 | import models.UserRole; 5 | import play.mvc.With; 6 | import controllers.CRUD; 7 | import controllers.deadbolt.Deadbolt; 8 | import controllers.deadbolt.Restrict; 9 | import controllers.deadbolt.Restrictions; 10 | 11 | @With({ Deadbolt.class }) 12 | @Restrictions(@Restrict(UserRole.ADMIN)) 13 | @CRUD.For(Discussion.class) 14 | public class Discussions extends CRUD { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /test/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | import org.junit.*; 2 | import play.test.*; 3 | import play.mvc.*; 4 | import play.mvc.Http.*; 5 | import models.*; 6 | 7 | public class ApplicationTest extends FunctionalTest { 8 | 9 | @Test 10 | public void testThatIndexPageWorks() { 11 | Response response = GET("/"); 12 | assertIsOk(response); 13 | assertContentType("text/html", response); 14 | assertCharset(play.Play.defaultWebEncoding, response); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /app/models/DocumentJobStatus.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.Lob; 5 | 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | 8 | import play.db.jpa.Model; 9 | 10 | @Entity 11 | public class DocumentJobStatus extends BackgroundJobStatus { 12 | 13 | @JsonProperty 14 | @Lob 15 | public String result; 16 | 17 | public DocumentJobStatus() { 18 | this.status = Status.PENDING; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/controllers/AdminController.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import models.UserRole; 4 | import play.mvc.With; 5 | import controllers.admin.Users; 6 | import controllers.deadbolt.Deadbolt; 7 | import controllers.deadbolt.Restrict; 8 | import controllers.deadbolt.Restrictions; 9 | 10 | @With({ Deadbolt.class }) 11 | @Restrictions(@Restrict(UserRole.ADMIN)) 12 | public class AdminController extends CRUD { 13 | 14 | public static void index() { 15 | Users.index(); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/controllers/api/DocumentJobsStatus.java: -------------------------------------------------------------------------------- 1 | package controllers.api; 2 | 3 | import models.DocumentJobStatus; 4 | import controllers.AppController; 5 | import play.mvc.With; 6 | 7 | @With(ApiController.class) 8 | public class DocumentJobsStatus extends AppController { 9 | 10 | public static void read(long id) { 11 | DocumentJobStatus documentJobStatus = DocumentJobStatus.findById(id); 12 | notFoundIfNull(documentJobStatus); 13 | renderJSON(documentJobStatus); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/views/errors/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Not found 6 | 7 | 8 | 9 | #{if play.mode.name() == 'DEV'} 10 | #{404 result /} 11 | #{/if} 12 | #{else} 13 |

Not found

14 |

15 | ${result.message} 16 |

17 | #{/else} 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/javascripts/jquery.timeago.fr.js: -------------------------------------------------------------------------------- 1 | // French 2 | jQuery.timeago.settings.strings = { 3 | // environ ~= about, it's optional 4 | prefixAgo: "il y a", 5 | prefixFromNow: "d'ici", 6 | seconds: "moins d'une minute", 7 | minute: "environ une minute", 8 | minutes: "environ %d minutes", 9 | hour: "environ une heure", 10 | hours: "environ %d heures", 11 | day: "environ un jour", 12 | days: "environ %d jours", 13 | month: "environ un mois", 14 | months: "environ %d mois", 15 | year: "un an", 16 | years: "%d ans" 17 | }; -------------------------------------------------------------------------------- /app/models/UserRole.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import models.deadbolt.Role; 4 | 5 | public class UserRole implements Role { 6 | 7 | public String name; 8 | 9 | public UserRole(String name) { 10 | this.name = name; 11 | } 12 | 13 | @Override 14 | public String getRoleName() { 15 | return this.name; 16 | } 17 | 18 | public final static String ADMIN = "admin"; 19 | public final static String MODERATOR = "moderator"; 20 | public final static String [] ROLES = {ADMIN, MODERATOR}; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /public/javascripts/jquery.timeago.en.js: -------------------------------------------------------------------------------- 1 | // English (Template) 2 | jQuery.timeago.settings.strings = { 3 | prefixAgo: null, 4 | prefixFromNow: null, 5 | suffixAgo: "ago", 6 | suffixFromNow: "from now", 7 | seconds: "less than a minute", 8 | minute: "about a minute", 9 | minutes: "%d minutes", 10 | hour: "about an hour", 11 | hours: "about %d hours", 12 | day: "a day", 13 | days: "%d days", 14 | month: "about a month", 15 | months: "%d months", 16 | year: "about a year", 17 | years: "%d years", 18 | wordSeparator: " ", 19 | numbers: [] 20 | }; 21 | -------------------------------------------------------------------------------- /app/jobs/IncrementDocumentCopyCountJob.java: -------------------------------------------------------------------------------- 1 | package jobs; 2 | 3 | import models.Document; 4 | import play.jobs.Job; 5 | 6 | public class IncrementDocumentCopyCountJob extends Job { 7 | 8 | private long documentId; 9 | 10 | public IncrementDocumentCopyCountJob(long documentId) { 11 | this.documentId = documentId; 12 | } 13 | 14 | @Override 15 | public void doJob() { 16 | Document document = Document.findById(this.documentId); 17 | if (document != null) { 18 | document.incrementCopyCountAndSave(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/jobs/IncrementDocumentReadCountJob.java: -------------------------------------------------------------------------------- 1 | package jobs; 2 | 3 | import models.Document; 4 | import play.jobs.Job; 5 | 6 | public class IncrementDocumentReadCountJob extends Job { 7 | 8 | private long documentId; 9 | 10 | public IncrementDocumentReadCountJob(long documentId) { 11 | this.documentId = documentId; 12 | } 13 | 14 | @Override 15 | public void doJob() { 16 | Document document = Document.findById(this.documentId); 17 | if (document != null) { 18 | document.incrementViewCountAndSave(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/jobs/IncrementDocumentDownloadCountJob.java: -------------------------------------------------------------------------------- 1 | package jobs; 2 | 3 | import models.Document; 4 | import play.jobs.Job; 5 | 6 | public class IncrementDocumentDownloadCountJob extends Job { 7 | 8 | private long documentId; 9 | 10 | public IncrementDocumentDownloadCountJob(long documentId) { 11 | this.documentId = documentId; 12 | } 13 | 14 | @Override 15 | public void doJob() { 16 | Document document = Document.findById(this.documentId); 17 | if (document != null) { 18 | document.incrementDownloadCountAndSave(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/controllers/web/Users.java: -------------------------------------------------------------------------------- 1 | package controllers.web; 2 | 3 | import java.util.List; 4 | 5 | import models.Discussion; 6 | import models.Document; 7 | import models.User; 8 | import play.mvc.With; 9 | import controllers.AppController; 10 | 11 | @With(WebController.class) 12 | public class Users extends AppController { 13 | 14 | public static void read(long id) { 15 | User user = User.findById(id); 16 | notFoundIfNull(user); 17 | List discussions = Discussion.findByUser(user); 18 | List documents = Document.findByUser(user); 19 | render(user, discussions, documents); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/views/tags/document/title.html: -------------------------------------------------------------------------------- 1 |
2 | #{if _document.mime.humanFriendlyName == '.pdf' } 3 | 4 | #{/if} 5 | #{elseif _document.mime.humanFriendlyName == '.docx' || _document.mime.humanFriendlyName == '.doc'} 6 | 7 | #{/elseif} 8 | #{elseif _document.mime.humanFriendlyName == '.pptx' } 9 | 10 | #{/elseif} 11 | #{else} 12 | 13 | #{/else} 14 | ${_document.title.toLowerCase()} 15 |
-------------------------------------------------------------------------------- /db/evolutions/3.sql: -------------------------------------------------------------------------------- 1 | # --- !Ups 2 | ALTER TABLE "document" ADD COLUMN searchable_text TSVECTOR; 3 | UPDATE "document" set searchable_text = to_tsvector('pg_catalog.english', coalesce(title,'') || ' ' || coalesce(description,'') || ' ' || coalesce(source,'')); 4 | 5 | CREATE INDEX search_idx ON "document" USING GIN(searchable_text); 6 | CREATE TRIGGER ts_searchable_text BEFORE INSERT OR UPDATE ON "document" 7 | FOR EACH ROW EXECUTE PROCEDURE tsvector_update_trigger(searchable_text, 'pg_catalog.english', title, description, source); 8 | 9 | # --- !Downs 10 | DROP TRIGGER ts_searchable_text ON "document"; 11 | DROP INDEX search_idx; 12 | ALTER TABLE "document" DROP COLUMN searchable_text; -------------------------------------------------------------------------------- /app/controllers/web/Comments.java: -------------------------------------------------------------------------------- 1 | package controllers.web; 2 | 3 | import java.util.List; 4 | 5 | import play.data.validation.Required; 6 | import play.mvc.With; 7 | 8 | import models.Comment; 9 | import models.Document; 10 | import controllers.AppController; 11 | 12 | @With(WebController.class) 13 | public class Comments extends AppController { 14 | 15 | public static void list(@Required String objectType, @Required long objectId, Integer page) { 16 | if (!validation.hasErrors()) { 17 | List comments = Comment.findByObject(objectType, objectId, page); 18 | render(comments, objectType, objectId); 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/views/admin/Categories/list.html: -------------------------------------------------------------------------------- 1 | #{extends 'CRUD/layout.html' /} 2 | #{set title:messages.get('crud.list.title', type.name) /} 3 | 4 |
5 | 6 |

&{'crud.list.title', type.name}

7 | 8 |
9 | #{crud.search /} 10 |
11 | 12 |
13 | #{crud.table fields:['name', 'objectType', 'created', 'updated']/} 14 |
15 | 16 |
17 | #{crud.pagination /} 18 |
19 | 20 |

21 | &{'crud.add', type.modelName} 22 |

23 | 24 |
-------------------------------------------------------------------------------- /app/views/admin/Users/list.html: -------------------------------------------------------------------------------- 1 | #{extends 'CRUD/layout.html' /} 2 | #{set title:messages.get('crud.list.title', type.name) /} 3 | 4 |
5 | 6 |

&{'crud.list.title', type.name}

7 | 8 |
9 | #{crud.search /} 10 |
11 | 12 |
13 | #{crud.table fields:['name', 'bio', 'userRoles', 'created', 'updated'] /} 14 |
15 | 16 |
17 | #{crud.pagination /} 18 |
19 | 20 |

21 | &{'crud.add', type.modelName} 22 |

23 | 24 |
25 | -------------------------------------------------------------------------------- /app/views/errors/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Application error 6 | 7 | 8 | 9 | #{if play.mode.name() == 'DEV'} 10 | #{500 exception /} 11 | #{/if} 12 | #{else} 13 |

Oops, an error occured

14 | #{if exception instanceof play.exceptions.PlayException} 15 |

16 | This exception has been logged with id ${exception.id}. 17 |

18 | #{/if} 19 | #{/else} 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/views/admin/Discussions/list.html: -------------------------------------------------------------------------------- 1 | #{extends 'CRUD/layout.html' /} 2 | #{set title:messages.get('crud.list.title', type.name) /} 3 | 4 |
5 | 6 |

&{'crud.list.title', type.name}

7 | 8 |
9 | #{crud.search /} 10 |
11 | 12 |
13 | #{crud.table fields:['title', 'category', 'content', 'tags', 'created', 'updated'] /} 14 |
15 | 16 |
17 | #{crud.pagination /} 18 |
19 | 20 |

21 | &{'crud.add', type.modelName} 22 |

23 | 24 |
25 | -------------------------------------------------------------------------------- /app/views/admin/Comments/list.html: -------------------------------------------------------------------------------- 1 | #{extends 'CRUD/layout.html' /} 2 | #{set title:messages.get('crud.list.title', type.name) /} 3 | 4 |
5 | 6 |

&{'crud.list.title', type.name}

7 | 8 |
9 | #{crud.search /} 10 |
11 | 12 |
13 | #{crud.table fields:['content', 'created', 'objectType', 'objectId', 'created', 'updated'] /} 14 |
15 | 16 |
17 | #{crud.pagination /} 18 |
19 | 20 |

21 | &{'crud.add', type.modelName} 22 |

23 | 24 |
25 | -------------------------------------------------------------------------------- /app/views/admin/Documents/list.html: -------------------------------------------------------------------------------- 1 | #{extends 'CRUD/layout.html' /} 2 | #{set title:messages.get('crud.list.title', type.name) /} 3 | 4 |
5 | 6 |

&{'crud.list.title', type.name}

7 | 8 |
9 | #{crud.search /} 10 |
11 | 12 |
13 | #{crud.table fields:['title', 'category', 'description', 'tags', 'created', 'updated'] /} 14 |
15 | 16 |
17 | #{crud.pagination /} 18 |
19 | 20 |

21 | &{'crud.add', type.modelName} 22 |

23 | 24 |
25 | -------------------------------------------------------------------------------- /db/evolutions/4.sql: -------------------------------------------------------------------------------- 1 | # --- !Ups 2 | 3 | ALTER TABLE "comment" 4 | ADD COLUMN iscensored boolean NOT NULL, 5 | ADD COLUMN isdeleted boolean NOT NULL, 6 | ADD COLUMN repliescount bigint NOT NULL, 7 | ADD COLUMN parent_id bigint; 8 | 9 | ALTER TABLE "comment" 10 | ADD CONSTRAINT fk9bde863fc152a78b FOREIGN KEY (parent_id) 11 | REFERENCES "comment" (id) MATCH SIMPLE 12 | ON UPDATE NO ACTION ON DELETE NO ACTION; 13 | 14 | # --- !Downs 15 | 16 | ALTER TABLE "comment" 17 | DROP CONSTRAINT fk9bde863fc152a78b; 18 | 19 | ALTER TABLE "comment" 20 | DROP COLUMN iscensored, 21 | DROP COLUMN isdeleted, 22 | DROP COLUMN repliescount, 23 | DROP COLUMN parent_id; 24 | -------------------------------------------------------------------------------- /app/jobs/UpdateDocumentCommentCountJob.java: -------------------------------------------------------------------------------- 1 | package jobs; 2 | 3 | import models.Document; 4 | import play.jobs.Job; 5 | 6 | public class UpdateDocumentCommentCountJob extends Job { 7 | 8 | private long documentId; 9 | private boolean increment; 10 | 11 | public UpdateDocumentCommentCountJob(long documentId, boolean increment) { 12 | this.documentId = documentId; 13 | this.increment = increment; 14 | } 15 | 16 | @Override 17 | public void doJob() { 18 | Document document = Document.findById(documentId); 19 | if (document!= null) { 20 | if (document != null) { 21 | document.updateCommentCountAndSave(this.increment); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/views/tags/form/field.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 11 |
12 |
13 | #{doBody /} 14 | 15 | ${_field.error} 16 | 17 | #{if _help} 18 |
19 | &{_help} 20 |
21 | #{/if} 22 |
23 |
-------------------------------------------------------------------------------- /app/jobs/FetchDocumentThumbnailJob.java: -------------------------------------------------------------------------------- 1 | package jobs; 2 | 3 | import java.io.IOException; 4 | 5 | import models.Document; 6 | import play.Logger; 7 | import play.jobs.Job; 8 | 9 | public class FetchDocumentThumbnailJob extends Job { 10 | 11 | private long documentId; 12 | 13 | public FetchDocumentThumbnailJob(long documentId) { 14 | this.documentId = documentId; 15 | } 16 | 17 | @Override 18 | public void doJob() { 19 | Document document = Document.find("id is ?", this.documentId).first(); 20 | if (document != null) { 21 | try { 22 | document.fetchThumbnailFromGoogleDriveAndSave(); 23 | } catch (IOException ex) { 24 | Logger.error(ex.getMessage()); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /conf/dependencies.yml: -------------------------------------------------------------------------------- 1 | # Application dependencies 2 | 3 | require: 4 | - play 1.2.5 5 | - play -> crud 6 | - play -> deadbolt 1.5.1 7 | - play -> press 1.0.36 8 | - com.fasterxml.jackson.core -> jackson-core 2.0.0 9 | - com.fasterxml.jackson.core -> jackson-annotations 2.0.0 10 | - com.fasterxml.jackson.core -> jackson-databind 2.0.0 11 | - com.google.api-client -> google-api-client 1.10.3-beta 12 | - com.google.apis -> google-api-services-drive v2-rev1-1.7.2-beta 13 | 14 | 15 | repositories: 16 | - google: 17 | type: http 18 | artifact: "http://mavenrepo.google-api-java-client.googlecode.com/hg/com/google/apis/google-api-services-drive/v2-rev1-1.7.2-beta/google-api-services-drive-v2-rev1-1.7.2-beta.jar" 19 | contains: 20 | - com.google.apis -> * -------------------------------------------------------------------------------- /app/utils/Labels.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | public class Labels { 4 | public static final String ACCESS_TOKEN = "access_token"; 5 | public static final String ACCESS_TYPE = "access_type"; 6 | public static final String AUTHORIZATION_CODE = "authorization_code"; 7 | public static final String CLIENT_ID = "client_id"; 8 | public static final String CLIENT_SECRET = "client_secret"; 9 | public static final String CODE = "code"; 10 | public static final String GRANT_TYPE = "grant_type"; 11 | public static final String OFFLINE = "offline"; 12 | public static final String REDIRECT_URI = "redirect_uri"; 13 | public static final String REFRESH_TOKEN = "refresh_token"; 14 | public static final String RESPONSE_TYPE = "response_type"; 15 | public static final String SCOPE = "scope"; 16 | } 17 | -------------------------------------------------------------------------------- /app/controllers/api/Categories.java: -------------------------------------------------------------------------------- 1 | package controllers.api; 2 | 3 | import java.util.List; 4 | 5 | import models.Category; 6 | import models.Document; 7 | import play.mvc.With; 8 | import controllers.AppController; 9 | 10 | @With(ApiController.class) 11 | public class Categories extends AppController { 12 | 13 | public static void list() { 14 | List categories = Category.all().fetch(); 15 | renderJSON(categories); 16 | } 17 | 18 | public static void listDocuments(long id, int page) { 19 | Category category = Category.findById(id); 20 | if (category != null) { 21 | List documents = Document.findByCategory(category, null, page); 22 | renderJSON(documents); 23 | } else { 24 | notFound(); 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/views/tags/document/preview.html: -------------------------------------------------------------------------------- 1 |
2 | Preview 3 |
4 | #{set 'modalDialogs'} 5 | #{if get('modalDialogs')} 6 | #{get 'modalDialogs' /} 7 | #{/if} 8 |
9 | #{document.title document : _document /} 10 | #{document.description document : _document /} 11 |
12 | 13 | ${_document.title} 14 | 15 |
16 | × 17 |
18 | #{/set} -------------------------------------------------------------------------------- /app/views/web/Documents/ajax_list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | #{if documents} 4 | #{document.list documents : documents /} 5 |
6 |
7 |

8 | 9 | &{'_.showMore'} 10 | 11 |

12 |
13 |
14 | #{/if} 15 | #{else} 16 |
17 |

18 | &{'document.list.end'} 19 |

20 |
21 | #{/else} 22 |
23 |
-------------------------------------------------------------------------------- /app/views/web/Discussions/ajax_list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | #{if discussions} 4 | #{discussion.list discussions : discussions /} 5 |
6 |
7 |

8 | 9 | &{'_.showMore'} 10 | 11 |

12 |
13 |
14 | #{/if} 15 | #{else} 16 |
17 |

18 | &{'document.list.end'} 19 |

20 |
21 | #{/else} 22 |
23 |
-------------------------------------------------------------------------------- /app/views/tags/block/top-bar-user.html: -------------------------------------------------------------------------------- 1 |
    2 |
  • 3 | #{if _user} 4 |
  • 5 | &{'_.account'} 6 | 17 |
  • 18 | #{/if} 19 | #{else} 20 |
  • 21 | #{a @web.Auth.googleCode(request.url), id : 'menu-login-link'} 22 | 23 | &{'auth.login'} 24 | #{/a} 25 |
  • 26 | #{/else} 27 |
-------------------------------------------------------------------------------- /db/evolutions/8.sql: -------------------------------------------------------------------------------- 1 | # --- !Ups 2 | 3 | ALTER TABLE "document" 4 | RENAME clonecount TO copycount; 5 | 6 | ALTER TABLE "document" 7 | RENAME readcount TO viewcount; 8 | 9 | ALTER TABLE "document" 10 | ADD COLUMN likecount integer DEFAULT 0 NOT NULL; 11 | 12 | CREATE TABLE "like" ( 13 | id bigint NOT NULL, 14 | created timestamp without time zone, 15 | updated timestamp without time zone, 16 | user_id bigint, 17 | objectid bigint, 18 | objectype character varying(255), 19 | CONSTRAINT like_pkey PRIMARY KEY (id) 20 | ) WITH ( 21 | OIDS=FALSE 22 | ); 23 | 24 | ALTER TABLE "like" 25 | ADD CONSTRAINT fklikeuser FOREIGN KEY (user_id) 26 | REFERENCES user_ (id) MATCH SIMPLE 27 | ON UPDATE NO ACTION ON DELETE CASCADE; 28 | 29 | # --- !Downs 30 | ALTER TABLE "like" 31 | DROP CONSTRAINT fklikeuser; 32 | 33 | DROP TABLE "like"; 34 | 35 | ALTER TABLE "document" 36 | DROP COLUMN likecount; 37 | 38 | ALTER TABLE "document" 39 | RENAME copycount TO clonecount; 40 | 41 | ALTER TABLE "document" 42 | RENAME viewcount TO readcount; -------------------------------------------------------------------------------- /app/models/ExportLink.java: -------------------------------------------------------------------------------- 1 | 2 | package models; 3 | 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import java.util.Date; 6 | import javax.persistence.Entity; 7 | import javax.persistence.ManyToOne; 8 | 9 | @Entity 10 | public class ExportLink extends BaseModel { 11 | 12 | @ManyToOne 13 | public Document document; 14 | 15 | @JsonProperty 16 | public String mimeType; 17 | 18 | @JsonProperty 19 | public String link; 20 | 21 | /** 22 | * 23 | * Constructor used during document upload to Google Drive. 24 | * We explicitely set the created and updated properties here because : 25 | * BaseModel.onCreate method does not get executed during batch insertions. 26 | * 27 | * @param document 28 | * @param mimeType 29 | * @param link 30 | * 31 | */ 32 | public ExportLink(Document document, String mimeType, String link) { 33 | this.document = document; 34 | this.mimeType = mimeType; 35 | this.link = link; 36 | this.created = this.updated = new Date(); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /app/views/tags/vote/button.html: -------------------------------------------------------------------------------- 1 | #{if _me} 2 | #{if _me.getVoteForObject(_objectType, _objectId)} 3 | 4 | (${_voteCount > 1 ? _voteCount - 1 + ' + ' + messages.get('_.you') : messages.get('_.you')}) 5 | 6 | #{/if} 7 | #{else} 8 | 9 | (${_voteCount}) 10 | 11 | #{/else} 12 | #{/if} 13 | #{else} 14 | 17 | #{/else} -------------------------------------------------------------------------------- /app/services/googleoauth/GoogleOAuthTokens.java: -------------------------------------------------------------------------------- 1 | package services.googleoauth; 2 | 3 | import utils.Labels; 4 | 5 | import com.google.gson.annotations.SerializedName; 6 | 7 | public class GoogleOAuthTokens { 8 | 9 | @SerializedName(Labels.ACCESS_TOKEN) 10 | private String accessToken; 11 | 12 | @SerializedName(Labels.REFRESH_TOKEN) 13 | private String refreshToken; 14 | 15 | public GoogleOAuthTokens(String accessToken) { 16 | this.accessToken = accessToken; 17 | } 18 | 19 | public GoogleOAuthTokens(String accessToken, String refreshToken) { 20 | this.accessToken = accessToken; 21 | this.refreshToken = refreshToken; 22 | } 23 | 24 | public String getAccessToken() { 25 | return this.accessToken; 26 | } 27 | 28 | public String getRefreshToken() { 29 | return this.refreshToken; 30 | } 31 | 32 | public void setAccessToken(String accessToken) { 33 | this.accessToken = accessToken; 34 | } 35 | 36 | public void setRefreshToken(String refreshToken) { 37 | this.refreshToken = refreshToken; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /app/services/googleoauth/GoogleOAuthCredentialListener.java: -------------------------------------------------------------------------------- 1 | package services.googleoauth; 2 | 3 | import models.User; 4 | import play.Logger; 5 | 6 | import com.google.api.client.auth.oauth2.Credential; 7 | import com.google.api.client.auth.oauth2.CredentialRefreshListener; 8 | import com.google.api.client.auth.oauth2.TokenErrorResponse; 9 | import com.google.api.client.auth.oauth2.TokenResponse; 10 | 11 | public class GoogleOAuthCredentialListener implements CredentialRefreshListener { 12 | 13 | private User me; 14 | 15 | public GoogleOAuthCredentialListener(User me) { 16 | super(); 17 | this.me = me; 18 | } 19 | 20 | @Override 21 | public void onTokenErrorResponse(Credential credential, TokenErrorResponse tokenResponse) { 22 | Logger.error("Failed to refresh token" + "\n User ID : %s" + "\n User Refresh Token : %s", this.me.id, this.me.googleOAuthRefreshToken); 23 | } 24 | 25 | @Override 26 | public void onTokenResponse(Credential credential, TokenResponse tokenResponse) { 27 | this.me.googleOAuthAccessToken = tokenResponse.getAccessToken(); 28 | this.me.save(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/services/googleoauth/GoogleUserInfo.java: -------------------------------------------------------------------------------- 1 | package services.googleoauth; 2 | 3 | public class GoogleUserInfo { 4 | private String email; 5 | private String id; 6 | private String name; 7 | private String picture; 8 | 9 | public GoogleUserInfo(String id, String name, String email, String picture) { 10 | this.id = id; 11 | this.name = name; 12 | this.email = email; 13 | this.picture = picture; 14 | } 15 | 16 | public String getEmail() { 17 | return this.email; 18 | } 19 | 20 | public String getId() { 21 | return this.id; 22 | } 23 | 24 | public String getName() { 25 | return this.name; 26 | } 27 | 28 | public String getPicture() { 29 | return this.picture; 30 | } 31 | 32 | public void setEmail(String email) { 33 | this.email = email; 34 | } 35 | 36 | public void setId(String id) { 37 | this.id = id; 38 | } 39 | 40 | public void setName(String name) { 41 | this.name = name; 42 | } 43 | 44 | public void setPicture(String picture) { 45 | this.picture = picture; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /app/views/tags/comment/list-item.html: -------------------------------------------------------------------------------- 1 |
2 | #{user.picture user : _comment.author, size : 60, class : 'comment-list-item-user-picture pull-left' /} 3 |
4 |
5 | 6 | #{block.time time : _comment.created /} 7 |
8 |
9 | #{user.name user : _comment.author /} 10 |
11 |

12 | ${_comment.content} 13 |

14 |
15 | #{vote.button class : 'label secondary radius', me : _me, objectType : 'comment', objectId : _comment.id, voteCount : _comment.voteCount /} 16 | #{if _me?.id == _comment.author.id} 17 | #{a @api.Comments.delete(_comment.id), class : 'label radius alert comment-delete right'} 18 | &{'_.delete'} 19 | #{/a} 20 | #{/if} 21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /app/views/CRUD/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | #{get 'title' /} 6 | 7 | 8 | 9 | 10 | 11 |
12 |

&{'crud.title'}

13 |
14 | 15 | #{if request.actionMethod != 'index'} 16 |
17 | #{crud.navigation /} 18 |
19 | #{/if} 20 | 21 | #{if flash.success} 22 |
23 | ${flash.success} 24 |
25 | #{/if} 26 | #{if flash.error || error} 27 |
28 | ${error ?: flash.error} 29 |
30 | #{/if} 31 | 32 |
33 | #{doLayout /} 34 |
35 | 36 |
37 | Generated by the Play CRUD module. Learn how to customize it ! 38 |
39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/views/tags/discussion/list-item.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | #{block.time time : _discussion.created /} 6 |
7 | #{discussion.title discussion : _discussion /} 8 |
9 | ${_discussion.content?.length() > 200 ? _discussion.content.substring(0, 200) + '...' : _discussion.content} 10 |   11 |
12 | #{if _discussion.tagsAsList} 13 |
14 | 15 | #{list _discussion.tagsAsList} 16 | ${_} 17 | #{/list} 18 |
19 | #{/if} 20 |
21 | #{user.picture user : _discussion.user, showName : true, size : 15 /} 22 |
23 |
24 |
25 | #{discussion.stats discussion : _discussion /} 26 |
27 |
28 |
-------------------------------------------------------------------------------- /app/models/DiscussionDocument.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import java.util.List; 4 | 5 | import javax.persistence.Entity; 6 | import javax.persistence.ManyToOne; 7 | 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | 10 | import play.data.validation.Required; 11 | 12 | @Entity 13 | public class DiscussionDocument extends BaseModel { 14 | 15 | @ManyToOne 16 | @Required 17 | @JsonProperty 18 | public Discussion discussion; 19 | 20 | @ManyToOne 21 | @Required 22 | @JsonProperty 23 | public Document document; 24 | 25 | @ManyToOne 26 | @Required 27 | @JsonProperty 28 | public User user; 29 | 30 | public DiscussionDocument(Discussion discussion, Document document, User user) { 31 | this.discussion = discussion; 32 | this.document = document; 33 | this.user = user; 34 | } 35 | 36 | public static List findByDiscussion(long discussionId) { 37 | return DiscussionDocument.find("discussion.id is ? order by created asc", discussionId).fetch(); 38 | } 39 | 40 | public static List findByDocument(long documentId) { 41 | return DiscussionDocument.find("document.id is ? order by created asc", documentId).fetch(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /app/views/tags/discussion/stats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 15 | 21 | 27 | 28 |
4 |
5 | 6 |
${_discussion.voteCount}
7 |
8 |
10 |
11 | 12 |
${_discussion.viewCount}
13 |
14 |
16 |
17 | 18 |
${_discussion.commentCount}
19 |
20 |
22 |
23 | 24 |
${_discussion.documentCount}
25 |
26 |
-------------------------------------------------------------------------------- /app/views/tags/document/list-item.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | #{block.time time : _document.created /} 6 |
7 | #{document.title document : _document /} 8 |
9 | #{document.preview document : _document /} 10 |
11 |
12 | ${_document.description?.length() > 200 ? _document.description.substring(0, 200) + '...' : _document.description} 13 |   14 |
15 | #{if _document.tagsAsList} 16 |
17 | 18 | #{list _document.tagsAsList} 19 | ${_} 20 | #{/list} 21 |
22 | #{/if} 23 |
24 | #{user.picture user : _document.owner, showName : true, size : 15 /} 25 |
26 |
27 |
28 | #{document.stats document : _document /} 29 |
30 |
31 |
-------------------------------------------------------------------------------- /app/views/admin/Categories/show.html: -------------------------------------------------------------------------------- 1 | #{extends 'CRUD/layout.html' /} 2 | #{set title:messages.get('crud.show.title', type.modelName) /} 3 | 4 |
5 | 6 |

&{'crud.show.title', type.modelName}

7 | 8 |
9 | #{form action:@save(object._key()), enctype:'multipart/form-data'} 10 | #{crud.form} 11 | #{crud.custom 'objectType'} 12 | 13 | ${object.objectType} 14 | #{/crud.custom} 15 | #{crud.custom 'created'} 16 | 17 | ${object.created?.since()} 18 | #{/crud.custom} 19 | #{crud.custom 'updated'} 20 | 21 | ${object.updated?.since()} 22 | #{/crud.custom} 23 | #{/crud.form} 24 |

25 | 26 | 27 |

28 | #{/form} 29 |
30 | 31 | #{form @delete(object._key())} 32 |

33 | 34 |

35 | #{/form} 36 | 37 |
-------------------------------------------------------------------------------- /app/controllers/api/Comments.java: -------------------------------------------------------------------------------- 1 | package controllers.api; 2 | 3 | import jobs.UpdateDocumentCommentCountJob; 4 | import models.Comment; 5 | import models.User; 6 | import play.data.validation.Required; 7 | import play.mvc.With; 8 | import controllers.AppController; 9 | 10 | @With(ApiController.class) 11 | public class Comments extends AppController { 12 | 13 | 14 | public static void create(@Required Comment comment) { 15 | checkAuthenticity(); 16 | if (!validation.hasErrors()) { 17 | User me = getMe(); 18 | comment.content = comment.content.trim(); 19 | comment.author = me; 20 | // TODO : Need to add validation rule in Comment model that checks that comment.objectType is either "document" or "comment". 21 | if (comment.validateAndSave()) { 22 | comment.updateCountForObject(true); 23 | renderJSON(comment); 24 | } 25 | } 26 | } 27 | 28 | public static void delete(long id) { 29 | checkAuthenticity(); 30 | Comment comment = Comment.findById(id); 31 | notFoundIfNull(comment); 32 | User me = getMe(); 33 | if (comment.author.id.equals(me.id)) { 34 | comment.updateCountForObject(false); 35 | comment.delete(); 36 | renderJSON(true); 37 | } else { 38 | unauthorized(); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /public/javascripts/app.js: -------------------------------------------------------------------------------- 1 | ;(function ($, window, undefined) { 2 | 'use strict'; 3 | 4 | var $doc = $(document), 5 | Modernizr = window.Modernizr; 6 | 7 | 8 | $.fn.foundationAlerts ? $doc.foundationAlerts() : null; 9 | $.fn.foundationAccordion ? $doc.foundationAccordion() : null; 10 | $.fn.foundationTooltips ? $doc.foundationTooltips() : null; 11 | $('input, textarea').placeholder(); 12 | 13 | 14 | $.fn.foundationButtons ? $doc.foundationButtons() : null; 15 | 16 | 17 | $.fn.foundationNavigation ? $doc.foundationNavigation() : null; 18 | 19 | 20 | $.fn.foundationCustomForms ? $doc.foundationCustomForms() : null; 21 | $.fn.foundationMediaQueryViewer ? $doc.foundationMediaQueryViewer() : null; 22 | 23 | 24 | $.fn.foundationTabs ? $doc.foundationTabs() : null; 25 | 26 | 27 | 28 | 29 | // UNCOMMENT THE LINE YOU WANT BELOW IF YOU WANT IE8 SUPPORT AND ARE USING .block-grids 30 | // $('.block-grid.two-up>li:nth-child(2n+1)').css({clear: 'both'}); 31 | // $('.block-grid.three-up>li:nth-child(3n+1)').css({clear: 'both'}); 32 | // $('.block-grid.four-up>li:nth-child(4n+1)').css({clear: 'both'}); 33 | // $('.block-grid.five-up>li:nth-child(5n+1)').css({clear: 'both'}); 34 | 35 | // Hide address bar on mobile devices 36 | if (Modernizr.touch) { 37 | $(window).load(function () { 38 | setTimeout(function () { 39 | window.scrollTo(0, 1); 40 | }, 0); 41 | }); 42 | } 43 | 44 | })(jQuery, this); 45 | -------------------------------------------------------------------------------- /app/models/dialects/FullTextSearchPostgreSQLDialect.java: -------------------------------------------------------------------------------- 1 | package models.dialects; 2 | 3 | 4 | import java.util.List; 5 | 6 | import org.hibernate.QueryException; 7 | import org.hibernate.dialect.PostgreSQLDialect; 8 | import org.hibernate.dialect.function.SQLFunction; 9 | import org.hibernate.engine.Mapping; 10 | import org.hibernate.engine.SessionFactoryImplementor; 11 | import org.hibernate.type.BooleanType; 12 | import org.hibernate.type.Type; 13 | 14 | public class FullTextSearchPostgreSQLDialect extends PostgreSQLDialect { 15 | 16 | public FullTextSearchPostgreSQLDialect() { 17 | registerFunction("fts", new PostgreSQLFullTextSearchFunction()); 18 | } 19 | 20 | public class PostgreSQLFullTextSearchFunction implements SQLFunction { 21 | 22 | @Override 23 | @SuppressWarnings("unchecked") 24 | public String render(Type type, List args, 25 | SessionFactoryImplementor factory) throws QueryException { 26 | if (args.size() != 1) { 27 | throw new IllegalArgumentException("The function must be passed 1 argument"); 28 | } 29 | String values = (String) args.get(0); 30 | return "searchable_text @@ plainto_tsquery('pg_catalog.english', " + values + ")"; 31 | } 32 | 33 | @Override 34 | public Type getReturnType(Type columnType, Mapping mapping) throws QueryException { 35 | return new BooleanType(); 36 | } 37 | 38 | @Override 39 | public boolean hasArguments() { 40 | return true; 41 | } 42 | 43 | @Override 44 | public boolean hasParenthesesIfNoArguments() { 45 | return false; 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /db/evolutions/6.sql: -------------------------------------------------------------------------------- 1 | # --- !Ups 2 | ALTER TABLE documentjobstatus ADD COLUMN result character varying(5000); 3 | ALTER TABLE documentjobstatus DROP COLUMN resultdocumentid; 4 | ALTER TABLE "document" DROP COLUMN originaldocument_id; 5 | ALTER TABLE "document" DROP COLUMN isarchived; 6 | ALTER TABLE "comment" DROP COLUMN isdeleted; 7 | ALTER TABLE "comment" DROP COLUMN iscensored; 8 | ALTER TABLE "comment" DROP COLUMN parent_id; 9 | ALTER TABLE "comment" DROP COLUMN repliescount; 10 | 11 | ALTER TABLE "comment" DROP CONSTRAINT fk9bde863fed0b1afe; 12 | ALTER TABLE "comment" 13 | ADD CONSTRAINT fk9bde863fed0b1afe FOREIGN KEY (document_id) 14 | REFERENCES "document" (id) MATCH SIMPLE 15 | ON UPDATE NO ACTION ON DELETE CASCADE; 16 | 17 | ALTER TABLE exportlink DROP CONSTRAINT fk834619eeed0b1afe; 18 | ALTER TABLE exportlink ADD CONSTRAINT fk834619eeed0b1afe FOREIGN KEY (document_id) 19 | REFERENCES "document" (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE; 20 | 21 | # --- !Downs 22 | ALTER TABLE documentjobstatus DROP COLUMN result; 23 | ALTER TABLE documentjobstatus ADD COLUMN resultdocumentid bigint; 24 | ALTER TABLE "comment" ADD COLUMN isdeleted boolean; 25 | ALTER TABLE "comment" ALTER COLUMN isdeleted SET NOT NULL; 26 | ALTER TABLE "comment" ADD COLUMN iscensored boolean; 27 | ALTER TABLE "comment" ALTER COLUMN iscensored SET NOT NULL; 28 | ALTER TABLE "comment" ADD COLUMN repliescount bigint; 29 | ALTER TABLE "comment" ALTER COLUMN repliescount SET NOT NULL; 30 | ALTER TABLE "comment" ADD COLUMN parent_id bigint; -------------------------------------------------------------------------------- /app/views/admin/Categories/blank.html: -------------------------------------------------------------------------------- 1 | #{extends 'CRUD/layout.html' /} 2 | #{set title:messages.get('crud.blank.title', type.modelName) /} 3 | 4 |
5 | 6 |

&{'crud.blank.title', type.modelName}

7 | 8 |
9 | #{form action:@create(), enctype:'multipart/form-data'} 10 | #{crud.form} 11 | #{crud.custom 'objectType'} 12 | 13 | 18 | #{/crud.custom} 19 | #{crud.custom 'created' } 20 | 21 |

Automatically set

22 | #{/crud.custom} 23 | #{crud.custom 'updated' } 24 | 25 |

Automatically set

26 | #{/crud.custom} 27 | #{/crud.form} 28 |

29 | 30 | 31 | 32 |

33 | #{/form} 34 |
35 | 36 |
-------------------------------------------------------------------------------- /app/views/admin/Documents/show.html: -------------------------------------------------------------------------------- 1 | #{extends 'CRUD/layout.html' /} 2 | #{set title:messages.get('crud.show.title', type.modelName) /} 3 | 4 |
5 | 6 |

&{'crud.show.title', type.modelName}

7 | 8 |
9 | #{form action:@save(object._key()), enctype:'multipart/form-data'} 10 |
11 | 12 | ${object.owner} - See profile 13 |
14 |
15 | 16 | View document 17 |
18 | #{crud.form} 19 | #{crud.custom 'created'} 20 | 21 | ${object.created?.since()} 22 | #{/crud.custom} 23 | #{crud.custom 'updated'} 24 | 25 | ${object.updated?.since()} 26 | #{/crud.custom} 27 | #{/crud.form} 28 |

29 | 30 | 31 |

32 | #{/form} 33 |
34 | 35 | #{form @delete(object._key())} 36 |

37 | 38 |

39 | #{/form} 40 | 41 |
-------------------------------------------------------------------------------- /app/views/admin/Discussions/show.html: -------------------------------------------------------------------------------- 1 | #{extends 'CRUD/layout.html' /} 2 | #{set title:messages.get('crud.show.title', type.modelName) /} 3 | 4 |
5 | 6 |

&{'crud.show.title', type.modelName}

7 | 8 |
9 | #{form action:@save(object._key()), enctype:'multipart/form-data'} 10 |
11 | 12 | ${object.user} - See profile 13 |
14 |
15 | 16 | View discussion 17 |
18 | #{crud.form} 19 | #{crud.custom 'created'} 20 | 21 | ${object.created?.since()} 22 | #{/crud.custom} 23 | #{crud.custom 'updated'} 24 | 25 | ${object.updated?.since()} 26 | #{/crud.custom} 27 | #{/crud.form} 28 |

29 | 30 | 31 |

32 | #{/form} 33 |
34 | 35 | #{form @delete(object._key())} 36 |

37 | 38 |

39 | #{/form} 40 | 41 |
-------------------------------------------------------------------------------- /app/views/admin/Comments/show.html: -------------------------------------------------------------------------------- 1 | #{extends 'CRUD/layout.html' /} 2 | #{set title:messages.get('crud.show.title', type.modelName) /} 3 | 4 |
5 | 6 |

&{'crud.show.title', type.modelName}

7 | 8 |
9 | #{form action:@save(object._key()), enctype:'multipart/form-data'} 10 |
11 | 12 | ${object.author} - See profile 13 |
14 | 18 | #{crud.form} 19 | #{crud.custom 'created'} 20 | 21 | ${object.created?.since()} 22 | #{/crud.custom} 23 | #{crud.custom 'updated'} 24 | 25 | ${object.updated?.since()} 26 | #{/crud.custom} 27 | #{/crud.form} 28 |

29 | 30 | 31 |

32 | #{/form} 33 |
34 | 35 | #{form @delete(object._key())} 36 |

37 | 38 |

39 | #{/form} 40 | 41 |
-------------------------------------------------------------------------------- /app/views/web/Comments/list.html: -------------------------------------------------------------------------------- 1 |
2 | #{if comments} 3 | #{list comments} 4 | #{comment.list-item comment : _, me : me /} 5 | #{/list} 6 | #{/if} 7 | #{else} 8 |

9 | &{'comment.list.empty'} 10 |

11 | #{/else} 12 |
13 | #{if me} 14 |
15 |
16 | #{user.picture user : me, size : 60 /} 17 |
18 |
19 | #{form @api.Comments.create(), class : 'comment-new-form', id : 'new-comment'} 20 |
21 |
22 | 23 | 24 |
25 | 26 |
27 |
28 | 29 |
30 | #{/form} 31 |
32 |
33 | #{/if} 34 | #{else} 35 |

36 | #{if objectType == 'document'} 37 | &{'comment.auth.login.needed'} 38 | #{/if} 39 | #{elseif objectType == 'discussion'} 40 | &{'comment.auth.login.needed'} 41 | #{/elseif} 42 |

43 | #{/else} 44 | -------------------------------------------------------------------------------- /app/controllers/api/Votes.java: -------------------------------------------------------------------------------- 1 | package controllers.api; 2 | 3 | import controllers.web.Auth; 4 | import models.Comment; 5 | import models.Document; 6 | import models.Vote; 7 | import models.User; 8 | import play.data.validation.Required; 9 | 10 | public class Votes extends ApiController { 11 | 12 | public static void create(@Required Vote vote) { 13 | checkAuthenticity(); 14 | if (!validation.hasErrors()) { 15 | User me = Auth.getMe(); 16 | Vote check = me.getVoteForObject(vote.objectType, vote.objectId); 17 | if (check == null) { 18 | vote.user = me; 19 | if (vote.validateAndSave()) { 20 | vote.updateCountForObject(true); 21 | renderJSON(vote); 22 | } 23 | } else { 24 | renderJSON(check); 25 | } 26 | } 27 | } 28 | 29 | public static void delete(long id) { 30 | checkAuthenticity(); 31 | User me = Auth.getMe(); 32 | Vote vote = Vote.findById(id); 33 | notFoundIfNull(vote); 34 | if (vote.user.id == me.id) { 35 | vote.updateCountForObject(false); 36 | Vote _vote = vote; 37 | vote.delete(); 38 | renderJSON(_vote); 39 | } else { 40 | unauthorized(); 41 | } 42 | } 43 | 44 | public static void read(@Required String objectType, @Required long objectId) { 45 | if (!validation.hasErrors()) { 46 | User me = Auth.getMe(); 47 | Vote vote = me.getVoteForObject(objectType, objectId); 48 | if (vote != null) { 49 | renderJSON(vote); 50 | } else { 51 | notFound(); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/controllers/api/ApiController.java: -------------------------------------------------------------------------------- 1 | package controllers.api; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import play.data.validation.Error; 7 | import play.i18n.Messages; 8 | import play.mvc.After; 9 | import play.mvc.Before; 10 | import controllers.AppController; 11 | import controllers.web.Auth; 12 | 13 | public class ApiController extends AppController { 14 | 15 | @Before(unless = { 16 | "api.Categories.list", 17 | "api.Categories.listDocuments", 18 | "api.Discussions.list", 19 | "api.Discussions.markAsViewed", 20 | "api.Documents.read", 21 | "api.Documents.readThumbnail", 22 | "api.Documents.markAsViewed", 23 | "api.Documents.download", 24 | "api.Documents.list" 25 | }) 26 | public static void checkAccess() { 27 | if (getMe() == null) { 28 | unauthorized(); 29 | } 30 | } 31 | 32 | @Before 33 | public static void setDefaultRequestParameters() { 34 | String pageParam = request.params.get("page"); 35 | if (pageParam != null) { 36 | int page = Integer.parseInt(pageParam); 37 | if (page < 1) { 38 | page = 1; 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * Translates and sends error validation messages as a JSON array if any. 45 | */ 46 | @After 47 | public static void renderValidationErrors() { 48 | if (validation.hasErrors()) { 49 | response.status = 400; 50 | Map errorsMap = new HashMap(); 51 | for (Error error : validation.errors()) { 52 | errorsMap.put(error.getKey(), Messages.get(error.message())); 53 | } 54 | renderJSON(errorsMap); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/controllers/api/DiscussionDocuments.java: -------------------------------------------------------------------------------- 1 | package controllers.api; 2 | 3 | import models.Discussion; 4 | import models.DiscussionDocument; 5 | import models.Document; 6 | import models.User; 7 | import models.Vote; 8 | import play.data.validation.Required; 9 | import play.mvc.With; 10 | import controllers.AppController; 11 | import controllers.web.Auth; 12 | 13 | @With(ApiController.class) 14 | public class DiscussionDocuments extends AppController { 15 | 16 | public static void create(DiscussionDocument discussionDocument) { 17 | checkAuthenticity(); 18 | notFoundIfNull(discussionDocument); 19 | User me = getMe(); 20 | discussionDocument.user = me; 21 | DiscussionDocument check = DiscussionDocument.find("discussion.id is ? and document.id is ?", discussionDocument.discussion.id, discussionDocument.document.id).first(); 22 | if (check != null) { 23 | renderJSON(check); 24 | } else if (discussionDocument.validateAndSave()) { 25 | discussionDocument.discussion.updateDocumentCountAndSave(true); 26 | discussionDocument.document.updateDiscussionCountAndSave(true); 27 | renderJSON(discussionDocument); 28 | } 29 | } 30 | 31 | public static void delete(long id) { 32 | checkAuthenticity(); 33 | User me = Auth.getMe(); 34 | DiscussionDocument discussionDocument = DiscussionDocument.findById(id); 35 | notFoundIfNull(discussionDocument); 36 | if (discussionDocument.user.id == me.id) { 37 | discussionDocument.delete(); 38 | discussionDocument.discussion.updateDocumentCountAndSave(false); 39 | discussionDocument.document.updateDiscussionCountAndSave(false); 40 | renderJSON(true); 41 | } else { 42 | unauthorized(); 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/controllers/RolesHandler.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import models.deadbolt.RoleHolder; 4 | import play.i18n.Messages; 5 | import play.mvc.Controller; 6 | import controllers.deadbolt.DeadboltHandler; 7 | import controllers.deadbolt.ExternalizedRestrictionsAccessor; 8 | import controllers.deadbolt.RestrictedResourcesHandler; 9 | import controllers.web.Auth; 10 | 11 | public class RolesHandler extends Controller implements DeadboltHandler { 12 | 13 | /** 14 | * Executed before each action that is restricted. Redirects to login if no 15 | * user session is present. 16 | */ 17 | @Override 18 | public void beforeRoleCheck() { 19 | AppController.setLanguage(); 20 | if (request.action.contains("admin")) { 21 | if (Auth.getMe() == null) { 22 | flash.error(Messages.get("auth.unauthorizedAccess")); 23 | redirect("/"); 24 | } 25 | } 26 | } 27 | 28 | @Override 29 | public ExternalizedRestrictionsAccessor getExternalizedRestrictionsAccessor() { 30 | return null; 31 | } 32 | 33 | @Override 34 | public RestrictedResourcesHandler getRestrictedResourcesHandler() { 35 | return null; 36 | } 37 | 38 | /** 39 | * Gets the current user in the session; 40 | * 41 | * @return Admin record. 42 | */ 43 | @Override 44 | public RoleHolder getRoleHolder() { 45 | return Auth.getMe(); 46 | } 47 | 48 | /** 49 | * Executed when a user does not have access to a page. 50 | * 51 | * @param controllerClassName 52 | * the name of the controller access was denied to 53 | */ 54 | @Override 55 | public void onAccessFailure(String controllerClassName) { 56 | flash.error(Messages.get("auth.unauthorizedAccess")); 57 | redirect("/"); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /app/jobs/CopyDocumentJob.java: -------------------------------------------------------------------------------- 1 | package jobs; 2 | 3 | import java.io.IOException; 4 | 5 | import com.google.api.services.drive.model.File; 6 | 7 | import models.BackgroundJobStatus; 8 | import models.DocumentJobStatus; 9 | import models.Document; 10 | import models.User; 11 | import play.Logger; 12 | import play.jobs.Job; 13 | 14 | public class CopyDocumentJob extends Job { 15 | 16 | private long documentJobStatusId; 17 | private long documentId; 18 | private long userId; 19 | 20 | public CopyDocumentJob(long documentJobStatusId, long documentId, long userId) { 21 | this.documentJobStatusId = documentJobStatusId; 22 | this.documentId = documentId; 23 | this.userId = userId; 24 | } 25 | 26 | @Override 27 | public void doJob() { 28 | DocumentJobStatus documentJobStatus = DocumentJobStatus.findById(this.documentJobStatusId); 29 | Document document = Document.findById(this.documentId); 30 | User user = User.findById(this.userId); 31 | if ((documentJobStatus != null) && (document != null) && (user != null)) { 32 | try { 33 | Logger.info("Start : clone document job"); 34 | File copiedFile = document.copyForUser(user); 35 | if (copiedFile != null) { 36 | documentJobStatus.result = copiedFile.getAlternateLink(); 37 | documentJobStatus.status = BackgroundJobStatus.Status.SUCCESS; 38 | documentJobStatus.save(); 39 | } 40 | } catch (IOException ex) { 41 | Logger.error("Error during document copy operation to Google Drive : %s", ex.getMessage()); 42 | documentJobStatus.status = BackgroundJobStatus.Status.FAIL; 43 | documentJobStatus.save(); 44 | } finally { 45 | Logger.info("End : clone document job"); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/views/tags/document/stats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 15 | 21 | 27 | 33 | 39 | 40 |
4 |
5 | 6 |
${_document.voteCount}
7 |
8 |
10 |
11 | 12 |
${_document.copyCount}
13 |
14 |
16 |
17 | 18 |
${_document.downloadCount}
19 |
20 |
22 |
23 | 24 |
${_document.viewCount}
25 |
26 |
28 |
29 | 30 |
${_document.commentCount}
31 |
32 |
34 |
35 | 36 |
${_document.discussionCount}
37 |
38 |
-------------------------------------------------------------------------------- /app/views/tags/block/menu.html: -------------------------------------------------------------------------------- 1 | 51 | -------------------------------------------------------------------------------- /app/views/web/Users/read.html: -------------------------------------------------------------------------------- 1 | #{extends 'web.html' /} #{set title : user.name /} 2 |
3 |
4 |
#{user.picture user : user, size : 300 /}
5 |

About

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 24 |
Joined${user.created}
Links 14 | 18 |
Karma${user.karma}
25 |
26 |
27 |
28 |
&{'_.recentDocuments'}
29 |
Recent Discussions
30 |
31 |
    32 |
  • 33 | #{if documents} #{document.list documents : documents /} #{/if} 34 | #{else} 35 |
    &{'document.list.end'}
    36 | #{/else} 37 |
  • 38 |
  • 39 | #{if discussions} 40 | #{discussion.list discussions : discussions /} 41 | #{/if} 42 | #{else} 43 |
    &{'document.list.end'}
    44 | #{/else} 45 |
  • 46 |
47 |
48 |
-------------------------------------------------------------------------------- /app/controllers/web/Documents.java: -------------------------------------------------------------------------------- 1 | package controllers.web; 2 | 3 | import java.util.Date; 4 | import java.util.List; 5 | import java.util.concurrent.ExecutionException; 6 | 7 | import models.Category; 8 | import models.Comment; 9 | import models.DiscussionDocument; 10 | import models.Document; 11 | import models.User; 12 | import play.db.jpa.JPA; 13 | import play.i18n.Messages; 14 | import play.mvc.After; 15 | import play.mvc.Before; 16 | import play.mvc.With; 17 | import controllers.AppController; 18 | 19 | @With(WebController.class) 20 | public class Documents extends AppController { 21 | 22 | @Before 23 | public static void addViewArgs() { 24 | List categories = Category.findForDocument(); 25 | renderArgs.put("categories", categories); 26 | } 27 | 28 | public static void add() { 29 | render(); 30 | } 31 | 32 | public static void edit(long id) { 33 | Document document = Document.findById(id); 34 | notFoundIfNull(document); 35 | User me = Auth.getMe(); 36 | if (document.belongsToUser(me)) { 37 | render(document); 38 | } else { 39 | go(id); 40 | } 41 | } 42 | 43 | public static void go(long id) { 44 | Document document = Document.findById(id); 45 | read(document.id, document.slug); 46 | } 47 | 48 | public static void list(String keyword, long categoryId, String order, Integer page) { 49 | Category category = Category.findById(categoryId); 50 | List documents = Document.search(keyword, categoryId, order, page); 51 | if (request.isAjax()) { 52 | renderTemplate("web/Documents/ajax_list.html", documents, category, keyword, order, page); 53 | } else { 54 | render(documents, category, keyword, order, page); 55 | } 56 | } 57 | 58 | public static void read(long id, String slug) { 59 | Document document = Document.findById(id); 60 | notFoundIfNull(document); 61 | List discussions = DiscussionDocument 62 | .findByDocument(document.id); 63 | render(document, discussions); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /app/models/BaseModel.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import java.util.Date; 4 | import java.util.List; 5 | 6 | import javax.persistence.MappedSuperclass; 7 | import javax.persistence.PrePersist; 8 | import javax.persistence.PreUpdate; 9 | import javax.persistence.Temporal; 10 | import javax.persistence.TemporalType; 11 | 12 | import play.db.jpa.Model; 13 | 14 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 15 | import com.fasterxml.jackson.annotation.JsonGetter; 16 | import com.fasterxml.jackson.annotation.JsonIgnore; 17 | import com.fasterxml.jackson.annotation.JsonProperty; 18 | import com.google.common.base.Function; 19 | import com.google.common.collect.Lists; 20 | 21 | @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE) 22 | @MappedSuperclass 23 | public abstract class BaseModel extends Model { 24 | 25 | @Temporal(TemporalType.TIMESTAMP) 26 | @JsonProperty 27 | public Date created; 28 | 29 | // Hack to remove persistent property from JSON output 30 | private transient boolean persistent; 31 | 32 | @Temporal(TemporalType.TIMESTAMP) 33 | @JsonProperty 34 | public Date updated; 35 | 36 | @JsonGetter 37 | @Override 38 | public Long getId() { 39 | return this.id; 40 | } 41 | 42 | @JsonIgnore 43 | public Boolean getPersistent() { 44 | return this.persistent; 45 | } 46 | 47 | @PrePersist 48 | protected void onCreate() { 49 | this.updated = this.created = new Date(); 50 | } 51 | 52 | @PreUpdate 53 | protected void onUpdate() { 54 | this.updated = new Date(); 55 | } 56 | 57 | public final static int DEFAULT_PAGINATE_COUNT = 20; 58 | 59 | public static List getIdsOfModels(List models) { 60 | List ids = Lists.transform(models, new Function() { 61 | @Override 62 | public Long apply(BaseModel model) { 63 | return model.id; 64 | } 65 | }); 66 | return ids; 67 | } 68 | 69 | public static int getStartFromPage(Integer page) { 70 | if ((page == null) || (page < 1)) { 71 | page = 1; 72 | } 73 | return ((page - 1) * DEFAULT_PAGINATE_COUNT); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /public/javascripts/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /*jshint eqnull:true */ 2 | /*! 3 | * jQuery Cookie Plugin v1.2 4 | * https://github.com/carhartl/jquery-cookie 5 | * 6 | * Copyright 2011, Klaus Hartl 7 | * Dual licensed under the MIT or GPL Version 2 licenses. 8 | * http://www.opensource.org/licenses/mit-license.php 9 | * http://www.opensource.org/licenses/GPL-2.0 10 | */ 11 | (function ($, document, undefined) { 12 | 13 | var pluses = /\+/g; 14 | 15 | function raw(s) { 16 | return s; 17 | } 18 | 19 | function decoded(s) { 20 | return decodeURIComponent(s.replace(pluses, ' ')); 21 | } 22 | 23 | $.cookie = function (key, value, options) { 24 | 25 | // key and at least value given, set cookie... 26 | if (value !== undefined && !/Object/.test(Object.prototype.toString.call(value))) { 27 | options = $.extend({}, $.cookie.defaults, options); 28 | 29 | if (value === null) { 30 | options.expires = -1; 31 | } 32 | 33 | if (typeof options.expires === 'number') { 34 | var days = options.expires, t = options.expires = new Date(); 35 | t.setDate(t.getDate() + days); 36 | } 37 | 38 | value = String(value); 39 | 40 | return (document.cookie = [ 41 | encodeURIComponent(key), '=', options.raw ? value : encodeURIComponent(value), 42 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 43 | options.path ? '; path=' + options.path : '', 44 | options.domain ? '; domain=' + options.domain : '', 45 | options.secure ? '; secure' : '' 46 | ].join('')); 47 | } 48 | 49 | // key and possibly options given, get cookie... 50 | options = value || $.cookie.defaults || {}; 51 | var decode = options.raw ? raw : decoded; 52 | var cookies = document.cookie.split('; '); 53 | for (var i = 0, parts; (parts = cookies[i] && cookies[i].split('=')); i++) { 54 | if (decode(parts.shift()) === key) { 55 | return decode(parts.join('=')); 56 | } 57 | } 58 | 59 | return null; 60 | }; 61 | 62 | $.cookie.defaults = {}; 63 | 64 | $.removeCookie = function (key, options) { 65 | if ($.cookie(key, options) !== null) { 66 | $.cookie(key, null, options); 67 | return true; 68 | } 69 | return false; 70 | }; 71 | 72 | })(jQuery, document); -------------------------------------------------------------------------------- /app/models/Category.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import java.util.List; 4 | 5 | import javax.persistence.Entity; 6 | import javax.persistence.PreRemove; 7 | 8 | import play.data.validation.Required; 9 | import play.db.jpa.GenericModel.JPAQuery; 10 | import play.i18n.Messages; 11 | import play.templates.JavaExtensions; 12 | 13 | import com.fasterxml.jackson.annotation.JsonProperty; 14 | 15 | @Entity 16 | public class Category extends BaseModel { 17 | 18 | @Required 19 | @JsonProperty 20 | public String name; 21 | 22 | @Required 23 | @JsonProperty 24 | public String objectType; 25 | 26 | public Category(String name, String objectType) { 27 | this.name = name; 28 | this.objectType = objectType; 29 | } 30 | 31 | @PreRemove 32 | public void beforeDelete() { 33 | if (this.hasObjects(this.objectType)) { 34 | throw new RuntimeException(Messages.get("category.delete.error.documents")); 35 | } 36 | } 37 | 38 | public String getSlug() { 39 | return JavaExtensions.slugify(this.name); 40 | } 41 | 42 | public boolean hasObjects(String objectType) { 43 | boolean hasObjects = false; 44 | if ("document".equals(this.objectType)) { 45 | Document check = Document.find("category is ?", this).first(); 46 | hasObjects = (check != null); 47 | } else if ("discussion".equals(this.objectType)) { 48 | Discussion check = Discussion.find("category is ?", this).first(); 49 | hasObjects = (check != null); 50 | } 51 | return hasObjects; 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return this.name + " (" + this.objectType + ")"; 57 | } 58 | 59 | public static List findForDocument() { 60 | return findForObjectType("document"); 61 | } 62 | 63 | public static List findForDiscussion() { 64 | return findForObjectType("discussion"); 65 | } 66 | 67 | private static List findForObjectType(String objectType) { 68 | if (objectType != null && ("document".equals(objectType) || "discussion".equals(objectType))) { 69 | return Category.find("objectType is ? order by name", objectType).fetch(); 70 | } else { 71 | throw new RuntimeException("Invalid object type."); 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /app/controllers/web/Discussions.java: -------------------------------------------------------------------------------- 1 | package controllers.web; 2 | 3 | import java.util.List; 4 | 5 | import models.Category; 6 | import models.Discussion; 7 | import models.DiscussionDocument; 8 | import models.Document; 9 | import models.User; 10 | import models.Vote; 11 | 12 | import play.mvc.Before; 13 | import play.mvc.With; 14 | 15 | import controllers.AppController; 16 | 17 | @With(WebController.class) 18 | public class Discussions extends AppController { 19 | 20 | @Before 21 | public static void addViewArgs() { 22 | List categories = Category.findForDiscussion(); 23 | renderArgs.put("categories", categories); 24 | } 25 | 26 | public static void add(long documentId) { 27 | if (documentId > 0) { 28 | Document document = Document.findById(documentId); 29 | notFoundIfNull(document); 30 | renderArgs.put("document", document); 31 | } 32 | render(); 33 | } 34 | 35 | public static void edit(long id) { 36 | Discussion discussion = Discussion.findById(id); 37 | notFoundIfNull(discussion); 38 | User me = Auth.getMe(); 39 | if (discussion.wasStartedByUser(me)) { 40 | render(discussion); 41 | } else { 42 | go(id); 43 | } 44 | } 45 | 46 | public static void go(long id) { 47 | Discussion discussion = Discussion.findById(id); 48 | notFoundIfNull(discussion); 49 | view(discussion.id, discussion.slug); 50 | } 51 | 52 | public static void list(String keyword, long categoryId, String order, Integer page) { 53 | Category category = Category.findById(categoryId); 54 | List discussions = Discussion.search(keyword, categoryId, order, page); 55 | if (request.isAjax()) { 56 | renderTemplate("web/Discussions/ajax_list.html", discussions, category, keyword, order, page); 57 | } else { 58 | render(discussions, category, keyword, order, page); 59 | } 60 | } 61 | 62 | public static void view(long id, String slug) { 63 | Discussion discussion = Discussion.findById(id); 64 | notFoundIfNull(discussion); 65 | List documents = DiscussionDocument.findByDiscussion(discussion.id); 66 | render(discussion, documents); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/views/tags/vote/scripts.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/tags/comment/scripts.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/stylesheets/icons.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon'; 3 | src:url('/public/stylesheets/fonts/icomoon.eot'); 4 | src:url('/public/stylesheets/fonts/icomoon.eot?#iefix') format('embedded-opentype'), 5 | url('/public/stylesheets/fonts/icomoon.svg#icomoon') format('svg'), 6 | url('/public/stylesheets/fonts/icomoon.woff') format('woff'), 7 | url('/public/stylesheets/fonts/icomoon.ttf') format('truetype'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | /* Use the following CSS code if you want to use data attributes for inserting your icons */ 13 | [data-icon]:before { 14 | font-family: 'icomoon'; 15 | content: attr(data-icon); 16 | speak: none; 17 | font-weight: normal; 18 | -webkit-font-smoothing: antialiased; 19 | } 20 | 21 | /* Use the following CSS code if you want to have a class per icon */ 22 | [class^="icon-"]:before, [class*=" icon-"]:before { 23 | font-family: 'icomoon'; 24 | font-style: normal; 25 | speak: none; 26 | font-weight: normal; 27 | -webkit-font-smoothing: antialiased; 28 | } 29 | .icon-document-alt-stroke:before { 30 | content: "\21"; 31 | } 32 | .icon-file-excel:before { 33 | content: "\22"; 34 | } 35 | .icon-file-powerpoint:before { 36 | content: "\23"; 37 | } 38 | .icon-file-word:before { 39 | content: "\24"; 40 | } 41 | .icon-paragraph-justify:before { 42 | content: "\25"; 43 | } 44 | .icon-file-pdf:before { 45 | content: "\26"; 46 | } 47 | .icon-file-xml:before { 48 | content: "\27"; 49 | } 50 | .icon-home:before { 51 | content: "\28"; 52 | } 53 | .icon-comments:before { 54 | content: "\29"; 55 | } 56 | .icon-comments-2:before { 57 | content: "\2a"; 58 | } 59 | .icon-copy:before { 60 | content: "\2b"; 61 | } 62 | .icon-upload:before { 63 | content: "\2c"; 64 | } 65 | .icon-user:before { 66 | content: "\2d"; 67 | } 68 | .icon-search:before { 69 | content: "\2e"; 70 | } 71 | .icon-thumbs-up:before { 72 | content: "\2f"; 73 | } 74 | .icon-eye:before { 75 | content: "\30"; 76 | } 77 | .icon-download:before { 78 | content: "\31"; 79 | } 80 | .icon-list:before { 81 | content: "\32"; 82 | } 83 | .icon-new-tab:before { 84 | content: "\33"; 85 | } 86 | .icon-google-plus:before { 87 | content: "\34"; 88 | } 89 | .icon-google-drive:before { 90 | content: "\35"; 91 | } 92 | .icon-pencil:before { 93 | content: "\36"; 94 | } 95 | .icon-folder:before { 96 | content: "\37"; 97 | } 98 | .icon-tag:before { 99 | content: "\38"; 100 | } 101 | .icon-clock:before { 102 | content: "\39"; 103 | } 104 | .icon-comments-3:before { 105 | content: "\3a"; 106 | } 107 | -------------------------------------------------------------------------------- /app/controllers/AppController.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import java.io.IOException; 4 | 5 | import models.User; 6 | import play.Logger; 7 | import play.Play; 8 | import play.i18n.Lang; 9 | import play.mvc.Before; 10 | import play.mvc.Controller; 11 | import play.mvc.Util; 12 | 13 | import com.fasterxml.jackson.core.JsonGenerationException; 14 | import com.fasterxml.jackson.databind.JsonMappingException; 15 | import com.fasterxml.jackson.databind.ObjectMapper; 16 | import com.fasterxml.jackson.databind.SerializationFeature; 17 | 18 | public class AppController extends Controller { 19 | 20 | protected static ObjectMapper jsonObjectMapper = null; 21 | 22 | @Util 23 | public static User getMe() { 24 | String meIdString = session.get("me.id"); 25 | if (meIdString != null) { 26 | return User.findById(Long.parseLong(meIdString)); 27 | } else { 28 | return null; 29 | } 30 | } 31 | 32 | @Util 33 | public static void renderJSON(Object object) { 34 | 35 | if (jsonObjectMapper == null) { 36 | jsonObjectMapper = new ObjectMapper(); 37 | jsonObjectMapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false); 38 | // jsonObjectMapper.setSerializationInclusion(Include.NON_NULL); 39 | } 40 | 41 | if (Play.mode.isDev() || Play.runingInTestMode()) { 42 | jsonObjectMapper.configure(SerializationFeature.INDENT_OUTPUT, true); 43 | Logger.setUp("DEBUG"); 44 | } 45 | 46 | try { 47 | String jsonString = jsonObjectMapper.writeValueAsString(object); 48 | Logger.debug("Rendering JSON" + "\n --------------------------------- " + "\n %s : %s" + "\n --------------------------------- ", object.getClass().getCanonicalName(), jsonString); 49 | Controller.renderJSON(jsonString); 50 | } catch (JsonGenerationException e) { 51 | throw new RuntimeException(e); 52 | } catch (JsonMappingException e) { 53 | throw new RuntimeException(e); 54 | } catch (IOException e) { 55 | throw new RuntimeException(e); 56 | } 57 | } 58 | 59 | @Before 60 | public static void setLanguage() { 61 | if (session.get("lang") != null) { 62 | Lang.set(session.get("lang")); 63 | } else { 64 | String lang = Play.configuration.getProperty("application.langs.default"); 65 | if (lang == null) { 66 | lang = "en"; 67 | } 68 | session.put("lang", lang); 69 | Lang.set(lang); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/views/admin/Users/show.html: -------------------------------------------------------------------------------- 1 | #{extends 'CRUD/layout.html' /} 2 | #{set title:messages.get('crud.show.title', type.modelName) /} 3 | 4 |
5 | 6 |

&{'crud.show.title', type.modelName}

7 | 8 |
9 | #{form action:@save(object._key()), enctype:'multipart/form-data'} 10 |
11 | 12 | ${object.email} 13 |
14 |
15 | 16 | See profile 17 |
18 |
19 | 20 | ${object.googleOAuthAccessToken} 21 |
22 |
23 | 24 | #{if object.googleOAuthRefreshToken} 25 | ${object.googleOAuthRefreshToken} 26 | #{/if} 27 | #{else} 28 | N/A 29 | #{/else} 30 |
31 |
32 | 33 | ${object.karma} 34 |
35 | #{crud.form} 36 | #{crud.custom 'picture'} 37 | 38 | #{if object.picture } 39 | ${object.name} 40 | #{/if} 41 | #{else} 42 | N/A 43 | #{/else} 44 | #{/crud.custom} 45 | #{crud.custom 'userRoles'} 46 | 47 | #{if object.userRoles} 48 | ${object.userRoles} 49 | #{/if} 50 | #{else} 51 | N/A 52 | #{/else} 53 | #{/crud.custom} 54 | #{crud.custom 'created'} 55 | 56 | ${object.created?.since()} 57 | #{/crud.custom} 58 | #{crud.custom 'updated'} 59 | 60 | ${object.updated?.since()} 61 | #{/crud.custom} 62 | #{/crud.form} 63 |

64 | 65 | 66 |

67 | #{/form} 68 |
69 | 70 | #{form @delete(object._key())} 71 |

72 | 73 |

74 | #{/form} 75 | 76 |
-------------------------------------------------------------------------------- /app/models/enums/Mime.java: -------------------------------------------------------------------------------- 1 | package models.enums; 2 | 3 | public enum Mime { 4 | 5 | ADOBE_PDF("application/pdf"), 6 | // GOOGLE_DOCS_SPREADSHEET("application/vnd.google-apps.spreadsheet"), 7 | GOOGLE_DOCS_DOCUMENT("application/vnd.google-apps.document"), GOOGLE_DOCS_PRESENTATION("application/vnd.google-apps.presentation"), MS_DOCX("application/vnd.openxmlformats-officedocument.wordprocessingml.document"), 8 | // MS_EXCEL("application/vnd.ms-excel"), 9 | MS_POWERPOINT("application/vnd.ms-powerpoint"), MS_WORD("application/msword"), OPENDOCS_DOCUMENT("application/vnd.oasis.opendocument.text"), 10 | // OPENDOCS_SPREADSHEET("application/vnd.oasis.opendocument.spreadsheet"), 11 | // OPENDOCS_SPREADSHEET_X("application/x-vnd.oasis.opendocument.spreadsheet"), 12 | OPENDOCS_PRESENTATION("application/vnd.openxmlformats-officedocument.presentationml.presentation"), TEXT_HTML("text/html"), TEXT_PLAIN("text/plain"), TEXT_RTF("application/rtf"); 13 | 14 | private String type; 15 | 16 | Mime(String type) { 17 | this.type = type; 18 | } 19 | 20 | public String getHumanFriendlyName() { 21 | if (this.equals(ADOBE_PDF)) { 22 | return ".pdf"; 23 | } else if (this.equals(MS_DOCX)) { 24 | return ".docx"; 25 | } else if (this.equals(MS_WORD)) { 26 | return ".doc"; 27 | } else if (this.equals(MS_POWERPOINT)) { 28 | return ".ppt"; 29 | } else if (this.equals(TEXT_PLAIN)) { 30 | return ".txt"; 31 | } else if (this.equals(TEXT_HTML)) { 32 | return ".html"; 33 | } else if (this.equals(TEXT_RTF)) { 34 | return ".rtf"; 35 | } else if (this.equals(OPENDOCS_DOCUMENT)) { 36 | return ".ods"; 37 | } else if (this.equals(OPENDOCS_PRESENTATION)) { 38 | return ".pptx"; 39 | } else { 40 | return "Document"; 41 | } 42 | } 43 | 44 | public String getType() { 45 | return this.type; 46 | } 47 | 48 | public boolean isDocument() { 49 | return (this.equals(MS_WORD) || this.equals(GOOGLE_DOCS_DOCUMENT) || this.equals(OPENDOCS_DOCUMENT) || this.equals(TEXT_RTF) || this.equals(TEXT_PLAIN)); 50 | } 51 | 52 | public boolean isPresentation() { 53 | return (this.equals(MS_POWERPOINT) || this.equals(GOOGLE_DOCS_PRESENTATION) || this.equals(OPENDOCS_PRESENTATION)); 54 | } 55 | 56 | public void setType(String type) { 57 | this.type = type; 58 | } 59 | 60 | @Override 61 | public String toString() { 62 | return this.getType(); 63 | } 64 | 65 | public static Mime parseName(String name) { 66 | Mime type = null; 67 | for (Mime item : Mime.values()) { 68 | if (item.getType().equals(name)) { 69 | type = item; 70 | break; 71 | } 72 | } 73 | return type; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /app/services/googleoauth/GoogleOAuthConfig.java: -------------------------------------------------------------------------------- 1 | package services.googleoauth; 2 | 3 | import java.util.Arrays; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | import play.Play; 9 | import utils.Labels; 10 | 11 | public class GoogleOAuthConfig { 12 | 13 | public static final String ACCESS_TOKEN_END_POINT_PROPERTY_NAME = "accessTokenEndPoint"; 14 | public static final String CODE_END_POINT_PROPERTY_NAME = "codeEndPoint"; 15 | public static final String USER_INFO_END_POINT_PROPERTY_NAME = "userInfoEndPoint"; 16 | 17 | private static final String[] CONF_PROPERTIES_NAMES = { Labels.CLIENT_ID, Labels.CLIENT_SECRET, Labels.SCOPE, Labels.REDIRECT_URI, ACCESS_TOKEN_END_POINT_PROPERTY_NAME, CODE_END_POINT_PROPERTY_NAME, USER_INFO_END_POINT_PROPERTY_NAME }; 18 | public static final String CONF_PROPERTY_NAME_PREFIX = "google.oAuth."; 19 | private static Map properties = null; 20 | 21 | public static String getAccessTokenEndPoint() { 22 | return getProperty(ACCESS_TOKEN_END_POINT_PROPERTY_NAME); 23 | } 24 | 25 | public static String getClientId() { 26 | return getProperty(Labels.CLIENT_ID); 27 | } 28 | 29 | public static String getClientSecret() { 30 | return getProperty(Labels.CLIENT_SECRET); 31 | } 32 | 33 | public static String getCodeEndPoint() { 34 | return getProperty(CODE_END_POINT_PROPERTY_NAME); 35 | } 36 | 37 | public static String getProperty(String propertyName) { 38 | if (properties == null) { 39 | loadProperties(); 40 | } 41 | if (Arrays.asList(CONF_PROPERTIES_NAMES).contains(propertyName)) { 42 | return properties.get(propertyName); 43 | } else { 44 | throw new RuntimeException("Invalid Google oAuth Config property : " + propertyName); 45 | } 46 | } 47 | 48 | public static String getRedirectUri() { 49 | return getProperty(Labels.REDIRECT_URI); 50 | } 51 | 52 | public static String getScopes() { 53 | return getProperty(Labels.SCOPE); 54 | } 55 | 56 | public static List getScopesAsArray() { 57 | return Arrays.asList(getScopes().split(" ")); 58 | } 59 | 60 | public static String getUserInfoEndPoint() { 61 | return getProperty(USER_INFO_END_POINT_PROPERTY_NAME); 62 | } 63 | 64 | public static void loadProperties() { 65 | 66 | properties = new HashMap(); 67 | for (String propertyName : CONF_PROPERTIES_NAMES) { 68 | String fullPropertyName = CONF_PROPERTY_NAME_PREFIX + propertyName; 69 | String property = Play.configuration.getProperty(fullPropertyName); 70 | if (property != null) { 71 | properties.put(propertyName, property); 72 | } else { 73 | throw new RuntimeException("Please set " + fullPropertyName + " in application.conf"); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /public/javascripts/bootstrap.button.js: -------------------------------------------------------------------------------- 1 | /* ============================================================ 2 | * bootstrap-button.js v2.1.1 3 | * http://twitter.github.com/bootstrap/javascript.html#buttons 4 | * ============================================================ 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ============================================================ */ 19 | 20 | 21 | !function ($) { 22 | 23 | "use strict"; // jshint ;_; 24 | 25 | 26 | /* BUTTON PUBLIC CLASS DEFINITION 27 | * ============================== */ 28 | 29 | var Button = function (element, options) { 30 | this.$element = $(element) 31 | this.options = $.extend({}, $.fn.button.defaults, options) 32 | } 33 | 34 | Button.prototype.setState = function (state) { 35 | var d = 'disabled' 36 | , $el = this.$element 37 | , data = $el.data() 38 | , val = $el.is('input') ? 'val' : 'html' 39 | 40 | state = state + 'Text' 41 | data.resetText || $el.data('resetText', $el[val]()) 42 | 43 | $el[val](data[state] || this.options[state]) 44 | 45 | // push to event loop to allow forms to submit 46 | setTimeout(function () { 47 | state == 'loadingText' ? 48 | $el.addClass(d).attr(d, d) : 49 | $el.removeClass(d).removeAttr(d) 50 | }, 0) 51 | } 52 | 53 | Button.prototype.toggle = function () { 54 | var $parent = this.$element.closest('[data-toggle="buttons-radio"]') 55 | 56 | $parent && $parent 57 | .find('.active') 58 | .removeClass('active') 59 | 60 | this.$element.toggleClass('active') 61 | } 62 | 63 | 64 | /* BUTTON PLUGIN DEFINITION 65 | * ======================== */ 66 | 67 | $.fn.button = function (option) { 68 | return this.each(function () { 69 | var $this = $(this) 70 | , data = $this.data('button') 71 | , options = typeof option == 'object' && option 72 | if (!data) $this.data('button', (data = new Button(this, options))) 73 | if (option == 'toggle') data.toggle() 74 | else if (option) data.setState(option) 75 | }) 76 | } 77 | 78 | $.fn.button.defaults = { 79 | loadingText: 'loading...' 80 | } 81 | 82 | $.fn.button.Constructor = Button 83 | 84 | 85 | /* BUTTON DATA-API 86 | * =============== */ 87 | 88 | $(function () { 89 | $('body').on('click.button.data-api', '[data-toggle^=button]', function ( e ) { 90 | var $btn = $(e.target) 91 | if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') 92 | $btn.button('toggle') 93 | }) 94 | }) 95 | 96 | }(window.jQuery); -------------------------------------------------------------------------------- /app/controllers/web/WebController.java: -------------------------------------------------------------------------------- 1 | package controllers.web; 2 | 3 | import java.util.List; 4 | 5 | import models.Category; 6 | import models.Discussion; 7 | import models.Document; 8 | import models.User; 9 | import play.Play; 10 | import play.mvc.Before; 11 | import play.mvc.Util; 12 | import controllers.AppController; 13 | 14 | public class WebController extends AppController { 15 | 16 | @Before 17 | public static void addViewArgs() { 18 | renderArgs.put("me", getMe()); 19 | renderArgs.put("siteName", Play.configuration.getProperty("siteName")); 20 | renderArgs.put("siteDescription", Play.configuration.getProperty("siteDescription")); 21 | } 22 | 23 | @Before(unless = { 24 | "web.Auth.googleCode", 25 | "web.Auth.googleToken", 26 | "web.Auth.googleOAuth", 27 | "web.Comments.list", 28 | "web.Discussions.goTo", 29 | "web.Discussions.list", 30 | "web.Discussions.view", 31 | "web.Documents.go", 32 | "web.Documents.list", 33 | "web.Documents.read", 34 | "web.Users.read", 35 | "web.WebController.index", 36 | "web.WebController.search" 37 | }) 38 | public static void checkAccess() { 39 | if (getMe() == null) { 40 | flash.error("auth.login.needed"); 41 | Auth.googleCode(request.url); 42 | } 43 | } 44 | 45 | @Before 46 | public static void setDefaultRequestParameters() { 47 | String pageParam = request.params.get("page"); 48 | if (pageParam != null) { 49 | int page = Integer.parseInt(pageParam); 50 | if (page < 1) { 51 | request.params.put("page", "1"); 52 | } 53 | } else { 54 | request.params.put("page", "1"); 55 | } 56 | } 57 | 58 | @Util // we are not using it for now 59 | public static void updateLanguage(String lang, String nextUrl) { 60 | checkAuthenticity(); 61 | if (lang != null) { 62 | if (lang.equals("en")) { 63 | session.put("lang", "en"); 64 | } else { 65 | session.put("lang", "fr"); 66 | } 67 | } 68 | redirect(nextUrl); 69 | } 70 | 71 | public static void index() { 72 | List recentDocuments = Document.find("order by created desc").fetch(3); 73 | List recentDiscussions = Discussion.find("order by created desc").fetch(3); 74 | List recentUsers = User.find("order by created desc").fetch(20); 75 | List documentCategories = Category.findForDocument(); 76 | List discussionCategories = Category.findForDiscussion(); 77 | render(discussionCategories, documentCategories, recentDiscussions, recentDocuments, recentUsers); 78 | } 79 | 80 | public static void search(String keyword, String type) { 81 | if (type != null) { 82 | if (type.equals("documents")) { 83 | Documents.list(keyword, 0, null, null); 84 | } else if (type.equals("discussions")) { 85 | Discussions.list(keyword, 0, null, null); 86 | } 87 | } 88 | index(); 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /app/controllers/web/Auth.java: -------------------------------------------------------------------------------- 1 | package controllers.web; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | 6 | import models.User; 7 | import play.Logger; 8 | import play.i18n.Messages; 9 | import play.mvc.Util; 10 | import play.mvc.With; 11 | import services.googleoauth.GoogleOAuth; 12 | import services.googleoauth.GoogleOAuthTokens; 13 | import services.googleoauth.GoogleUserInfo; 14 | import controllers.AppController; 15 | 16 | @With(WebController.class) 17 | public class Auth extends AppController { 18 | 19 | @Util 20 | public static String getOAuthRedirectUriHost() { 21 | String redirectHost = request.host; 22 | if (request.secure) { 23 | redirectHost = "https://" + redirectHost; 24 | } else { 25 | redirectHost = "http://" + redirectHost; 26 | } 27 | return redirectHost; 28 | } 29 | 30 | public static void googleCode(String next) { 31 | if (next != null) { 32 | session.put("next", next); 33 | } 34 | redirect(GoogleOAuth.buildCodeRequestUrl(getOAuthRedirectUriHost())); 35 | } 36 | 37 | public static void googleToken(String code) { 38 | if (code != null) { 39 | try { 40 | GoogleOAuthTokens googleOAuthTokens = GoogleOAuth.askForOAuthTokens(code, getOAuthRedirectUriHost()); 41 | GoogleUserInfo googleUserInfo = GoogleOAuth.askForUserInfo(googleOAuthTokens); 42 | 43 | User me = User.findByGoogleUserId(googleUserInfo.getId()); 44 | if (me == null) { 45 | me = User.createFromGoogleUserInfo(googleUserInfo); 46 | } 47 | 48 | me.googleOAuthAccessToken = googleOAuthTokens.getAccessToken(); 49 | if (googleOAuthTokens.getRefreshToken() != null) { 50 | me.googleOAuthRefreshToken = googleOAuthTokens.getRefreshToken(); 51 | } 52 | 53 | User checkUser = User.find("").first(); 54 | if (checkUser == null) { 55 | me.userRoles = new ArrayList(); 56 | me.userRoles.add("admin"); 57 | } 58 | me.save(); 59 | session.put("me.id", me.id); 60 | flash.success(Messages.get("auth.login.success", me.name)); 61 | flash.keep(); 62 | } catch (IOException e) { 63 | flash.error(Messages.get("auth.oauth.error")); 64 | Logger.error("Error during sign in with Google proccess : " + "\n Message : ", e.getMessage()); 65 | redirect("/"); 66 | } 67 | if (session.get("next") == null) { 68 | redirect("/"); 69 | } else { 70 | String next = session.get("next"); 71 | session.remove("next"); 72 | redirect(next); 73 | } 74 | } 75 | } 76 | 77 | public static void logout() { 78 | checkAuthenticity(); 79 | User me = Auth.getMe(); 80 | if (me != null) { 81 | session.remove("me.id"); 82 | flash.success(Messages.get("auth.logout.success", me.name)); 83 | } 84 | redirect("/"); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/models/User.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import javax.persistence.Entity; 7 | import javax.persistence.Table; 8 | 9 | import models.deadbolt.Role; 10 | import models.deadbolt.RoleHolder; 11 | import play.data.binding.NoBinding; 12 | import play.data.validation.Email; 13 | import play.data.validation.MaxSize; 14 | import play.data.validation.Required; 15 | import services.googleoauth.GoogleUserInfo; 16 | 17 | import com.fasterxml.jackson.annotation.JsonProperty; 18 | 19 | @Entity 20 | @Table(name = "user_") 21 | public class User extends BaseModel implements RoleHolder { 22 | 23 | @JsonProperty 24 | @MaxSize(1000) 25 | public String bio; 26 | 27 | @Required 28 | @Email 29 | @NoBinding 30 | public String email; 31 | 32 | @Required 33 | @NoBinding 34 | public String googleOAuthAccessToken; 35 | 36 | @Required 37 | @NoBinding 38 | public String googleOAuthRefreshToken; 39 | 40 | @Required 41 | @JsonProperty 42 | @NoBinding 43 | public String googleUserId; 44 | 45 | @Required 46 | @JsonProperty 47 | public String name; 48 | 49 | @JsonProperty 50 | public String picture; 51 | 52 | @JsonProperty 53 | @NoBinding 54 | public int karma; 55 | 56 | public ArrayList userRoles; 57 | 58 | public User(String googleUserId, String name, String email, String picture) { 59 | this.googleUserId = googleUserId; 60 | this.name = name; 61 | this.email = email; 62 | this.picture = picture; 63 | this.karma = 0; 64 | } 65 | 66 | public int addToKarmaAndSave(int addition) { 67 | this.karma = this.karma + addition; 68 | this.save(); 69 | return this.karma; 70 | } 71 | 72 | @Override 73 | public List getRoles() { 74 | List roles = new ArrayList(); 75 | if (this.userRoles != null) { 76 | for (String role : this.userRoles) { 77 | roles.add(new UserRole(role)); 78 | } 79 | } 80 | return roles; 81 | } 82 | 83 | public Vote getVoteForObject(String objectType, long objectId) { 84 | if (this.id != null && objectType != null) { 85 | return Vote.find("user.id is ? and objectType is ? and objectId is ?", this.id, objectType.toLowerCase(), objectId).first(); 86 | } else { 87 | return null; 88 | } 89 | } 90 | 91 | @Override 92 | public String toString() { 93 | return this.name; 94 | } 95 | 96 | public static User createFromGoogleUserInfo(GoogleUserInfo googleUserInfo) { 97 | User user = new User(googleUserInfo.getId(), googleUserInfo.getName(), googleUserInfo.getEmail(), googleUserInfo.getPicture()); 98 | return user; 99 | } 100 | 101 | public static User findByEmail(String email) { 102 | if (email != null) { 103 | return User.find("email is ?", email).first(); 104 | } else { 105 | return null; 106 | } 107 | } 108 | 109 | public static User findByGoogleUserId(String googleUserId) { 110 | if (googleUserId != null) { 111 | return User.find("googleUserId is ?", googleUserId).first(); 112 | } else { 113 | return null; 114 | } 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /app/models/Vote.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.ManyToOne; 5 | import javax.persistence.MappedSuperclass; 6 | 7 | import com.fasterxml.jackson.annotation.JsonGetter; 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | 10 | import play.data.validation.Required; 11 | 12 | @Entity 13 | public class Vote extends BaseModel { 14 | 15 | @Required 16 | @ManyToOne 17 | @JsonProperty 18 | public User user; 19 | 20 | @Required 21 | @JsonProperty 22 | public String objectType; 23 | 24 | @Required 25 | @JsonProperty 26 | public long objectId; 27 | 28 | @JsonGetter 29 | public int getObjectVoteCount() { 30 | int count = -1; 31 | if ("document".equals(this.objectType)) { 32 | Document document = Document.findById(this.objectId); 33 | if (document != null) { 34 | count = document.voteCount; 35 | } 36 | } else if ("comment".equals(this.objectType)) { 37 | Comment comment = Comment.findById(this.objectId); 38 | if (comment != null) { 39 | count = comment.voteCount; 40 | } 41 | } else if ("discussion".equals(this.objectType)) { 42 | Discussion discussion = Discussion.findById(this.objectId); 43 | if (discussion != null) { 44 | count = discussion.voteCount; 45 | } 46 | } 47 | return count; 48 | } 49 | 50 | public int updateCountForObject(boolean increase) { 51 | int count = -1; 52 | if ("document".equals(this.objectType)) { 53 | Document document = Document.findById(this.objectId); 54 | if (document != null) { 55 | count = document.updateVoteCountAndSave(increase); 56 | if (this.user.id != document.owner.id) { 57 | document.owner.addToKarmaAndSave(increase ? 1 : -1); 58 | } 59 | } 60 | } else if ("comment".equals(this.objectType)) { 61 | Comment comment = Comment.findById(this.objectId); 62 | if (comment != null) { 63 | count = comment.updateVoteCountAndSave(increase); 64 | if (this.user.id != comment.author.id) { 65 | comment.author.addToKarmaAndSave(increase ? 1 : -1); 66 | } 67 | } 68 | } else if ("discussion".equals(this.objectType)) { 69 | Discussion discussion = Discussion.findById(this.objectId); 70 | if (discussion != null) { 71 | count = discussion.updateVoteCountAndSave(increase); 72 | if (this.user.id != discussion.user.id) { 73 | discussion.user.addToKarmaAndSave(increase ? 1 : -1); 74 | } 75 | } 76 | } 77 | return count; 78 | } 79 | 80 | public static void deleteAllForComment(long id) { 81 | deleteAllForObject("comment", id); 82 | } 83 | 84 | public static void deleteAllForDiscussion(long id) { 85 | deleteAllForObject("discussion", id); 86 | } 87 | 88 | public static void deleteAllForDocument(long id) { 89 | deleteAllForObject("document", id); 90 | } 91 | 92 | private static void deleteAllForObject(String objectType, long objectId) { 93 | if (objectType != null) { 94 | Vote.delete("objectType is ? and objectId is ?", objectType, objectId); 95 | } else { 96 | throw new RuntimeException("objectType parameter can't be null"); 97 | } 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /app/models/Comment.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import javax.persistence.Entity; 7 | import javax.persistence.Lob; 8 | import javax.persistence.ManyToOne; 9 | 10 | import org.hibernate.annotations.Index; 11 | 12 | import net.sf.oval.constraint.MinSize; 13 | 14 | import com.fasterxml.jackson.annotation.JsonGetter; 15 | import com.fasterxml.jackson.annotation.JsonProperty; 16 | 17 | import play.data.binding.NoBinding; 18 | import play.data.validation.MaxSize; 19 | import play.data.validation.Required; 20 | import play.db.jpa.Transactional; 21 | 22 | @Entity 23 | public class Comment extends BaseModel { 24 | 25 | @ManyToOne 26 | @JsonProperty 27 | @NoBinding 28 | public User author; 29 | 30 | @JsonProperty 31 | @Required 32 | @MaxSize(5000) 33 | public String content; 34 | 35 | @JsonProperty 36 | public long objectId; 37 | 38 | @JsonProperty 39 | public String objectType; 40 | 41 | @JsonProperty 42 | @NoBinding 43 | public int voteCount; 44 | 45 | public Comment(User author, String content, long objectId) { 46 | this.author = author; 47 | this.content = content; 48 | this.objectId = objectId; 49 | } 50 | 51 | @JsonGetter 52 | public int getObjectCommentCount() { 53 | int count = -1; 54 | if ("document".equals(this.objectType)) { 55 | Document document = Document.findById(this.objectId); 56 | if (document != null) { 57 | count = document.commentCount; 58 | } 59 | } else if ("discussion".equals(this.objectType)) { 60 | Discussion discussion = Discussion.findById(this.objectId); 61 | if (discussion != null) { 62 | count = discussion.commentCount; 63 | } 64 | } 65 | return count; 66 | } 67 | 68 | public int updateCountForObject(boolean increase) { 69 | int count = -1; 70 | if ("document".equals(this.objectType)) { 71 | Document document = Document.findById(this.objectId); 72 | if (document != null) { 73 | count = document.updateCommentCountAndSave(increase); 74 | } 75 | } else if ("discussion".equals(this.objectType)) { 76 | Discussion discussion = Discussion.findById(this.objectId); 77 | if (discussion != null) { 78 | count = discussion.updateCommentCountAndSave(increase); 79 | } 80 | } 81 | return count; 82 | } 83 | 84 | @Transactional 85 | public int updateVoteCountAndSave(boolean increment) { 86 | if (increment) { 87 | this.voteCount++; 88 | } else { 89 | this.voteCount--; 90 | } 91 | this.save(); 92 | return this.voteCount; 93 | } 94 | 95 | public static List findByObject(String objectType, long objectId, int page) { 96 | if (objectType != null) { 97 | return Comment.find("objectType is ? and objectId is ? order by created asc", objectType, objectId).fetch(page, DEFAULT_PAGINATE_COUNT); 98 | } else { 99 | throw new RuntimeException("Please specify objectType parameter"); 100 | } 101 | } 102 | 103 | public static int deleteAllForObject(String objectType, long objectId) { 104 | if (objectType != null) { 105 | return Comment.delete("objectType is ? and objectId is ?", objectType, objectId); 106 | } else { 107 | throw new RuntimeException("Please specify objectType parameter"); 108 | } 109 | } 110 | 111 | } -------------------------------------------------------------------------------- /app/controllers/api/Discussions.java: -------------------------------------------------------------------------------- 1 | package controllers.api; 2 | 3 | import java.util.Date; 4 | import java.util.List; 5 | 6 | import jobs.UpdateDocumentCommentCountJob; 7 | import models.Category; 8 | import models.Discussion; 9 | import models.DiscussionDocument; 10 | import models.Document; 11 | import models.User; 12 | import models.Vote; 13 | import play.data.validation.Required; 14 | import play.mvc.With; 15 | import controllers.AppController; 16 | import controllers.web.Auth; 17 | 18 | @With(ApiController.class) 19 | public class Discussions extends AppController { 20 | 21 | public static void create(@Required Discussion discussion, Document document) { 22 | if (!validation.hasErrors()) { 23 | User me = getMe(); 24 | discussion.content = discussion.content.trim(); 25 | discussion.user = me; 26 | if (discussion.validateAndSave()) { 27 | if (document.id != null) { 28 | DiscussionDocument discussionDocument = new DiscussionDocument(discussion, document, me); 29 | if (discussionDocument.validateAndSave()) { 30 | discussion.updateDocumentCountAndSave(true); 31 | document.updateDiscussionCountAndSave(true); 32 | } 33 | } 34 | renderJSON(discussion); 35 | } 36 | } 37 | } 38 | 39 | public static void delete(long id) { 40 | checkAuthenticity(); 41 | User me = Auth.getMe(); 42 | Discussion discussion = Discussion.findById(id); 43 | notFoundIfNull(discussion); 44 | if (discussion.user.id == me.id) { 45 | Vote.deleteAllForDiscussion(discussion.id); 46 | discussion.delete(); 47 | renderJSON(true); 48 | } else { 49 | unauthorized(); 50 | } 51 | } 52 | 53 | public static void list(String keyword, long categoryId, String order, Integer page) { 54 | List documents = Document.search(keyword, categoryId, order, page); 55 | renderJSON(documents); 56 | } 57 | 58 | public static void markAsViewed(long id) { 59 | checkAuthenticity(); 60 | Discussion discussion = Discussion.findById(id); 61 | if (discussion != null) { 62 | String sessionKey = String.format("lastDiscussionViewed", id); 63 | 64 | if (session.get(sessionKey) != null) { 65 | if (session.get(sessionKey) != discussion.id.toString()) { 66 | discussion.increaseViewCountAndSave(); 67 | session.put(sessionKey, discussion.id); 68 | } 69 | } else { 70 | discussion.increaseViewCountAndSave(); 71 | session.put(sessionKey, new Date().getTime()); 72 | } 73 | } else { 74 | notFound(); 75 | } 76 | ok(); 77 | } 78 | 79 | public static void update(long id, @Required Discussion discussion) { 80 | checkAuthenticity(); 81 | User me = Auth.getMe(); 82 | Discussion check = Discussion.findById(id); 83 | notFoundIfNull(check); 84 | if (check.user.id == me.id) { 85 | if (!validation.hasErrors()) { 86 | check.title = discussion.title; 87 | check.category = discussion.category; 88 | check.content = discussion.content; 89 | check.tags = discussion.tags; 90 | check.user = me; 91 | discussion = check; 92 | if (discussion.validateAndSave()) { 93 | renderJSON(discussion); 94 | } 95 | } 96 | } else { 97 | unauthorized(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/views/web/Discussions/edit.html: -------------------------------------------------------------------------------- 1 | #{extends 'web.html' /} 2 | #{set title : discussion.title/} 3 | #{set 'subTitle'} 4 | &{'discussion.edit'} 5 | #{/set} 6 | #{i18n keys:['discussion.update.error', 'discussion.update.success', 'discussion.delete.success', 'discussion.delete.error'] /} 7 |
8 |
9 |
10 |

11 |

12 |
13 | #{form @api.Discussions.update(discussion.id), enctype:'multipart/form-data'} 14 | #{field 'discussion.title'} 15 | #{form.input field : field, value : discussion?.title, class : 'input-xxlarge', required : true /} 16 | #{/field} 17 | #{field 'discussion.category.id'} 18 | #{form.select field : field, items : categories, valueProperty : 'id', labelProperty : 'name', value : discussion?.category?.id /} 19 | #{/field} 20 | #{field 'discussion.content'} 21 | #{form.textarea field : field, value : discussion?.content, required : true /} 22 | #{/field} 23 | #{field 'discussion.tags'} 24 | #{form.input field : field, value : discussion?.tags, class : 'input-xxlarge', required : true /} 25 | #{/field} 26 |
27 | 28 | #{a @web.Discussions.view(discussion?.id, discussion?.slug), class : 'button secondary'}&{'_.cancel'}#{/a} 29 |
30 | #{/form} 31 | 32 | #{a @api.Discussions.delete(discussion.id), class : 'button alert small button-discussion-delete', 'data-loading-text' : messages.get('_.loading')}&{'discussion.delete'}#{/a} 33 |
34 |
35 | #{set 'moreScripts'} 36 | 83 | #{/set} -------------------------------------------------------------------------------- /app/views/web/Discussions/add.html: -------------------------------------------------------------------------------- 1 | #{extends 'web.html' /} 2 | #{set 'title'} 3 | &{'_.startDiscussion'} 4 | #{/set} 5 | #{i18n keys:['discussion.create.success', 'discussion.create.error'] /} 6 |
7 |
8 |
9 |

10 |

11 |
12 | #{form @api.Discussions.create(), class : 'discussion-create-form'} 13 | #{if document} 14 | 15 | #{/if} 16 | #{field 'discussion.title'} 17 | #{form.input field : field, value : discussion?.title, class : 'input-xxlarge', required : true /} 18 | #{/field} 19 | #{field 'discussion.category.id'} 20 | #{form.select field : field, items : categories, valueProperty : 'id', labelProperty : 'name', value : discussion?.category?.id /} 21 | #{/field} 22 | #{field 'discussion.content'} 23 | #{form.textarea field : field, value : discussion?.content, class : 'input-xxlarge', required : true /} 24 | #{/field} 25 | #{field 'discussion.tags'} 26 | #{form.input field : field, value : discussion?.tags, class : 'input-xxlarge', 'help' : 'tags.help', required : true /} 27 | #{/field} 28 |
29 | 30 | #{a @web.Discussions.list(), class : 'button secondary'}&{'_.cancel'}#{/a} 31 |
32 | #{/form} 33 |
34 |
35 | #{if document} 36 |
37 |

Linked Document

38 |
    39 |
  • 40 | #{document.title document : document /} 41 |
  • 42 |
43 |

44 | Please note that you will be able to link additional documents to this discussion once you save it. 45 |

46 |
47 | #{/if} 48 |
49 |
50 | #{set 'moreScripts'} 51 | 88 | #{/set} -------------------------------------------------------------------------------- /app/views/web/Documents/edit.html: -------------------------------------------------------------------------------- 1 | #{extends 'web.html' /} 2 | #{set title : document.title/} 3 | #{set 'subTitle'} 4 | &{'document.edit'} 5 | #{/set} 6 | #{i18n keys:['document.update.error', 'document.update.success', 'document.delete.success', 'document.delete.error'] /} 7 |
8 |
9 |
10 |

11 |

12 |
13 | #{form @api.Documents.update(document.id), enctype:'multipart/form-data'} 14 | #{field 'document.title'} 15 | #{form.input field : field, value : document?.title, class : 'input-xxlarge', required : true /} 16 | #{/field} 17 | #{field 'document.category.id'} 18 | #{form.select field : field, items : categories, valueProperty : 'id', labelProperty : 'name', value : document?.category?.id /} 19 | #{/field} 20 | #{field 'document.description'} 21 | #{form.textarea field : field, value : document?.description, class : 'input-xxlarge', required : true /} 22 | #{/field} 23 | #{field 'document.source'} 24 | #{form.input field : field, value : document?.source, class : 'input-xxlarge', required : false /} 25 | #{/field} 26 | #{field 'document.tags'} 27 | #{form.input field : field, value : document?.tags, class : 'input-xxlarge', required : true /} 28 | #{/field} 29 |
30 | 31 | #{a @web.Documents.read(document?.id, document?.slug), class : 'button secondary'}&{'_.cancel'}#{/a} 32 |
33 | #{/form} 34 | 35 | #{a @api.Documents.delete(document.id), class : 'button alert small button-document-delete', 'data-loading-text' : messages.get('_.loading')}&{'document.delete'}#{/a} 36 |
37 |
38 | #{set 'moreScripts'} 39 | 86 | #{/set} -------------------------------------------------------------------------------- /app/views/web/Discussions/list.html: -------------------------------------------------------------------------------- 1 | #{extends 'web.html' /} 2 | #{set title : messages.get('discussions') /} 3 | #{i18n keys:['_.errorTryAgain', '_.loading'] /} 4 | 5 | #{if order == null} 6 | #{set 'title' } 7 | &{'discussions'} 8 | #{/set} 9 | #{/if} 10 | 11 | #{if keyword} 12 | #{set 'subTitle' } 13 | #{if get('subTitle')} 14 | #{get 'subTitle' /} - 15 | #{/if} 16 | '${keyword}' 17 | #{/set} 18 | #{/if} 19 | 20 | #{if category} 21 | #{set 'subTitle' } 22 | #{if get('subTitle')} 23 | #{get 'subTitle' /} - 24 | #{/if} 25 | ${category.name} 26 | #{/set} 27 | #{/if} 28 | 29 | #{if order} 30 | #{set 'subTitle' } 31 | #{if get('subTitle')} 32 | #{get 'subTitle' /} - 33 | #{/if} 34 | &{'document.list.order.' + order} 35 | #{/set} 36 | #{/if} 37 | 38 |
39 |
40 |
41 | #{include 'web/Discussions/ajax_list.html' /} 42 |
43 |
44 |
45 |
46 |

Search Filter

47 | #{form @web.Discussions.list()} 48 | 49 | 50 | #{/form} 51 |

52 | #{if keyword} 53 | &{'document.list.clearSearchOptions'} 54 | #{/if} 55 |

56 |
57 | 82 |
83 |

&{'_.categories'}

84 |
    85 |
  • 86 | #{a @web.Discussions.list(keyword, null, order, null)}&{'_.allCategories'}#{/a} 87 |
  • 88 | #{list categories} 89 |
  • 90 | #{a @web.Discussions.list(keyword, _.id, order, null)}${_.name}#{/a} 91 |
  • 92 | #{/list} 93 |
94 |
95 |
96 |
97 | 98 | #{set 'moreScripts'} 99 | 118 | #{/set} -------------------------------------------------------------------------------- /app/services/googleoauth/GoogleOAuth.java: -------------------------------------------------------------------------------- 1 | package services.googleoauth; 2 | 3 | import java.io.IOException; 4 | 5 | import models.User; 6 | import play.libs.URLs; 7 | import play.libs.WS; 8 | import play.libs.WS.WSRequest; 9 | import utils.Labels; 10 | 11 | import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; 12 | import com.google.api.client.http.HttpTransport; 13 | import com.google.api.client.http.javanet.NetHttpTransport; 14 | import com.google.api.client.json.jackson.JacksonFactory; 15 | import com.google.api.services.drive.Drive; 16 | import com.google.gson.Gson; 17 | 18 | public class GoogleOAuth { 19 | 20 | public static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); 21 | public static final JacksonFactory JSON_FACTORY = new JacksonFactory(); 22 | 23 | public static GoogleOAuthTokens askForOAuthTokens(String code, String redirectHost) throws IOException { 24 | WSRequest tokensRequest = WS.url(GoogleOAuthConfig.getAccessTokenEndPoint()); 25 | tokensRequest.setParameter(Labels.CLIENT_ID, GoogleOAuthConfig.getClientId()); 26 | tokensRequest.setParameter(Labels.CLIENT_SECRET, GoogleOAuthConfig.getClientSecret()); 27 | tokensRequest.setParameter(Labels.REDIRECT_URI, redirectHost + GoogleOAuthConfig.getRedirectUri()); 28 | tokensRequest.setParameter(Labels.CODE, code).setParameter(Labels.GRANT_TYPE, Labels.AUTHORIZATION_CODE); 29 | WS.HttpResponse tokensResponse = tokensRequest.post(); 30 | 31 | if (tokensResponse.getStatus() == 200) { 32 | return new Gson().fromJson(tokensResponse.getJson(), GoogleOAuthTokens.class); 33 | } else { 34 | throw new IOException(String.format("Error during Tokens Request :" + "\n Response Status : %s " + "\n Response Text : %s", tokensResponse.getStatus(), tokensResponse.getString())); 35 | } 36 | } 37 | 38 | public static GoogleUserInfo askForUserInfo(GoogleOAuthTokens googleOAuthTokens) throws IOException { 39 | WSRequest userInfoRequest = WS.url(GoogleOAuthConfig.getUserInfoEndPoint()); 40 | userInfoRequest.setParameter(Labels.ACCESS_TOKEN, googleOAuthTokens.getAccessToken()); 41 | WS.HttpResponse userInfoResponse = userInfoRequest.get(); 42 | 43 | if (userInfoResponse.getStatus() == 200) { 44 | return new Gson().fromJson(userInfoResponse.getJson(), GoogleUserInfo.class); 45 | } else { 46 | throw new IOException(String.format("Error during User Info Request :" + "\n Response Status : %s " + "\n Response Text : %s", userInfoResponse.getStatus(), userInfoResponse.getString())); 47 | } 48 | } 49 | 50 | public static String buildCodeRequestUrl(String redirectHost) { 51 | String codeRequestUrl = GoogleOAuthConfig.getCodeEndPoint(); 52 | codeRequestUrl = URLs.addParam(codeRequestUrl, Labels.CLIENT_ID, GoogleOAuthConfig.getClientId()); 53 | codeRequestUrl = URLs.addParam(codeRequestUrl, Labels.REDIRECT_URI, redirectHost + GoogleOAuthConfig.getRedirectUri()); 54 | codeRequestUrl = URLs.addParam(codeRequestUrl, Labels.SCOPE, GoogleOAuthConfig.getScopes()); 55 | codeRequestUrl = URLs.addParam(codeRequestUrl, Labels.RESPONSE_TYPE, Labels.CODE); 56 | codeRequestUrl = URLs.addParam(codeRequestUrl, Labels.ACCESS_TYPE, Labels.OFFLINE); 57 | return codeRequestUrl; 58 | } 59 | 60 | public static GoogleCredential buildCredentialForUser(User me) { 61 | String clientId = GoogleOAuthConfig.getClientId(); 62 | String clientSecret = GoogleOAuthConfig.getClientSecret(); 63 | 64 | GoogleCredential.Builder credentialBuilder = new GoogleCredential.Builder(); 65 | credentialBuilder.setClientSecrets(clientId, clientSecret); 66 | credentialBuilder.setTransport(HTTP_TRANSPORT); 67 | credentialBuilder.setJsonFactory(JSON_FACTORY); 68 | credentialBuilder.addRefreshListener(new GoogleOAuthCredentialListener(me)); 69 | 70 | GoogleCredential credential = credentialBuilder.build(); 71 | credential.setAccessToken(me.googleOAuthAccessToken); 72 | credential.setRefreshToken(me.googleOAuthRefreshToken); 73 | 74 | return credential; 75 | } 76 | 77 | public static Drive buildDriveServiceForUser(User user) throws IOException { 78 | GoogleCredential credential = buildCredentialForUser(user); 79 | credential.refreshToken(); 80 | return new Drive.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential).build(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/views/web/WebController/index.html: -------------------------------------------------------------------------------- 1 | #{extends 'web.html' /} 2 | #{set 'title'}&{'welcome'}#{/set} 3 |
4 |
5 |

&{'_.welcome', siteName}

6 |

7 | ${siteDescription} 8 |

9 |
10 |
11 |
12 |
13 |

&{'_.explore'}

14 | 15 |

16 | &{'_.explore.expand'} 17 |

18 |
19 |
20 |

&{'_.discuss'}

21 | 22 |

23 | &{'_.discuss.expand'} 24 |

25 |
26 |
27 |

&{'_.share'}

28 | 29 |

30 | &{'_.share.expand'} 31 |

32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |

&{'_.recentDocuments'}

42 | #{list recentDocuments} 43 |
44 | #{document.title document : _ /} 45 |
46 | #{block.time time : _.created /} 47 |
48 |
49 | #{user.picture showName : true, size : 15, user : _.owner /} 50 |
51 |
52 | #{/list} 53 |

54 | #{a @web.Documents.list()}All categories#{/a} 55 | #{list documentCategories} 56 | - #{a @web.Documents.list(null, _.id, null, null)}${_.name}#{/a} 57 | #{/list} 58 |

59 |
60 |
61 |

Recent discussions

62 | #{list recentDiscussions} 63 |
64 | #{discussion.title discussion : _ /} 65 |
66 | #{block.time time : _.created /} 67 |
68 |
69 | #{user.picture showName : true, size : 15, user : _.user /} 70 |
71 |
72 | #{/list} 73 |

74 | #{a @web.Discussions.list()}All categories#{/a} 75 | #{list discussionCategories} 76 | - #{a @web.Discussions.list(null, _.id, null, null)}${_.name}#{/a} 77 | #{/list} 78 |

79 |
80 |
81 |

&{'_.newestMembers'}

82 |
83 | #{list recentUsers} 84 | #{user.picture user : _, size : 50 /} 85 | #{/list} 86 |
87 |
88 |
89 |
    90 |
  1. 91 |

    92 | Hello! This quick tour will guide through you the main features of this application. 93 |

    94 |
  2. 95 |
  3. 96 |

    97 | Explore documents : read, comment and copy them directly to your Google Drive account. 98 |

    99 |
  4. 100 |
  5. 101 |

    102 | Discussions are deeper conversations about documents. You can explore and read them here. You can also link additional documents to a discussion. 103 |

    104 |
  6. 105 |
  7. 106 |

    107 | Upload a document to share it with others. A copy of that document will also be uploaded to your Google Drive. 108 |

    109 |
  8. 110 |
  9. 111 |

    112 | Start a discussion : ask a question, post an idea or debate. Link documents to these discussions. 113 |

    114 |
  10. 115 |
  11. 116 |

    117 | Quick search : access documents and discussions directly from here. 118 |

    119 |
  12. 120 |
  13. 121 |

    122 | You can use this application without signing in. However for community features, you will need to sign in. 123 |

    124 |
  14. 125 |
126 | #{set 'moreScripts'} 127 | 135 | #{/set} -------------------------------------------------------------------------------- /db/evolutions/1.sql: -------------------------------------------------------------------------------- 1 | # --- !Ups 2 | CREATE TABLE category 3 | ( 4 | id bigint NOT NULL, 5 | created timestamp without time zone, 6 | updated timestamp without time zone, 7 | documentcount integer NOT NULL, 8 | "name" character varying(255), 9 | CONSTRAINT category_pkey PRIMARY KEY (id) 10 | ) 11 | WITH ( 12 | OIDS=FALSE 13 | ); 14 | 15 | CREATE TABLE clonedocumentjobstatus 16 | ( 17 | id bigint NOT NULL, 18 | cloneddocumentid bigint NOT NULL, 19 | done boolean NOT NULL, 20 | CONSTRAINT clonedocumentjobstatus_pkey PRIMARY KEY (id) 21 | ) 22 | WITH ( 23 | OIDS=FALSE 24 | ); 25 | 26 | CREATE TABLE "comment" 27 | ( 28 | id bigint NOT NULL, 29 | created timestamp without time zone, 30 | updated timestamp without time zone, 31 | "content" character varying(255), 32 | author_id bigint, 33 | document_id bigint, 34 | CONSTRAINT comment_pkey PRIMARY KEY (id) 35 | ) 36 | WITH ( 37 | OIDS=FALSE 38 | ); 39 | 40 | CREATE TABLE "document" 41 | ( 42 | id bigint NOT NULL, 43 | created timestamp without time zone, 44 | updated timestamp without time zone, 45 | alternatelink character varying(255), 46 | clonecount integer NOT NULL, 47 | commentcount integer NOT NULL, 48 | description character varying(255), 49 | downloadcount integer NOT NULL, 50 | embedlink character varying(255), 51 | filesize bigint NOT NULL, 52 | googledrivefileid character varying(255), 53 | isarchived boolean NOT NULL, 54 | mimetype character varying(255), 55 | modifieddate character varying(255), 56 | readcount integer NOT NULL, 57 | slug character varying(255), 58 | source character varying(255), 59 | title character varying(255), 60 | category_id bigint, 61 | originaldocument_id bigint, 62 | owner_id bigint, 63 | thumbnail_id bigint, 64 | CONSTRAINT document_pkey PRIMARY KEY (id) 65 | ) 66 | WITH ( 67 | OIDS=FALSE 68 | ); 69 | 70 | CREATE TABLE exportlink 71 | ( 72 | id bigint NOT NULL, 73 | created timestamp without time zone, 74 | updated timestamp without time zone, 75 | link character varying(255), 76 | mimetype character varying(255), 77 | document_id bigint, 78 | CONSTRAINT exportlink_pkey PRIMARY KEY (id) 79 | ) 80 | WITH ( 81 | OIDS=FALSE 82 | ); 83 | 84 | CREATE TABLE thumbnail 85 | ( 86 | id bigint NOT NULL, 87 | image oid, 88 | mimetype character varying(255), 89 | CONSTRAINT thumbnail_pkey PRIMARY KEY (id) 90 | ) 91 | WITH ( 92 | OIDS=FALSE 93 | ); 94 | 95 | CREATE TABLE user_ 96 | ( 97 | id bigint NOT NULL, 98 | created timestamp without time zone, 99 | updated timestamp without time zone, 100 | bio character varying(255), 101 | documentcount integer, 102 | email character varying(255), 103 | googleoauthaccesstoken character varying(255), 104 | googleoauthrefreshtoken character varying(255), 105 | googleuserid character varying(255), 106 | "name" character varying(255), 107 | picture character varying(255), 108 | score integer, 109 | userroles bytea, 110 | CONSTRAINT user__pkey PRIMARY KEY (id) 111 | ) 112 | WITH ( 113 | OIDS=FALSE 114 | ); 115 | 116 | ALTER TABLE "comment" 117 | ADD CONSTRAINT fk9bde863fa7cd013e FOREIGN KEY (author_id) 118 | REFERENCES user_ (id) MATCH SIMPLE 119 | ON UPDATE NO ACTION ON DELETE NO ACTION, 120 | ADD CONSTRAINT fk9bde863fed0b1afe FOREIGN KEY (document_id) 121 | REFERENCES "document" (id) MATCH SIMPLE 122 | ON UPDATE NO ACTION ON DELETE NO ACTION; 123 | ALTER TABLE "document" 124 | ADD CONSTRAINT fk3737353b120f48d FOREIGN KEY (originaldocument_id) 125 | REFERENCES "document" (id) MATCH SIMPLE 126 | ON UPDATE NO ACTION ON DELETE NO ACTION, 127 | ADD CONSTRAINT fk3737353bb2fabf16 FOREIGN KEY (owner_id) 128 | REFERENCES user_ (id) MATCH SIMPLE 129 | ON UPDATE NO ACTION ON DELETE NO ACTION, 130 | ADD CONSTRAINT fk3737353bdf01b96 FOREIGN KEY (thumbnail_id) 131 | REFERENCES thumbnail (id) MATCH SIMPLE 132 | ON UPDATE NO ACTION ON DELETE NO ACTION, 133 | ADD CONSTRAINT fk3737353bfa266c1e FOREIGN KEY (category_id) 134 | REFERENCES category (id) MATCH SIMPLE 135 | ON UPDATE NO ACTION ON DELETE NO ACTION; 136 | ALTER TABLE exportlink 137 | ADD CONSTRAINT fk834619eeed0b1afe FOREIGN KEY (document_id) 138 | REFERENCES "document" (id) MATCH SIMPLE 139 | ON UPDATE NO ACTION ON DELETE NO ACTION; 140 | 141 | CREATE SEQUENCE hibernate_sequence 142 | INCREMENT 1 143 | MINVALUE 1 144 | MAXVALUE 9223372036854775807 145 | START 1 146 | CACHE 1; 147 | 148 | # --- !Downs 149 | ALTER TABLE "comment" 150 | DROP CONSTRAINT fk9bde863fa7cd013e, 151 | DROP CONSTRAINT fk9bde863fed0b1afe; 152 | ALTER TABLE "document" 153 | DROP CONSTRAINT fk3737353b120f48d, 154 | DROP CONSTRAINT fk3737353bb2fabf16, 155 | DROP CONSTRAINT fk3737353bdf01b96, 156 | DROP CONSTRAINT fk3737353bfa266c1e; 157 | ALTER TABLE exportlink 158 | DROP CONSTRAINT fk834619eeed0b1afe; 159 | 160 | DROP TABLE category; 161 | DROP TABLE clonedocumentjobstatus; 162 | DROP TABLE "comment"; 163 | DROP TABLE "document"; 164 | DROP TABLE exportlink; 165 | DROP TABLE thumbnail; 166 | DROP TABLE user_; 167 | DROP SEQUENCE hibernate_sequence; -------------------------------------------------------------------------------- /public/javascripts/jquery.placeholder.js: -------------------------------------------------------------------------------- 1 | /*! http://mths.be/placeholder v2.0.7 by @mathias */ 2 | ;(function(window, document, $) { 3 | 4 | var isInputSupported = 'placeholder' in document.createElement('input'), 5 | isTextareaSupported = 'placeholder' in document.createElement('textarea'), 6 | prototype = $.fn, 7 | valHooks = $.valHooks, 8 | hooks, 9 | placeholder; 10 | 11 | if (isInputSupported && isTextareaSupported) { 12 | 13 | placeholder = prototype.placeholder = function() { 14 | return this; 15 | }; 16 | 17 | placeholder.input = placeholder.textarea = true; 18 | 19 | } else { 20 | 21 | placeholder = prototype.placeholder = function() { 22 | var $this = this; 23 | $this 24 | .filter((isInputSupported ? 'textarea' : ':input') + '[placeholder]') 25 | .not('.placeholder') 26 | .bind({ 27 | 'focus.placeholder': clearPlaceholder, 28 | 'blur.placeholder': setPlaceholder 29 | }) 30 | .data('placeholder-enabled', true) 31 | .trigger('blur.placeholder'); 32 | return $this; 33 | }; 34 | 35 | placeholder.input = isInputSupported; 36 | placeholder.textarea = isTextareaSupported; 37 | 38 | hooks = { 39 | 'get': function(element) { 40 | var $element = $(element); 41 | return $element.data('placeholder-enabled') && $element.hasClass('placeholder') ? '' : element.value; 42 | }, 43 | 'set': function(element, value) { 44 | var $element = $(element); 45 | if (!$element.data('placeholder-enabled')) { 46 | return element.value = value; 47 | } 48 | if (value == '') { 49 | element.value = value; 50 | // Issue #56: Setting the placeholder causes problems if the element continues to have focus. 51 | if (element != document.activeElement) { 52 | // We can't use `triggerHandler` here because of dummy text/password inputs :( 53 | setPlaceholder.call(element); 54 | } 55 | } else if ($element.hasClass('placeholder')) { 56 | clearPlaceholder.call(element, true, value) || (element.value = value); 57 | } else { 58 | element.value = value; 59 | } 60 | // `set` can not return `undefined`; see http://jsapi.info/jquery/1.7.1/val#L2363 61 | return $element; 62 | } 63 | }; 64 | 65 | isInputSupported || (valHooks.input = hooks); 66 | isTextareaSupported || (valHooks.textarea = hooks); 67 | 68 | $(function() { 69 | // Look for forms 70 | $(document).delegate('form', 'submit.placeholder', function() { 71 | // Clear the placeholder values so they don't get submitted 72 | var $inputs = $('.placeholder', this).each(clearPlaceholder); 73 | setTimeout(function() { 74 | $inputs.each(setPlaceholder); 75 | }, 10); 76 | }); 77 | }); 78 | 79 | // Clear placeholder values upon page reload 80 | $(window).bind('beforeunload.placeholder', function() { 81 | $('.placeholder').each(function() { 82 | this.value = ''; 83 | }); 84 | }); 85 | 86 | } 87 | 88 | function args(elem) { 89 | // Return an object of element attributes 90 | var newAttrs = {}, 91 | rinlinejQuery = /^jQuery\d+$/; 92 | $.each(elem.attributes, function(i, attr) { 93 | if (attr.specified && !rinlinejQuery.test(attr.name)) { 94 | newAttrs[attr.name] = attr.value; 95 | } 96 | }); 97 | return newAttrs; 98 | } 99 | 100 | function clearPlaceholder(event, value) { 101 | var input = this, 102 | $input = $(input); 103 | if (input.value == $input.attr('placeholder') && $input.hasClass('placeholder')) { 104 | if ($input.data('placeholder-password')) { 105 | $input = $input.hide().next().show().attr('id', $input.removeAttr('id').data('placeholder-id')); 106 | // If `clearPlaceholder` was called from `$.valHooks.input.set` 107 | if (event === true) { 108 | return $input[0].value = value; 109 | } 110 | $input.focus(); 111 | } else { 112 | input.value = ''; 113 | $input.removeClass('placeholder'); 114 | input == document.activeElement && input.select(); 115 | } 116 | } 117 | } 118 | 119 | function setPlaceholder() { 120 | var $replacement, 121 | input = this, 122 | $input = $(input), 123 | $origInput = $input, 124 | id = this.id; 125 | if (input.value == '') { 126 | if (input.type == 'password') { 127 | if (!$input.data('placeholder-textinput')) { 128 | try { 129 | $replacement = $input.clone().attr({ 'type': 'text' }); 130 | } catch(e) { 131 | $replacement = $('').attr($.extend(args(this), { 'type': 'text' })); 132 | } 133 | $replacement 134 | .removeAttr('name') 135 | .data({ 136 | 'placeholder-password': true, 137 | 'placeholder-id': id 138 | }) 139 | .bind('focus.placeholder', clearPlaceholder); 140 | $input 141 | .data({ 142 | 'placeholder-textinput': $replacement, 143 | 'placeholder-id': id 144 | }) 145 | .before($replacement); 146 | } 147 | $input = $input.removeAttr('id').hide().prev().attr('id', id).show(); 148 | // Note: `$input[0] != input` now! 149 | } 150 | $input.addClass('placeholder'); 151 | $input[0].value = $input.attr('placeholder'); 152 | } else { 153 | $input.removeClass('placeholder'); 154 | } 155 | } 156 | 157 | }(this, document, jQuery)); 158 | -------------------------------------------------------------------------------- /conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | GET /admin AdminController.index 6 | * /admin module:crud 7 | * / module:press 8 | 9 | 10 | #WEB 11 | GET /auth/google/code web.Auth.googleCode 12 | GET /auth/google/token web.Auth.googleToken 13 | GET /comments web.Comments.list 14 | GET /documents web.Documents.list 15 | GET /documents/add web.Documents.add 16 | GET /documents/{<[0-9]+>id}-{slug} web.Documents.read 17 | GET /documents/{<[0-9]+>id}/go web.Documents.go 18 | GET /documents/{<[0-9]+>id}/edit web.Documents.edit 19 | GET /discussions web.Discussions.list 20 | GET /discussions/add web.Discussions.add 21 | GET /discussions/{<[0-9]+>id}-{slug} web.Discussions.view 22 | GET /discussions/{<[0-9]+>id}/go web.Discussions.go 23 | GET /discussions/{<[0-9]+>id}/edit web.Discussions.edit 24 | POST /logout web.Auth.logout 25 | GET /users/{<[0-9]+>id} web.Users.read 26 | GET / web.WebController.index 27 | 28 | 29 | # ================== API ===================== 30 | GET /api/categories api.Categories.list 31 | GET /api/categories/{<[0-9]+>id}/documents api.Categories.listDocuments 32 | GET /api/categories/{<[0-9]+>id} api.Categories.read 33 | POST /api/categories api.Categories.create 34 | PUT /api/categories/{<[0-9]+>id} api.Categories.update 35 | DELETE /api/categories/{<[0-9]+>id} api.Categories.delete 36 | GET /api/documents api.Documents.list 37 | POST /api/documents api.Documents.create 38 | GET /api/documents/{<[0-9]+>id}/thumbnail api.Documents.readThumbnail 39 | PUT /api/documents/{<[0-9]+>id} api.Documents.update 40 | POST /api/documents/{<[0-9]+>id}/copy api.Documents.createCopy 41 | POST /api/documents/{<[0-9]+>id}/viewed api.Documents.markAsViewed 42 | DELETE /api/documents/{<[0-9]+>id} api.Documents.delete 43 | POST /api/documents/downloads/{<[0-9]+>exportLinkId} api.Documents.download 44 | GET /api/documentsjobsstatus/{<[0-9]+>id} api.DocumentsJobsStatus.read 45 | 46 | GET /api/discussions api.Discussions.list 47 | POST /api/discussions api.Discussions.create 48 | PUT /api/discussions/{<[0-9]+>id} api.Discussions.update 49 | POST /api/discussions/{<[0-9]+>id}/viewed api.Discussions.markAsViewed 50 | DELETE /api/discussions/{<[0-9]+>id} api.Discussions.delete 51 | POST /api/discussions/{<[0-9]+>id}/documents api.Discussions.tagDocument 52 | 53 | POST /api/discussionDocuments api.DiscussionDocuments.create 54 | DELETE /api/discussionDocuments api.DiscussionDocuments.delete 55 | 56 | POST /api/comments api.Comments.create 57 | DELETE /api/comments/{<[0-9]+>id} api.Comments.delete 58 | POST /api/votes api.Votes.create 59 | DELETE /api/votes/{<[0-9]+>id} api.Votes.delete 60 | GET /api/votes api.Votes.read 61 | 62 | 63 | * /api/* 404 64 | 65 | # Map static resources from the /app/public folder to the /public path 66 | GET /public/ staticDir:public 67 | 68 | # Ignore favicon requests 69 | GET /favicon.ico 404 70 | 71 | # Catch all 72 | * /{controller}/{action} {controller}.{action} 73 | -------------------------------------------------------------------------------- /app/views/web/Documents/list.html: -------------------------------------------------------------------------------- 1 | #{extends 'web.html' /} 2 | #{set title : 'Documents' /} 3 | #{i18n keys:['_.errorTryAgain', '_.loading'] /} 4 | 5 | #{if order == null} 6 | #{set 'title' } 7 | &{'Documents'} 8 | #{/set} 9 | #{/if} 10 | 11 | #{if keyword} 12 | #{set 'subTitle' } 13 | #{if get('subTitle')} 14 | #{get 'subTitle' /} - 15 | #{/if} 16 | '${keyword}' 17 | #{/set} 18 | #{/if} 19 | 20 | #{if category} 21 | #{set 'subTitle' } 22 | #{if get('subTitle')} 23 | #{get 'subTitle' /} - 24 | #{/if} 25 | ${category.name} 26 | #{/set} 27 | #{/if} 28 | 29 | #{if order} 30 | #{set 'subTitle' } 31 | #{if get('subTitle')} 32 | #{get 'subTitle' /} - 33 | #{/if} 34 | &{'document.list.order.' + order} 35 | #{/set} 36 | #{/if} 37 | 38 |
39 |
40 |
41 | #{include 'web/Documents/ajax_list.html' /} 42 |
43 |
44 |
45 |
46 |

Search Filter

47 | #{form @web.Documents.list()} 48 | 49 | 50 | #{/form} 51 |

52 | #{if keyword} 53 | &{'document.list.clearSearchOptions'} 54 | #{/if} 55 |

56 |
57 | 97 |
98 |

&{'_.categories'}

99 |
    100 |
  • 101 | #{a @web.Documents.list(keyword, null, order, null)}&{'_.allCategories'}#{/a} 102 |
  • 103 | #{list categories} 104 |
  • 105 | #{a @web.Documents.list(keyword, _.id, order, null)}${_.name}#{/a} 106 |
  • 107 | #{/list} 108 |
109 |
110 |
111 |
112 | 113 | #{set 'moreScripts'} 114 | 139 | #{/set} -------------------------------------------------------------------------------- /public/javascripts/jquery.timeago.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Timeago is a jQuery plugin that makes it easy to support automatically 3 | * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). 4 | * 5 | * @name timeago 6 | * @version 0.11.4 7 | * @requires jQuery v1.2.3+ 8 | * @author Ryan McGeary 9 | * @license MIT License - http://www.opensource.org/licenses/mit-license.php 10 | * 11 | * For usage and examples, visit: 12 | * http://timeago.yarp.com/ 13 | * 14 | * Copyright (c) 2008-2012, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org) 15 | */ 16 | (function($) { 17 | $.timeago = function(timestamp) { 18 | if (timestamp instanceof Date) { 19 | return inWords(timestamp); 20 | } else if (typeof timestamp === "string") { 21 | return inWords($.timeago.parse(timestamp)); 22 | } else if (typeof timestamp === "number") { 23 | return inWords(new Date(timestamp)); 24 | } else { 25 | return inWords($.timeago.datetime(timestamp)); 26 | } 27 | }; 28 | var $t = $.timeago; 29 | 30 | $.extend($.timeago, { 31 | settings: { 32 | refreshMillis: 60000, 33 | allowFuture: false, 34 | strings: { 35 | prefixAgo: null, 36 | prefixFromNow: null, 37 | suffixAgo: "ago", 38 | suffixFromNow: "from now", 39 | seconds: "less than a minute", 40 | minute: "about a minute", 41 | minutes: "%d minutes", 42 | hour: "about an hour", 43 | hours: "about %d hours", 44 | day: "a day", 45 | days: "%d days", 46 | month: "about a month", 47 | months: "%d months", 48 | year: "about a year", 49 | years: "%d years", 50 | wordSeparator: " ", 51 | numbers: [] 52 | } 53 | }, 54 | inWords: function(distanceMillis) { 55 | var $l = this.settings.strings; 56 | var prefix = $l.prefixAgo; 57 | var suffix = $l.suffixAgo; 58 | if (this.settings.allowFuture) { 59 | if (distanceMillis < 0) { 60 | prefix = $l.prefixFromNow; 61 | suffix = $l.suffixFromNow; 62 | } 63 | } 64 | 65 | var seconds = Math.abs(distanceMillis) / 1000; 66 | var minutes = seconds / 60; 67 | var hours = minutes / 60; 68 | var days = hours / 24; 69 | var years = days / 365; 70 | 71 | function substitute(stringOrFunction, number) { 72 | var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; 73 | var value = ($l.numbers && $l.numbers[number]) || number; 74 | return string.replace(/%d/i, value); 75 | } 76 | 77 | var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || 78 | seconds < 90 && substitute($l.minute, 1) || 79 | minutes < 45 && substitute($l.minutes, Math.round(minutes)) || 80 | minutes < 90 && substitute($l.hour, 1) || 81 | hours < 24 && substitute($l.hours, Math.round(hours)) || 82 | hours < 42 && substitute($l.day, 1) || 83 | days < 30 && substitute($l.days, Math.round(days)) || 84 | days < 45 && substitute($l.month, 1) || 85 | days < 365 && substitute($l.months, Math.round(days / 30)) || 86 | years < 1.5 && substitute($l.year, 1) || 87 | substitute($l.years, Math.round(years)); 88 | 89 | var separator = $l.wordSeparator === undefined ? " " : $l.wordSeparator; 90 | return $.trim([prefix, words, suffix].join(separator)); 91 | }, 92 | parse: function(iso8601) { 93 | var s = $.trim(iso8601); 94 | s = s.replace(/\.\d+/,""); // remove milliseconds 95 | s = s.replace(/-/,"/").replace(/-/,"/"); 96 | s = s.replace(/T/," ").replace(/Z/," UTC"); 97 | s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 98 | return new Date(s); 99 | }, 100 | datetime: function(elem) { 101 | var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title"); 102 | return $t.parse(iso8601); 103 | }, 104 | isTime: function(elem) { 105 | // jQuery's `is()` doesn't play well with HTML5 in IE 106 | return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); 107 | } 108 | }); 109 | 110 | $.fn.timeago = function() { 111 | var self = this; 112 | self.each(refresh); 113 | 114 | var $s = $t.settings; 115 | if ($s.refreshMillis > 0) { 116 | setInterval(function() { self.each(refresh); }, $s.refreshMillis); 117 | } 118 | return self; 119 | }; 120 | 121 | function refresh() { 122 | var data = prepareData(this); 123 | if (!isNaN(data.datetime)) { 124 | $(this).text(inWords(data.datetime)); 125 | } 126 | return this; 127 | } 128 | 129 | function prepareData(element) { 130 | element = $(element); 131 | if (!element.data("timeago")) { 132 | element.data("timeago", { datetime: $t.datetime(element) }); 133 | var text = $.trim(element.text()); 134 | if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) { 135 | element.attr("title", text); 136 | } 137 | } 138 | return element.data("timeago"); 139 | } 140 | 141 | function inWords(date) { 142 | return $t.inWords(distance(date)); 143 | } 144 | 145 | function distance(date) { 146 | return (new Date().getTime() - date.getTime()); 147 | } 148 | 149 | // fix for IE6 suckage 150 | document.createElement("abbr"); 151 | document.createElement("time"); 152 | }(jQuery)); 153 | -------------------------------------------------------------------------------- /app/controllers/api/Documents.java: -------------------------------------------------------------------------------- 1 | package controllers.api; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.util.Date; 7 | import java.util.List; 8 | 9 | import jobs.CopyDocumentJob; 10 | import jobs.IncrementDocumentDownloadCountJob; 11 | import models.Category; 12 | import models.Comment; 13 | import models.DiscussionDocument; 14 | import models.Document; 15 | import models.DocumentJobStatus; 16 | import models.ExportLink; 17 | import models.Vote; 18 | import models.User; 19 | import models.enums.Mime; 20 | import play.Logger; 21 | import play.data.Upload; 22 | import play.data.validation.Required; 23 | import play.data.validation.Valid; 24 | import play.mvc.With; 25 | import controllers.AppController; 26 | import controllers.web.Auth; 27 | 28 | @With(ApiController.class) 29 | public class Documents extends AppController { 30 | 31 | public static void create(@Required Document document, @Required Upload file) { 32 | User me = Auth.getMe(); 33 | if (!validation.hasErrors()) { 34 | document.setMime(Mime.parseName(file.getContentType())); 35 | try { 36 | document.owner = me; 37 | if (validation.valid(document).ok) { 38 | document.uploadToGoogleDriveAndSave(file); 39 | } 40 | renderJSON(document); 41 | } catch (IOException e) { 42 | Logger.error("Could not insert file on Google Drive :" + "\n\t File Name : %s" + "\n\t File Size : %s" + "\n\t File Owner ID : %s" + "\n\t Exception Message : %s", file.getFileName(), file.getSize(), me.id, e.getMessage()); 43 | response.status = 500; 44 | renderJSON(e.getMessage()); 45 | } 46 | 47 | } 48 | } 49 | 50 | public static void createCopy(long id) { 51 | checkAuthenticity(); 52 | User me = Auth.getMe(); 53 | Document document = Document.findById(id); 54 | notFoundIfNull(document); 55 | if (me != document.owner) { 56 | DocumentJobStatus documentJobStatus = new DocumentJobStatus(); 57 | documentJobStatus.save(); 58 | new CopyDocumentJob(documentJobStatus.id, document.id, me.id).in(1); 59 | renderJSON(documentJobStatus); 60 | } else { 61 | error(); 62 | } 63 | } 64 | 65 | public static void delete(long id) { 66 | checkAuthenticity(); 67 | User me = Auth.getMe(); 68 | Document document = Document.findById(id); 69 | notFoundIfNull(document); 70 | if (document.owner.id == me.id) { 71 | Vote.deleteAllForDocument(document.id); 72 | document.delete(); 73 | renderJSON(true); 74 | } else { 75 | unauthorized(); 76 | } 77 | } 78 | 79 | public static void download(long exportLinkId) { 80 | checkAuthenticity(); 81 | ExportLink exportLink = ExportLink.findById(exportLinkId); 82 | notFoundIfNull(exportLink); 83 | new IncrementDocumentDownloadCountJob(exportLink.document.id).in(30); 84 | redirect(exportLink.link); 85 | } 86 | 87 | public static void list(String keyword, long categoryId, String order, Integer page) { 88 | List documents = Document.search(keyword, categoryId, order, page); 89 | renderJSON(documents); 90 | } 91 | 92 | public static void markAsViewed(long id) { 93 | checkAuthenticity(); 94 | Document document = Document.findById(id); 95 | if (document != null) { 96 | String sessionKey = String.format("lastViewedId", id); 97 | 98 | if (session.get(sessionKey) != null) { 99 | if (session.get(sessionKey) != document.id.toString()) { 100 | document.incrementViewCountAndSave(); 101 | session.put(sessionKey, document.id); 102 | } 103 | } else { 104 | document.incrementViewCountAndSave(); 105 | session.put(sessionKey, new Date().getTime()); 106 | } 107 | } else { 108 | notFound(); 109 | } 110 | ok(); 111 | } 112 | 113 | public static void read(long id) { 114 | Document document = Document.findById(id); 115 | notFoundIfNull(document); 116 | renderJSON(document); 117 | } 118 | 119 | public static void readThumbnail(long id) { 120 | Document document = Document.findById(id); 121 | if (document != null) { 122 | if (document.thumbnail != null) { 123 | ByteArrayInputStream thumbnailStream = new ByteArrayInputStream(document.thumbnail.image); 124 | if (thumbnailStream != null) { 125 | response.cacheFor("120mn"); 126 | renderBinary(thumbnailStream); 127 | } 128 | } 129 | File defaultThumbnail = new File("public/images/doc.png"); 130 | renderBinary(defaultThumbnail); 131 | } 132 | notFound(); 133 | } 134 | 135 | public static void update(long id, @Required Document document) { 136 | checkAuthenticity(); 137 | User me = Auth.getMe(); 138 | Document check = Document.findById(id); 139 | notFoundIfNull(check); 140 | if (check.owner.id == me.id) { 141 | if (!validation.hasErrors()) { 142 | check.title = document.title; 143 | check.category = document.category; 144 | check.source = document.source; 145 | check.description = document.description; 146 | check.tags = document.tags; 147 | check.owner = me; 148 | document = check; 149 | if (document.validateAndSave()) { 150 | renderJSON(document); 151 | } 152 | } 153 | } else { 154 | unauthorized(); 155 | } 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /conf/messages.en: -------------------------------------------------------------------------------- 1 | _.account = Account 2 | _.allCategories = All Categories 3 | _.cancel = Cancel 4 | _.categories = Categories 5 | _.comments = Comments 6 | _.commentsIconHelp = Number of comments 7 | _.copies = Copies 8 | _.copiesIconHelp = Number of copies 9 | _.delete = Delete 10 | _.discuss = Discuss 11 | _.discuss.expand = Give feedback, ask questions, leave a comment, start a conversation. 12 | _.discussions = Discussions 13 | _.discussionsIconHelp = Number of discussions 14 | _.documents = Documents 15 | _.documentsIconHelp = Number of linked documents 16 | _.downloads = Downloads 17 | _.downloadsIconHelp = Number of downloads 18 | _.emptyList = No results 19 | _.enterKeyword = Type a word here 20 | _.error = An error occurred! 21 | _.error.title = Error! 22 | _.errorTryAgain = Error! Please Try Again. 23 | _.explore = Explore 24 | _.explore.expand = Browse interesting documents & copy them to your Google Drive. 25 | _.filterResults = Filter Results 26 | _.home = Home 27 | _.loading = Loading... 28 | _.newestMembers = Newest Members 29 | _.next = Next 30 | _.openDocsHubProject = The Open Docs Hub Project 31 | _.order = Order 32 | _.prev = Prev 33 | _.recentDocuments = Recent Documents 34 | _.save = Save 35 | _.search = Search 36 | _.searchDocuments = Search Documents 37 | _.searchDiscussions = Search Discussions 38 | _.share = Share 39 | _.share.expand = Upload a document and share it with the community. 40 | _.showMore = Show More 41 | _.startedBy = started by 42 | _.startDiscussion = Start a discussion 43 | _.sorry = Sorry! 44 | _.success.title = Success! 45 | _.unlike = Unlike 46 | _.unvote = Cancel Vote 47 | _.uploadDocument = Share a document 48 | _.views = Views 49 | _.viewsIconHelp = Number of views 50 | _.vote = Vote 51 | _.votes = Votes 52 | _.votesIconHelp = Number of votes 53 | _.welcome = Welcome to %s 54 | _.welcomeMessage = Explore, share and discuss documents. 55 | _.you = You 56 | application.description = Read, share & discuss open documents and data. 57 | application.welcomeTitle = Welcome to %s 58 | auth.login = Sign in with Google + 59 | auth.login.needed = Please sign in :-) 60 | auth.login.success = Welcome %s 61 | auth.logout = Logout 62 | auth.logout.success = Good Bye %s, See you soon :-) 63 | auth.oauth.error = An error occurred. We could not sign in with Google + 64 | auth.unauthorizedAccess = You are not authorized to access this resource 65 | comment.add = Leave a comment 66 | comment.auth.login.needed = Please sign in to post a comment. 67 | comment.deleted = Your comment has been deleted. 68 | comment.list.empty = There are no comments yet. 69 | comment.post = Post my comment 70 | comment.reply = Reply 71 | discussion.about = About this discussion 72 | discussion.category.id = Category 73 | discussion.content = Content 74 | discussion.create = Create discussion 75 | discussion.created = Created on 76 | discussion.create.success = New discussion saved. 77 | discussion.create.error = We could not create your new discussion. 78 | discussion.delete = Delete discussion 79 | discussion.delete.error = Discussion not deleted! 80 | discussion.delete.success = Discussion deleted! 81 | discussion.edit = Edit 82 | discussion.postComment = Post a comment 83 | discussion.startedBy = Started by 84 | discussion.tags = Tags 85 | discussion.title = Title 86 | discussion.update = Update discussion 87 | discussion.update.error = Discussion not updated! 88 | discussion.update.success = Discussion updated! 89 | document.about = About this document 90 | document.category = Category 91 | document.category.id = Category 92 | document.copy.error = An error occurred. We were not able to copy this document to your Google Drive. Please try again. 93 | document.copy.success = A copy of this document has been uploaded to your Google Drive. 94 | document.copyToGoogleDrive = Copy to Google Drive 95 | document.created = Upload date 96 | document.delete = Delete this document 97 | document.delete.error = We could not delete your document. Please try again. 98 | document.delete.success = Your document has been deleted. 99 | document.description = Description 100 | document.description = Description 101 | document.download = Download 102 | document.edit = Edit details 103 | document.editDetails = Edit details 104 | document.fileSize = File Size 105 | document.list.clearSearchOptions = Clear search options 106 | document.list.end = No results 107 | document.list.order.comments = Most commented 108 | document.list.order.copies = Most copied 109 | document.list.order.discussions = Most discussed 110 | document.list.order.downloads = Most downloaded 111 | document.list.order.recent = Newest 112 | document.list.order.views = Most viewed 113 | document.list.order.votes = Most voted 114 | document.mimeType = Format 115 | document.openOnGoogleDrive = Open on Google Drive 116 | document.previewNotAvailable = A preview of this document is not available 117 | document.postComment = Post a comment 118 | document.share = Share 119 | document.shareAnother = Share another document 120 | document.share.text = I am reading %s 121 | document.sharedBy = Shared by 122 | document.source = Source 123 | document.source.help = Is the document accurate? What is the source? 124 | document.startDiscussion = Start a discussion 125 | document.stats = Stats 126 | document.tags = Tags 127 | document.title = Title 128 | document.update = Update document 129 | document.update.error = We could not update your document. Please try again. 130 | document.update.success = Your document has been updated. 131 | document.updatedNotice = Your document details have been saved. 132 | document.upload = Upload Document 133 | document.upload.error = We could not upload your document. Please try again. 134 | document.upload.notice = A copy of this file will be uploaded to your Google Drive and shared on this website. 135 | document.upload.submit = Upload ! 136 | document.upload.success = Your document has been uploaded. 137 | document.upload.thumbnail.notice = The thumbnail for this document will be generated in a few minutes. Meanwhile you can keep using the website. 138 | document.view = View this document 139 | error.notice = An error occurred. Please try again. 140 | file = File 141 | tags.help = Separate them with a comma. 142 | read.document.on = Read %s on -------------------------------------------------------------------------------- /app/views/web.html.bak: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #{get 'title' /}#{if get('subTitle')} - #{get 'subTitle' /}#{/if} - ${play.configuration.getProperty('application.fullName')} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | #{press.stylesheet 'bootstrap.min.css' /} 20 | #{press.stylesheet 'web.css' /} 21 | #{press.compressed-stylesheet /} 22 | 23 | 24 | 27 | 28 | 29 | 30 | 41 | 42 |
43 |
44 | 49 |
50 | #{form @web.Webcontroller.search(), class : 'form-search pull-right'} 51 | 52 | 56 | 59 | #{/form} 60 |
61 |
62 |
63 | 66 |
67 |
68 | #{if flash?.error || flash?.success} 69 |
70 |
71 | #{if flash?.error} 72 |
73 | ${flash?.error.raw()} 74 |
75 | #{/if} 76 | #{if flash?.success} 77 |
78 | ${flash?.success.raw()} 79 |
80 | #{/if} 81 |
82 |
83 | #{/if} 84 | #{if request.url != '/'} 85 | 92 | #{/if} 93 | #{doLayout /} 94 |
95 |
96 | 104 | 105 | 106 | 107 | #{press.script 'jquery.timeago.js' /} 108 | #{press.script 'jquery.timeago.' + lang + '.js' /} 109 | #{press.script 'jquery-form-3.10.js' /} 110 | #{press.script 'json2.js' /} 111 | #{press.compressed-script /} 112 | 118 | #{get 'moreScripts' /} 119 | 120 | -------------------------------------------------------------------------------- /app/views/web.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | #{get 'title' /}#{if get('subTitle')} - #{get 'subTitle' /}#{/if} - ${siteName} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | #{press.stylesheet 'foundation.min.css' /} 23 | #{press.stylesheet 'icons.css' /} 24 | #{press.stylesheet 'joyride-2.0.1.css' /} 25 | #{press.stylesheet 'app.css' /} 26 | #{press.compressed-stylesheet /} 27 | 28 | 31 | 32 | 33 | 49 |
50 |
51 | #{if flash?.error || flash?.success} 52 |
53 |
54 | #{if flash?.error} 55 |
56 | ${flash?.error.raw()} 57 |
58 | #{/if} 59 | #{if flash?.success} 60 |
61 | ${flash?.success.raw()} 62 |
63 | #{/if} 64 |
65 |
66 | #{/if} 67 | #{if request.url != '/'} 68 |
69 |
70 |

${get('title')?.toLowerCase()?.raw()} 71 | #{if get('subTitle')} 72 | :: #{get 'subTitle' /} 73 | #{/if} 74 |

75 |
76 |
77 | #{/if} 78 |
79 |
80 | #{doLayout /} 81 |
82 |
83 |
84 |
85 | 94 | 95 | #{get 'modalDialogs' /} 96 |
97 |

Hello there!

98 |

Join our community!

99 |

100 | #{a @web.Auth.googleCode(request.url), class : 'button large success'} 101 | 102 | &{'auth.login'} 103 | #{/a} 104 |

105 | × 106 |
107 | 108 | #{press.script 'foundation.min.js' /} 109 | #{press.script 'app.js' /} 110 | #{press.script 'jquery.joyride-2.0.1.js' /} 111 | #{press.script 'jquery.cookie.js' /} 112 | #{press.script 'bootstrap.button.js' /} 113 | #{press.script 'jquery.timeago.js' /} 114 | #{press.script 'jquery.timeago.' + lang + '.js' /} 115 | #{press.script 'jquery-form-3.10.js' /} 116 | #{press.script 'json2.js' /} 117 | #{press.compressed-script /} 118 | 129 | #{get 'moreScripts' /} 130 | 131 | --------------------------------------------------------------------------------