--- a/.hgignore Thu May 24 11:19:26 2012 +0200
+++ b/.hgignore Wed May 30 22:51:02 2012 +0200
@@ -7,3 +7,4 @@
src/main/webapp/img/logo*.png
tags
pgsql
+attachments
--- a/src/main/resources/db/db-schema.sql Thu May 24 11:19:26 2012 +0200
+++ b/src/main/resources/db/db-schema.sql Wed May 30 22:51:02 2012 +0200
@@ -160,6 +160,19 @@
"contact" bigint not null,
"entity" bigint not null
);
+create table "attachment" (
+ "name" varchar(100) not null,
+ "updated_at" timestamp not null,
+ "id" bigint primary key not null,
+ "mime_type" varchar(80) not null,
+ "content_id" varchar(20) not null,
+ "size" bigint not null,
+ "note" varchar(10240),
+ "created_at" timestamp not null,
+ "created_by" bigint,
+ "updated_by" bigint
+ );
+create sequence "attachment_id_seq";
create table "project" (
"id" bigint primary key not null,
"name" varchar(100) not null,
@@ -210,6 +223,14 @@
"location" bigint not null,
"project" bigint not null
);
+create table "project_attachment" (
+ "project" bigint not null,
+ "attachment" bigint not null
+ );
+create table "task_attachment" (
+ "task" bigint not null,
+ "attachment" bigint not null
+ );
create table "service" (
"name" varchar(100) not null,
"updated_at" timestamp not null,
@@ -262,10 +283,16 @@
alter table "project_company" add foreign key ("company") references "company"("id") on delete cascade;
alter table "project_location" add foreign key ("project") references "project"("id") on delete cascade;
alter table "project_location" add foreign key ("location") references "location"("id") on delete cascade;
+alter table "project_attachment" add foreign key ("project") references "project"("id") on delete cascade;
+alter table "project_attachment" add foreign key ("attachment") references "attachment"("id") on delete cascade;
+alter table "task_attachment" add foreign key ("task") references "task"("id") on delete cascade;
+alter table "task_attachment" add foreign key ("attachment") references "attachment"("id") on delete cascade;
-- composite key indexes :
alter table "company_contact" add unique("entity","contact");
alter table "user_contact" add unique("entity","contact");
alter table "project_company" add unique("project","company");
alter table "project_location" add unique("project","location");
+alter table "project_attachment" add unique("project","attachment");
+alter table "task_attachment" add unique("task","attachment");
-- column group indexes :
create index "user_deleted_active_idx" on "user" ("deleted","active");
--- a/src/main/resources/db/schema-changes-0.2-0.3.sql Thu May 24 11:19:26 2012 +0200
+++ b/src/main/resources/db/schema-changes-0.2-0.3.sql Wed May 30 22:51:02 2012 +0200
@@ -41,3 +41,31 @@
alter table "service_payment" add foreign key ("period") references "code_list_item"("id");
alter table "service_payment" add foreign key ("currency") references "code_list_item"("id");
+-- attachments
+create table "attachment" (
+ "name" varchar(100) not null,
+ "updated_at" timestamp not null,
+ "id" bigint primary key not null,
+ "mime_type" varchar(80) not null,
+ "note" varchar(10240),
+ "content_id" varchar(20) not null,
+ "size" bigint not null,
+ "created_at" timestamp not null,
+ "created_by" bigint,
+ "updated_by" bigint
+ );
+create sequence "attachment_id_seq";
+create table "project_attachment" (
+ "project" bigint not null,
+ "attachment" bigint not null
+ );
+create table "task_attachment" (
+ "task" bigint not null,
+ "attachment" bigint not null
+ );
+alter table "project_attachment" add foreign key ("project") references "project"("id") on delete cascade;
+alter table "project_attachment" add foreign key ("attachment") references "attachment"("id") on delete cascade;
+alter table "task_attachment" add foreign key ("task") references "task"("id") on delete cascade;
+alter table "task_attachment" add foreign key ("attachment") references "attachment"("id") on delete cascade;
+alter table "project_attachment" add unique("project","attachment");
+alter table "task_attachment" add unique("task","attachment");
--- a/src/main/resources/default.props Thu May 24 11:19:26 2012 +0200
+++ b/src/main/resources/default.props Wed May 30 22:51:02 2012 +0200
@@ -10,3 +10,6 @@
datatables.rows=50
# lang defs
datatables.lang.cs=/jquery/js/dataTables.cs.txt
+
+# Attachments
+dir.attachment=attachments
--- a/src/main/scala/bootstrap/liftweb/Boot.scala Thu May 24 11:19:26 2012 +0200
+++ b/src/main/scala/bootstrap/liftweb/Boot.scala Wed May 30 22:51:02 2012 +0200
@@ -20,10 +20,13 @@
import fis.aaa.model._
import fis.aaa.ui._
import fis.crm.ui._
+import fis.fs.lib._
+import fis.fs.model._
import fis.geo.ui._
import fis.pm.ui._
import fis.sr.ui._
import fis.db.SquerylTxMgr
+import java.io.File
import net.datatables.DataTables
import net.liftweb.common._
import net.liftweb.http._
@@ -78,6 +81,17 @@
/* Language */
LangSwitcher.init()
LiftRules.localeCalculator = { _ => CurLanguage.get }
+
+ /* Attachment */
+ for {
+ dn <- Props get "dir.attachment"
+ } {
+ val r = new FileSystemAttachmentStore(new File(dn))
+ Attachment.repo.default.set(Vendor(Full(r)))
+ LiftRules.maxMimeSize = 80 * 1024 * 1024
+ LiftRules.maxMimeFileSize = 70 * 1024 * 1024
+ }
+
}
}
--- a/src/main/scala/fis/base/ui/SecNav.scala Thu May 24 11:19:26 2012 +0200
+++ b/src/main/scala/fis/base/ui/SecNav.scala Wed May 30 22:51:02 2012 +0200
@@ -84,4 +84,13 @@
}) toSeq
}
+object NavLink {
+ def apply[T](l: Loc[T])(v: T): NodeSeq = {
+ l.testAccess match {
+ case Left(true) => <a href={l.calcHref(v)}>{l.linkText(v)}</a>
+ case _ => NodeSeq.Empty
+ }
+ }
+}
+
// vim: set ts=2 sw=2 et:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/fis/fs/lib/AttachmentStore.scala Wed May 30 22:51:02 2012 +0200
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2010-2012 Tomas Zeman <tzeman@volny.cz>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * (Originaly based on net.backup project, (c) of Tomas Zeman).
+ */
+package fis.fs.lib
+
+import java.io.InputStream
+import net.liftweb.common.{Box, Empty, Failure, Full, LazyLoggable}
+
+/**
+ * Generic interface contract for content retrieval/storage.
+ */
+trait AttachmentStore {
+
+ /**
+ * Stores content to repository.
+ * @param content Content stream.
+ * @return Returns content id and length.
+ */
+ def put(content: InputStream): Box[(String, Long)]
+
+ /**
+ * Reads content stream from repository.
+ * @param id Content id.
+ * @param block Function on input stream.
+ * @return T func result.
+ */
+ def get[T](id: String)(block: InputStream => T): Box[T]
+
+ /**
+ * Reads content stream from repository.
+ * Called must close the stream afterwards.
+ * @param id Content id.
+ * @return Content stream.
+ */
+ def getContent(id: String): Box[InputStream]
+
+ /**
+ * Removes content from repository.
+ * @param id Content id.
+ * @return True if content was successfuly removed.
+ */
+ def remove(id: String): Boolean
+
+ /**
+ * Checks if content <code>id</code> is in the repository.
+ * @param id Content id.
+ * @return True if content <code>id</code> is in the repository.
+ */
+ def exists(id: String): Boolean
+}
+
+import java.io.{BufferedInputStream, File, FileInputStream, FileOutputStream}
+import java.nio.channels.Channels
+import java.util.concurrent.atomic.AtomicLong
+import net.liftweb.util.Helpers._
+
+/**
+ * Filesystem based attachment store.
+ * @param baseDir Base directory. Attempts to create if does not exist.
+ */
+class FileSystemAttachmentStore(baseDir: File) extends AttachmentStore
+ with LazyLoggable {
+
+ /* C'tor */
+ if (!baseDir.exists) {
+ logger.info("Creating " + baseDir)
+ baseDir.mkdirs
+ }
+
+ if (baseDir.exists && !baseDir.isDirectory)
+ throw new IllegalArgumentException("baseDir %s must be a directory.".format(
+ baseDir.toString))
+
+ logger.info("Attachment store initialized in %s".format(baseDir.toString))
+
+ /* AttachmentStore implementation methods. */
+
+ override def put(content: InputStream): Box[(String, Long)] = for {
+ (tmp, os) <- tryo {
+ val f = File.createTempFile(".fscs.", ".tmp", baseDir)
+ val os = new FileOutputStream(f)
+ (f, os)
+ }
+ len <- use(os) {
+ os getChannel() transferFrom(Channels newChannel content, 0, Long.MaxValue)
+ }
+ (id, f, renamed) <- tryo {
+ val s = serial.incrementAndGet.toString
+ val f = new File(baseDir, s)
+ (s, f, tmp renameTo f)
+ } if renamed
+ } yield (id, len)
+
+ def getContent(id: String): Box[InputStream] = withFile(id) { f =>
+ new BufferedInputStream(new FileInputStream(f)) }
+
+ def get[T](id: String)(block: InputStream => T): Box[T] =
+ withStream(id)(block)
+
+ override def remove(id: String): Boolean =
+ withFile(id)(_.delete) openOr false
+
+ override def exists(id: String): Boolean =
+ withFile(id)(_.exists) openOr false
+
+ /* Implementation private. */
+
+ def use[T](c: { def close(): Unit })(block: => T): Box[T] = {
+ val r = tryo(block)
+ tryo(c.close)
+ r
+ }
+
+ protected def withFile[T](id: String)(block: File => T): Box[T] = for {
+ f <- getFile(id)
+ r <- tryo(block(f))
+ } yield r
+
+ protected def withStream[T](id: String)(block: InputStream => T): Box[T] = (for {
+ f <- getFile(id)
+ is <- tryo { new BufferedInputStream(new FileInputStream(f)) }
+ } yield {
+ logger.debug("withStream(%s)".format(id))
+ val r = tryo(block(is))
+ tryo { is.close }
+ r
+ }) flatMap { v => v }
+
+ protected def sanitizeId(id: String): Box[String] = for {
+ i <- (Box !! id) if !i.exists(!_.isDigit)
+ } yield i
+
+ protected def getFile(id: String): Box[File] = for {
+ i <- sanitizeId(id)
+ } yield new File(baseDir, i)
+
+ private lazy val serial = new AtomicLong(millis)
+}
+
+
+// vim: set ts=2 sw=2 et:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/fis/fs/model/Attachment.scala Wed May 30 22:51:02 2012 +0200
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2012 Tomas Zeman <tzeman@volny.cz>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package fis.fs.model
+
+import fis.base.model.{Entity, MetaEntity, ReadOnlyField}
+import fis.fs.lib.AttachmentStore
+import java.io.InputStream
+import net.liftweb.common._
+import net.liftweb.http.FileParamHolder
+import net.liftweb.record.{MetaRecord, Record}
+import net.liftweb.record.field._
+import net.liftweb.squerylrecord.RecordTypeMode._
+import net.liftweb.util._
+import net.tz.lift.model.{FieldLabel => FL}
+import org.squeryl.annotations.Column
+import scala.xml.Text
+
+class Attachment private() extends Record[Attachment] with Entity[Attachment] {
+
+ def meta = Attachment
+
+ @Column("content_id")
+ protected val contentIdFld = new StringField(this, 20)
+ @Column("size")
+ protected val sizeFld = new LongField(this) with FL {
+ override def asHtml = {
+ val s = get
+ Text(if (s > 1024) "%2.1f kB".format(s.toFloat / 1024) else s + " B")
+ }
+ }
+ val mimeType = new StringField(this, 80) with FL
+
+ lazy val size = ReadOnlyField(sizeFld)
+
+ def contentSize = sizeFld.get
+
+ def getContent: Box[InputStream] = meta.getContent(this)
+ def get[T](block: InputStream => T): Box[T] = meta.get(this)(block)
+ def store(fph: FileParamHolder): Box[Attachment] = meta.store(this, fph)
+ def remove: Box[Boolean] = meta.remove(this)
+
+ def headers = {
+ val n = name.get
+ ("Content-type" -> mimeType.get) ::
+ ("Content-length" -> sizeFld.get.toString) ::
+ ("Content-disposition" ->
+ "attachment; filename=\"%s\"; filename*=utf-8''%s".
+ format(n, Helpers.urlEncode(n))) :: Nil
+ }
+}
+
+object Attachment extends Attachment with MetaRecord[Attachment] with SimpleInjector {
+
+ object repo extends Inject[Box[AttachmentStore]](Empty)
+
+ def getContent(a: Attachment): Box[InputStream] = for {
+ r <- repo.vend
+ is <- r.getContent(a.contentIdFld.get)
+ } yield is
+
+ def get[T](a: Attachment)(block: InputStream => T): Box[T] = for {
+ r <- repo.vend
+ rv <- r.get(a.contentIdFld.get)(block)
+ } yield rv
+
+ def store(a: Attachment, is: InputStream): Box[Attachment] = {
+ for {
+ r <- repo.vend
+ (id, len) <- r put is
+ } yield a.sizeFld(len).contentIdFld(id)
+ }
+
+ def store(a: Attachment, fph: FileParamHolder): Box[Attachment] = {
+ for {
+ rv <- store(a, fph.fileStream)
+ } yield rv.name(fph.fileName).mimeType(fph.mimeType)
+ }
+
+ def remove(a: Attachment): Box[Boolean] = for {
+ r <- repo.vend
+ } yield r remove a.contentIdFld.get
+}
+
+// vim: set ts=2 sw=2 et:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/fis/fs/model/AttachmentCrud.scala Wed May 30 22:51:02 2012 +0200
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2012 Tomas Zeman <tzeman@volny.cz>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package fis.fs.model
+
+import fis.base.model.RecordCrud
+
+trait AttachmentCrud extends RecordCrud[Attachment] {
+ val table = FsSchema.attachmentT
+}
+
+object AttachmentCrud extends AttachmentCrud
+
+
+
+// vim: set ts=2 sw=2 et:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/fis/fs/model/FsSchema.scala Wed May 30 22:51:02 2012 +0200
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2012 Tomas Zeman <tzeman@volny.cz>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package fis.fs.model
+
+import fis.base.model.BaseSchema
+import net.liftweb.squerylrecord.RecordTypeMode._
+
+trait FsSchema extends BaseSchema {
+
+ val attachmentT = tableWithSeq[Attachment]
+}
+
+object FsSchema extends FsSchema
+
+// vim: set ts=2 sw=2 et:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/fis/fs/ui/AttachmentForm.scala Wed May 30 22:51:02 2012 +0200
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2012 Tomas Zeman <tzeman@volny.cz>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package fis.fs.ui
+
+import fis.base.ui._
+import fis.fs.model._
+import net.liftweb.common._
+import net.liftweb.http._
+import net.liftweb.util._
+import net.tz.lift.model._
+
+abstract class AttachmentForm extends HorizontalScreen with CancelButton with
+ SaveButton with AttachmentCrud {
+
+ protected object attachment extends ScreenVar[Attachment](
+ Attachment.createRecord)
+
+ override def hasUploadField = true
+
+ protected val file = makeField[Box[FileParamHolder], Nothing](l10n("File"),
+ Empty, field => SHtml.fileUpload(fph => field.set(Full(fph))),
+ NothingOtherValueInitializer)
+
+ protected def onSuccess(a: Attachment): Unit
+
+ def finish() { for {
+ fph <- file.get
+ a1 <- attachment.store(fph)
+ a2 <- save(a1)
+ } {
+ onSuccess(a2)
+ }}
+}
+
+abstract class DeleteAttachmentForm extends HorizontalScreen with CancelButton
+ with DeleteButton with AttachmentCrud {
+
+ protected def getAttachment: Box[Attachment]
+
+ val confirm = field(l10n("Really delete this attachment?"), false)
+
+ protected def onSuccess(objectDeleted: Boolean, contentRemoved: Boolean): Unit
+
+ def finish() {
+ for {
+ a <- getAttachment if confirm
+ r <- delete(a)
+ rm <- a.remove
+ } {
+ onSuccess(r, rm)
+ }
+ }
+
+}
+
+// vim: set ts=2 sw=2 et:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/fis/fs/ui/AttachmentTable.scala Wed May 30 22:51:02 2012 +0200
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2012 Tomas Zeman <tzeman@volny.cz>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package fis.fs.ui
+
+import fis.fs.model.Attachment
+import net.liftweb.http.Templates
+import net.liftweb.util.Helpers._
+import scala.xml.NodeSeq
+
+object AttachmentTable {
+ protected def load = Templates(List("templates-hidden", "attachments")) openOr
+ NodeSeq.Empty
+
+ def apply(downloadLink: Attachment => NodeSeq,
+ removeLink: Attachment => NodeSeq)(l: Iterable[Attachment]): NodeSeq =
+ (".attachment" #> { l map { a =>
+ ".attachment-name" #> downloadLink(a) &
+ ".attachment-size" #> a.size.asHtml &
+ ".attachment-date" #> a.updatedAt.asHtml &
+ ".attachment-delete" #> removeLink(a)
+ }})(load)
+
+}
+
+// vim: set ts=2 sw=2 et:
--- a/src/main/scala/fis/pm/model/PmSchema.scala Thu May 24 11:19:26 2012 +0200
+++ b/src/main/scala/fis/pm/model/PmSchema.scala Wed May 30 22:51:02 2012 +0200
@@ -18,9 +18,11 @@
import fis.aaa.model.{AaaSchema, User}
import fis.base.model.{BaseSchema, Entity}
import fis.cl.model.CodeListSchema
+import fis.fs.model.FsSchema
import net.liftweb.squerylrecord.RecordTypeMode._
-trait PmSchema extends BaseSchema with AaaSchema with CodeListSchema {
+trait PmSchema extends BaseSchema with AaaSchema with CodeListSchema with
+ FsSchema {
/* project */
val projectT = tableWithSeq[Project]
@@ -81,6 +83,24 @@
))
projectLocation.leftForeignKeyDeclaration.constrainReference(onDelete cascade)
projectLocation.rightForeignKeyDeclaration.constrainReference(onDelete cascade)
+
+ /* project/task attachments */
+ val projectAttachment = manyToManyRelation(projectT, attachmentT).
+ via[ProjectAttachment]((p, a, pa) => (
+ p.id === pa.project,
+ a.id === pa.attachment
+ ))
+ projectAttachment.leftForeignKeyDeclaration.constrainReference(onDelete cascade)
+ projectAttachment.rightForeignKeyDeclaration.constrainReference(onDelete cascade)
+
+ val taskAttachment = manyToManyRelation(taskT, attachmentT).
+ via[TaskAttachment]((t, a, ta) => (
+ t.id === ta.task,
+ a.id === ta.attachment
+ ))
+ taskAttachment.leftForeignKeyDeclaration.constrainReference(onDelete cascade)
+ taskAttachment.rightForeignKeyDeclaration.constrainReference(onDelete cascade)
+
}
object PmSchema extends PmSchema
@@ -128,6 +148,20 @@
from(PmSchema.projectLocation.right(l))(p => select(p) orderBy(p.name asc))
}
+object ProjectAttachments {
+ import fis.fs.model._
+ def apply(p: Project): Iterable[Attachment] =
+ from(PmSchema.projectAttachment.left(p))(a =>
+ select(a) orderBy(a.name asc))
+}
+
+object TaskAttachments {
+ import fis.fs.model._
+ def apply(t: Task): Iterable[Attachment] =
+ from(PmSchema.taskAttachment.left(t))(a =>
+ select(a) orderBy(a.name asc))
+}
+
/* Many-to-many relations */
import org.squeryl.KeyedEntity
@@ -143,4 +177,14 @@
def id = CompositeKey2(project, location)
}
+case class ProjectAttachment(val project: Long, val attachment: Long)
+ extends KeyedEntity[CompositeKey2[Long, Long]] {
+ def id = CompositeKey2(project, attachment)
+}
+
+case class TaskAttachment(val task: Long, val attachment: Long)
+ extends KeyedEntity[CompositeKey2[Long, Long]] {
+ def id = CompositeKey2(task, attachment)
+}
+
// vim: set ts=2 sw=2 et:
--- a/src/main/scala/fis/pm/ui/ProjectSnippet.scala Thu May 24 11:19:26 2012 +0200
+++ b/src/main/scala/fis/pm/ui/ProjectSnippet.scala Wed May 30 22:51:02 2012 +0200
@@ -19,6 +19,8 @@
import fis.base.model.ReadOnlyField
import fis.base.ui._
import fis.crm.model._
+import fis.fs.model._
+import fis.fs.ui._
import fis.geo.model._
import fis.pm.model._
import net.liftweb.common._
@@ -46,7 +48,8 @@
private val viewPre = Menu.param[Project]("project.view", l10n("Project"), parse,
encode) / prefix / * >> Title(p => i18n("Project %s", p.linkName)) >>
locTpl("project/view") >> Snippet("panel", panel) >>
- Snippet("tasks", tasks) >> Hidden
+ Snippet("tasks", tasks) >> Snippet("attachments", attachmentsTable) >>
+ Hidden
private val editPre = Menu.param[Project]("project.edit", l10n("Edit"), parse,
encode) / prefix / * / EDIT >>
@@ -70,10 +73,11 @@
IfLoggedIn.testVal >>
locTpl("entity/form") >> Snippet("form", locationsF) >> Hidden
+
private val listM = listPre >> SecNav(createPre).build
private val createM = createPre >> SecNav(listPre).build
private val viewM = viewPre >> (SecNav(editPre) + deletePre +
- locationsPre + createTaskPre).build
+ locationsPre + createTaskPre + attachments.addPre).build
private val editM = editPre >> SecNav(viewPre).build
private val deleteM = deletePre >> SecNav(viewPre).build
private val createTaskM = createTaskPre >> SecNav(viewPre).build
@@ -84,7 +88,7 @@
private lazy val deleteLoc = deleteM.toLoc
val menu = listM submenus(viewM, editM, createM, deleteM, createTaskM,
- locationsM)
+ locationsM, attachments.menu)
private def cur = viewLoc.currentValue or editLoc.currentValue or
deleteLoc.currentValue
@@ -95,6 +99,9 @@
private def tasks: CssTr = "*" #> cur.map { p => TaskTable(ProjectTasks(p)) }
+ private def attachmentsTable: CssTr = "*" #> cur.map { p =>
+ attachments.table(p)(ProjectAttachments(p)) }
+
object url {
def view: Project => Box[String] = (viewLoc.calcHref _) andThen (Box !! _)
def list: String = listM.loc.calcDefaultHref
@@ -230,6 +237,73 @@
}}
}
+ /** Project attachments. */
+ private object attachments {
+
+ val addPre = Menu.param[Project]("project.add-attachment",
+ l10n("Add attachment"), parse, encode) / prefix / * / "attachment" / ADD >>
+ Title(_ => i18n("Add attachment")) >> IfLoggedIn.testVal >>
+ locTpl("entity/form") >> Snippet("form", attachmentF) >> Hidden
+
+ type PA = (Project, Attachment)
+ private object paMemo extends RequestMemoize[List[String], Box[PA]]()
+ private def parsePA(ids: List[String]): Box[PA] = paMemo(ids, ids match {
+ case AsLong(pid) :: AsLong(aid) :: Nil => for {
+ p <- get(pid)
+ a <- AttachmentCrud.get(aid)
+ } yield (p, a)
+ case _ => Empty
+ })
+ private def encodePA(pa: PA) = List(pa._1, pa._2) map(_.id.toString)
+
+ private val downloadPre = Menu.params[PA]("project.attachment",
+ LinkText(pa => Text(pa._2.linkName)),
+ parsePA, encodePA) / prefix / * / "attachment" / * >>
+ EarlyResponse(download _)
+
+ private val deleteM = Menu.params[PA]("project.delete-attachment",
+ l10n("Delete"), parsePA, encodePA) / prefix / * / "attachment" / * / DELETE >>
+ Title(pa => i18n("Delete attachment %s", pa._2.linkName)) >>
+ IfLoggedIn.testVal >> locTpl("entity/form") >>
+ Snippet("form", attachmentD) >> Hidden
+
+ private val addM = addPre >> SecNav(viewPre).build
+
+ val downloadM = downloadPre submenus(addM, deleteM)
+ val menu = downloadM
+
+ private lazy val downloadLoc = downloadM.toLoc
+
+ private def download(): Box[LiftResponse] = for {
+ (p, a) <- downloadLoc.currentValue
+ is <- a.getContent
+ } yield
+ StreamingResponse(is, () => is.close, a.contentSize, a.headers, Nil, 200)
+
+ private object attachmentF extends AttachmentForm {
+
+ override protected def onSuccess(a: Attachment) =
+ addM.toLoc.currentValue.foreach { p =>
+ PmSchema.projectAttachment.left(p).associate(a)
+ S notice l10n("Attachment %s saved.", a.name.get)
+ S redirectTo viewLoc.calcHref(p)
+ }
+ }
+
+ private object attachmentD extends DeleteAttachmentForm {
+ protected def getAttachment = deleteM.toLoc.currentValue map(_._2)
+ protected def onSuccess(d: Boolean, rm: Boolean) {
+ S notice l10n("Attachment deleted.")
+ deleteM.toLoc.currentValue map(_._1) foreach { p =>
+ S redirectTo viewLoc.calcHref(p) }
+ }
+ }
+
+ def table(p: Project) = AttachmentTable(
+ { a => NavLink(downloadLoc)((p, a)) },
+ { a => NavLink(deleteM.toLoc)((p, a)) }) _
+ }
+
}
// vim: set ts=2 sw=2 et:
--- a/src/main/scala/fis/pm/ui/TaskSnippet.scala Thu May 24 11:19:26 2012 +0200
+++ b/src/main/scala/fis/pm/ui/TaskSnippet.scala Wed May 30 22:51:02 2012 +0200
@@ -17,6 +17,8 @@
import fis.aaa.ui.IfLoggedIn
import fis.base.ui._
+import fis.fs.model._
+import fis.fs.ui._
import fis.pm.model._
import net.liftweb.common._
import net.liftweb.http._
@@ -38,6 +40,7 @@
private val viewPre = Menu.param[Task]("task.view", l10n("Task"), parse,
encode) / prefix / * >> Title(t => i18n("Task %s", t.linkName)) >>
locTpl("task/view") >> Snippet("panel", panel) >>
+ Snippet("attachments", attachmentsTable) >>
Snippet("comments", comments) >> Hidden
private val editPre = Menu.param[Task]("task.edit", l10n("Edit"), parse,
@@ -58,7 +61,7 @@
Snippet("comments", comments) >> Hidden
private val viewM = viewPre >> (SecNav(editPre) + deletePre +
- postCommentPre).build
+ attachments.addPre + postCommentPre).build
private val editM = editPre >> SecNav(viewPre).build
private val deleteM = deletePre >> SecNav(viewPre).build
private val postCommentM = postCommentPre >> SecNav(viewPre).build
@@ -68,7 +71,8 @@
private lazy val deleteLoc = deleteM.toLoc
private lazy val postCommentLoc = postCommentM.toLoc
- val menu = listM submenus(viewM, editM, deleteM, postCommentM)
+ val menu = listM submenus(viewM, editM, deleteM, postCommentM,
+ attachments.menu)
private def cur = viewLoc.currentValue or editLoc.currentValue or
deleteLoc.currentValue or postCommentLoc.currentValue
@@ -78,6 +82,9 @@
private def comments: CssTr = "*" #> cur.map { t =>
CommentTable(TaskComments(t)) }
+ private def attachmentsTable: CssTr = "*" #> cur.map { t =>
+ attachments.table(t)(TaskAttachments(t)) }
+
object url {
def view: Task => Box[String] = (viewLoc.calcHref _) andThen (Box !! _)
}
@@ -139,6 +146,73 @@
}}
}
+ /** Task attachments. */
+ private object attachments {
+
+ val addPre = Menu.param[Task]("task.add-attachment",
+ l10n("Add attachment"), parse, encode) / prefix / * / "attachment" / ADD >>
+ Title(_ => i18n("Add attachment")) >> IfLoggedIn.testVal >>
+ locTpl("entity/form") >> Snippet("form", attachmentF) >> Hidden
+
+ type TA = (Task, Attachment)
+ private object taMemo extends RequestMemoize[List[String], Box[TA]]()
+ private def parseTA(ids: List[String]): Box[TA] = taMemo(ids, ids match {
+ case AsLong(tid) :: AsLong(aid) :: Nil => for {
+ t <- get(tid)
+ a <- AttachmentCrud.get(aid)
+ } yield (t, a)
+ case _ => Empty
+ })
+ private def encodeTA(ta: TA) = List(ta._1, ta._2) map(_.id.toString)
+
+ private val downloadPre = Menu.params[TA]("task.attachment",
+ LinkText(ta => Text(ta._2.linkName)),
+ parseTA, encodeTA) / prefix / * / "attachment" / * >>
+ EarlyResponse(download _)
+
+ private val deleteM = Menu.params[TA]("task.delete-attachment",
+ l10n("Delete"), parseTA, encodeTA) / prefix / * / "attachment" / * / DELETE >>
+ Title(ta => i18n("Delete attachment %s", ta._2.linkName)) >>
+ IfLoggedIn.testVal >> locTpl("entity/form") >>
+ Snippet("form", attachmentD) >> Hidden
+
+ private val addM = addPre >> SecNav(viewPre).build
+
+ val downloadM = downloadPre submenus(addM, deleteM)
+ val menu = downloadM
+
+ private lazy val downloadLoc = downloadM.toLoc
+
+ private def download(): Box[LiftResponse] = for {
+ (t, a) <- downloadLoc.currentValue
+ is <- a.getContent
+ } yield
+ StreamingResponse(is, () => is.close, a.contentSize, a.headers, Nil, 200)
+
+ private object attachmentF extends AttachmentForm {
+
+ override protected def onSuccess(a: Attachment) =
+ addM.toLoc.currentValue.foreach { t =>
+ PmSchema.taskAttachment.left(t).associate(a)
+ S notice l10n("Attachment %s saved.", a.name.get)
+ S redirectTo viewLoc.calcHref(t)
+ }
+ }
+
+ private object attachmentD extends DeleteAttachmentForm {
+ protected def getAttachment = deleteM.toLoc.currentValue map(_._2)
+ protected def onSuccess(d: Boolean, rm: Boolean) {
+ S notice l10n("Attachment deleted.")
+ deleteM.toLoc.currentValue map(_._1) foreach { t =>
+ S redirectTo viewLoc.calcHref(t) }
+ }
+ }
+
+ def table(t: Task) = AttachmentTable(
+ { a => NavLink(downloadLoc)((t, a)) },
+ { a => NavLink(deleteM.toLoc)((t, a)) }) _
+ }
+
}
// vim: set ts=2 sw=2 et:
--- a/src/main/webapp/project/view.html Thu May 24 11:19:26 2012 +0200
+++ b/src/main/webapp/project/view.html Wed May 30 22:51:02 2012 +0200
@@ -7,9 +7,13 @@
<body class="lift:content_id=main">
<div id="main" class="lift:surround?with=default;at=content">
<div class="row">
- <div class="span12">
+ <div class="span7">
<span class="lift:panel"></span>
</div>
+ <div class="span5">
+ <h3><span class="lift:loc?locid=project.attachments"></span></h3>
+ <span class="lift:attachments"></span>
+ </div>
</div> <!-- /row -->
<div class="row section">
<div class="span12">
--- a/src/main/webapp/task/view.html Thu May 24 11:19:26 2012 +0200
+++ b/src/main/webapp/task/view.html Wed May 30 22:51:02 2012 +0200
@@ -10,9 +10,13 @@
<link rel="stylesheet" href="/css/comment.css" type="text/css" media="screen, projection"/>
</head_merge>
<div class="row">
- <div class="span12">
+ <div class="span7">
<span class="lift:panel"></span>
</div>
+ <div class="span5">
+ <h3><span class="lift:loc?locid=task.attachments"></span></h3>
+ <span class="lift:attachments"></span>
+ </div>
</div> <!-- /row -->
<div class="row section">
<div class="span12">
--- a/src/main/webapp/templates-hidden/_resources.html Thu May 24 11:19:26 2012 +0200
+++ b/src/main/webapp/templates-hidden/_resources.html Wed May 30 22:51:02 2012 +0200
@@ -17,6 +17,7 @@
Remove
Entity type
Check
+ File
-->
<!-- authn -->
@@ -219,6 +220,7 @@
<res name="project.tasks" lang="en" default="true">Tasks</res>
<res name="project.company" lang="en" default="true">Company</res>
<res name="project.locations" lang="en" default="true">Locations</res>
+ <res name="project.attachments" lang="en" default="true">Attachments</res>
<!-- project states -->
<res name="project.state.assigned" lang="en" default="true">Assigned</res>
<res name="project.state.paused" lang="en" default="true">Paused</res>
@@ -250,6 +252,7 @@
<res name="task.responsible" lang="en" default="true">Responsible</res>
<res name="task.stateFld" lang="en" default="true">State [%%]</res>
<res name="task.comments" lang="en" default="true">Comments</res>
+ <res name="task.attachments" lang="en" default="true">Attachments</res>
<!-- task types:
Task type 1
Task type 2
@@ -314,6 +317,16 @@
<res name="servicePayment.currency" lang="en" default="true">Currency</res>
+ <!-- attachments
+ default strings:
+ Add attachment
+ Delete attachment %s
+ Attachment %s saved.
+ Attachment deleted.
+ Really delete this attachment?
+ -->
+
+
<!--
vim: et sw=2 ts=2
--- a/src/main/webapp/templates-hidden/_resources_cs.html Thu May 24 11:19:26 2012 +0200
+++ b/src/main/webapp/templates-hidden/_resources_cs.html Wed May 30 22:51:02 2012 +0200
@@ -16,6 +16,7 @@
<res name="updatedAt" lang="cs">Aktualizováno</res>
<res name="updatedBy" lang="cs">Aktualizoval</res>
<res name="Check" lang="cs">Označit</res>
+ <res name="File" lang="cs">Soubor</res>
<!-- authn -->
@@ -207,6 +208,7 @@
<res name="project.tasks" lang="cs">Úkoly</res>
<res name="project.company" lang="cs">Společnost</res>
<res name="project.locations" lang="cs">Lokality</res>
+ <res name="project.attachments" lang="cs">Přílohy</res>
<!-- project states -->
<res name="project.state.assigned" lang="cs">Přidělen</res>
<res name="project.state.paused" lang="cs">Pozastaven</res>
@@ -236,6 +238,7 @@
<res name="task.responsible" lang="cs">Odpovědný</res>
<res name="task.stateFld" lang="cs">Stav [%%]</res>
<res name="task.comments" lang="cs">Komentáře</res>
+ <res name="task.attachments" lang="cs">Přílohy</res>
<!-- task types: -->
<res name="Task type 1" lang="cs">Typ 1</res>
<res name="Task type 2" lang="cs">Typ 2</res>
@@ -297,6 +300,14 @@
<res name="servicePayment.currency" lang="cs">Měna</res>
+ <!-- attachments -->
+ <res name="Add attachment" lang="cs">Přidat přílohu</res>
+ <res name="Delete attachment %s" lang="cs">Smazat přílohu %s</res>
+ <res name="Attachment %s saved." lang="cs">Příloha %s uložena.</res>
+ <res name="Attachment deleted." lang="cs">Příloha smazána.</res>
+ <res name="Really delete this attachment?" lang="cs">Skutečně smazat přílohu?</res>
+
+
<!--
vim: et sw=2 ts=2
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/templates-hidden/attachments.html Wed May 30 22:51:02 2012 +0200
@@ -0,0 +1,7 @@
+<span class="attachments-container">
+ <div class="attachment">
+ <span class="attachment-name"></span>
+ (<span class="attachment-size"></span>, <span class="attachment-date"></span>)
+ <span class="attachment-delete"></span>
+ </div>
+</span>