# HG changeset patch # User Tomas Zeman # Date 1366706164 -7200 # Node ID ef4e3e0ef83fd07d127e505f1cbc478ca0da3252 # Parent 26e9d0e4a619057ac18fe74bcbd57ade3ecb3b9d 84a94fa29a67504b Task/Project notifications diff -r 26e9d0e4a619 -r ef4e3e0ef83f src/main/resources/default.props --- a/src/main/resources/default.props Tue Jun 05 15:40:45 2012 +0200 +++ b/src/main/resources/default.props Tue Apr 23 10:36:04 2013 +0200 @@ -13,3 +13,8 @@ # Attachments dir.attachment=attachments + +# Sender +url.base=http://localhost:8081 +sender.email=fis-dev@localhost +sender.name=FIS Development diff -r 26e9d0e4a619 -r ef4e3e0ef83f src/main/resources/logback.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/resources/logback.xml Tue Apr 23 10:36:04 2013 +0200 @@ -0,0 +1,17 @@ + + + + + + %d [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + diff -r 26e9d0e4a619 -r ef4e3e0ef83f src/main/scala/fis/base/model/InjectableRecordCrud.scala --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/scala/fis/base/model/InjectableRecordCrud.scala Tue Apr 23 10:36:04 2013 +0200 @@ -0,0 +1,37 @@ +/* + * Copyright 2012 Tomas Zeman + * + * 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.base.model + +import net.liftweb.common._ +import net.liftweb.record.Record +import net.liftweb.squerylrecord.KeyedRecord +import net.liftweb.util._ + +trait InjectableRecordCrud[T <: Record[T] with KeyedRecord[Long]] extends RecordCrud[T] { + def injectors: CrudInjectors[T] + + override protected def afterSave(orig: Box[T], v: Box[T]): Box[T] = { + val r = super.afterSave(orig, v) + injectors.afterSave.flatMap(_(orig, v)) or r + } +} + +trait CrudInjectors[T <: Record[T] with KeyedRecord[Long]] { + + @volatile var afterSave: Box[(Box[T], Box[T]) => Box[T]] = Empty +} + +// vim: set ts=2 sw=2 et: diff -r 26e9d0e4a619 -r ef4e3e0ef83f src/main/scala/fis/base/model/RecordCrud.scala --- a/src/main/scala/fis/base/model/RecordCrud.scala Tue Jun 05 15:40:45 2012 +0200 +++ b/src/main/scala/fis/base/model/RecordCrud.scala Tue Apr 23 10:36:04 2013 +0200 @@ -47,12 +47,13 @@ */ def save(v: T): Box[T] = { val isCreate = v.id == v.idField.defaultValue + val orig = get(v.id) val res = for { b <- if (isCreate) beforeCreate(v) else beforeUpdate(v) - bs <- beforeSave(b) + bs <- beforeSave(orig, b) r <- tryo { table insertOrUpdate bs } } yield { r } - val as = afterSave(res) + val as = afterSave(orig, res) if (isCreate) afterCreate(as) else afterUpdate(as) } @@ -84,12 +85,12 @@ v } - protected def beforeSave(v: T): Box[T] = { + protected def beforeSave(orig: Box[T], v: T): Box[T] = { v.meta.foreachCallback(v, _.beforeSave) Full(v) } - protected def afterSave(v: Box[T]): Box[T] = { + protected def afterSave(orig: Box[T], v: Box[T]): Box[T] = { v foreach { r => r.meta.foreachCallback(r, _.afterSave) } v } diff -r 26e9d0e4a619 -r ef4e3e0ef83f src/main/scala/fis/notif/Notification.scala --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/scala/fis/notif/Notification.scala Tue Apr 23 10:36:04 2013 +0200 @@ -0,0 +1,117 @@ +/* + * Copyright 2012 Tomas Zeman + * + * 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.notif + +import fis.aaa.model.{User, UserContacts} +import javax.mail.internet.MimeUtility +import net.liftweb.common._ +import net.liftweb.http._ +import net.liftweb.util._ +import net.liftweb.util.Helpers._ +import net.tz.lift.snippet._ +import scala.xml.NodeSeq + +import Mailer._ + +case class Notification(_path: String, _title: CssTr, + _content: CssTr, _note: CssTr, _from: From, _subject: String, + _mailTypes: Seq[MailTypes]) { + + def title(t: CssTr) = this.copy(_title = t) + def title(t: NodeSeq) = this.copy(_title = lift(t)) + def content(c: CssTr) = this.copy(_content = c) + def note(n: CssTr) = this.copy(_note = n) + def note(n: String) = this.copy(_note = "*" #> n) + def from(f: From) = this.copy(_from = f) + def subject(s: String) = this.copy(_subject = s) + def >> (mt: MailTypes) = this.copy(_mailTypes = _mailTypes :+ mt) + def >> (u: User) = this.copy(_mailTypes = _mailTypes ++ + (UserContacts(u) map { c => To(c.workMail.get, Box !! c.linkName)})) + def to(u: Box[User]): Notification = u.dmap(this)(>> _) + + private def lift(ns: NodeSeq): CssTr = { _ => ns } +} + +object Notification { + def make: Notification = Notification("entity/notif", ClearNodes, + ClearNodes, ClearNodes, From(Props.get("sender.email") openOr "", + Props.get("sender.name")), "", Nil) +} + +class NotificationEngine extends Loggable { + + type Tr = Notification => Notification + + import java.util.concurrent.atomic.AtomicReference + import scala.collection.mutable.ArrayBuffer + + private lazy val _rules = new ArrayBuffer[Rule]() + + class RuleEntrance(val name: String) { + def when(cond: => Boolean) = new { + def then(f: Tr*) = { + val r = new Rule(name, cond, f.toSeq) + _rules += r + logger.debug("Defined new rule: %s, rules count: %d".format(r.name, + _rules.size)) + r + } + } + } + + implicit def str2ruleEntrance(name: String) = new RuleEntrance(name) + + class Rule(val name: String, cond: => Boolean, tr: Seq[Tr]) { + lazy val n = Notification.make + def active: Boolean = cond + def apply() = tr.foldLeft(n)((_n, f) => f(_n)) + } + + def subject(s: String): Tr = _.subject(s) + def title(t: CssTr): Tr = _.title(t) + def content(c: CssTr): Tr = _.content(c) + def to(u: Box[User]): Tr = _.to(u) + def note(n: CssTr): Tr = _.note(n) + def note(n: String): Tr = _.note(n) + + private def enc(s: String) = MimeUtility.encodeText(s, "utf-8", null) + + def execute(f: => Rule) = { + withBaseUrl.run(Props.get("url.base") openOr "") { + f + for { + r <- _rules if r.active + (out, n) <- { + val n = r() + S.runTemplate(n._path split "/" toList, + "title" -> n._title, + "panel" -> n._content, + "note" -> n._note) map(_ -> n) + } + } yield { + sendMail(n._from, Subject(enc(n._subject)), (n._mailTypes :+ XHTMLMailBodyType(out)):_*) + (n, n._subject, n._mailTypes flatMap { + case To(to, _) => Full(to) + case CC(cc, _) => Full(cc) + case _ => Empty + }) + } + } + } + +} + +// vim: set ts=2 sw=2 et: diff -r 26e9d0e4a619 -r ef4e3e0ef83f src/main/scala/fis/pm/model/TaskCrud.scala --- a/src/main/scala/fis/pm/model/TaskCrud.scala Tue Jun 05 15:40:45 2012 +0200 +++ b/src/main/scala/fis/pm/model/TaskCrud.scala Tue Apr 23 10:36:04 2013 +0200 @@ -15,12 +15,13 @@ */ package fis.pm.model -import fis.base.model.RecordCrud +import fis.base.model._ -trait TaskCrud extends RecordCrud[Task] { +trait TaskCrud extends InjectableRecordCrud[Task] { + def injectors = TaskCrud val table = PmSchema.taskT } -object TaskCrud extends TaskCrud +object TaskCrud extends TaskCrud with CrudInjectors[Task] // vim: set ts=2 sw=2 et: diff -r 26e9d0e4a619 -r ef4e3e0ef83f src/main/scala/fis/pm/ui/ProjectNotification.scala --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/scala/fis/pm/ui/ProjectNotification.scala Tue Apr 23 10:36:04 2013 +0200 @@ -0,0 +1,78 @@ +/* + * Copyright 2012 Tomas Zeman + * + * 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.pm.ui + +import fis.notif._ +import fis.pm.model._ +import net.liftweb.common._ +import net.liftweb.http._ +import net.liftweb.sitemap._ +import net.liftweb.util._ +import net.liftweb.util.Helpers._ +import net.tz.lift.model._ +import net.tz.lift.snippet._ + +trait ProjectNotification extends Loggable { + + def viewLoc: Loc[Project] + def panel(p: Project): CssTr + + def apply(orig: Box[Project], proj: Box[Project]) { + for { + p <- proj + } { + val submitter = p.createdBy.valueBox + val resp = p.responsible.valueBox + val respN = p.responsible.user.dmap("")(_.linkName) + val origResp = orig map(_.responsible.valueBox) + val origRespU = orig flatMap(_.responsible.user) + val updater = p.updatedBy.valueBox + val by = p.updatedBy.user.dmap("")(_.linkName) + + logger.debug("Executing notifications for project %s".format(p.linkName)) + val ne = new NotificationEngine + import ne._ + execute { + val cnt = panel(p) + "Project update" when(origResp == resp) then ( + subject(l10n("Project %s modified by %s", p.linkName, by)), + title("*" #> a(viewLoc.calcHref(p))(viewLoc.title(p))), + content(cnt), + to(p.createdBy.user filterNot { u => updater == submitter }), + to(p.responsible.user filterNot { u => updater == resp }) + ) + "Project assignment" when(!(origResp == resp)) then ( + subject(l10n("Project %s assigned to %s by %s", p.linkName, respN, by)), + title("*" #> a(viewLoc.calcHref(p))(viewLoc.title(p))), + content(cnt), + to(p.createdBy.user filterNot { u => updater == submitter }), + to(origRespU filterNot { u => updater == origResp }) + ) + "Project assigned to you" when(!(origResp == resp) && !(updater == resp)) then ( + subject(l10n("Project %s assigned to you by %s", p.linkName, by)), + title("*" #> a(viewLoc.calcHref(p))(viewLoc.title(p))), + content(cnt), + to(p.responsible.user) + ) + } foreach { res => + S notice(l10n("Sent notification to [%s] with subject %s", + res._3 mkString ",", res._2)) + } + } + } +} + +// vim: set ts=2 sw=2 et: diff -r 26e9d0e4a619 -r ef4e3e0ef83f src/main/scala/fis/pm/ui/ProjectSnippet.scala --- a/src/main/scala/fis/pm/ui/ProjectSnippet.scala Tue Jun 05 15:40:45 2012 +0200 +++ b/src/main/scala/fis/pm/ui/ProjectSnippet.scala Tue Apr 23 10:36:04 2013 +0200 @@ -37,6 +37,7 @@ import scala.xml.{Elem, NodeSeq, Text} object ProjectSnippet extends ProjectCrud with EntitySnippet[Project] { + val prefix = "project" private val listPre = Menu("project.list", l10n("Projects")) / prefix >> @@ -358,6 +359,14 @@ { a => NavLink(deleteM.toLoc)((p, a)) }) _ } + /** Project notifications. */ + private object notifications extends ProjectNotification { + lazy val viewLoc = ProjectSnippet.this.viewLoc + def panel(p: Project) = ViewPanel(fields(p)) + } + + override protected def afterSave(orig: Box[Project], proj: Box[Project]) = + proj $ { _ => notifications(orig, proj) } } // vim: set ts=2 sw=2 et: diff -r 26e9d0e4a619 -r ef4e3e0ef83f src/main/scala/fis/pm/ui/TaskNotification.scala --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/scala/fis/pm/ui/TaskNotification.scala Tue Apr 23 10:36:04 2013 +0200 @@ -0,0 +1,77 @@ +/* + * Copyright 2012 Tomas Zeman + * + * 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.pm.ui + +import fis.notif._ +import fis.pm.model._ +import net.liftweb.common._ +import net.liftweb.http._ +import net.liftweb.sitemap._ +import net.liftweb.util._ +import net.liftweb.util.Helpers._ +import net.tz.lift.model._ +import net.tz.lift.snippet._ + +trait TaskNotification extends Loggable { + + def viewLoc: Loc[Task] + def panel(t: Task): CssTr + + def apply(orig: Box[Task], task: Box[Task]) { + for { + t <- task + } { + val submitter = t.createdBy.valueBox + val resp = t.responsible.valueBox + val respN = t.responsible.user.dmap("")(_.linkName) + val origResp = orig map(_.responsible.valueBox) + val origRespU = orig flatMap(_.responsible.user) + val updater = t.updatedBy.valueBox + val by = t.updatedBy.user.dmap("")(_.linkName) + + logger.debug("Executing notifications for task %s".format(t.linkName)) + val ne = new NotificationEngine + import ne._ + execute { + val cnt = content(panel(t)) + val _title = title("*" #> a(viewLoc.calcHref(t))(viewLoc.title(t))) + "Task update" when(origResp == resp) then ( + subject(l10n("[Task %s] %s modified by %s", t.numberStr, t.linkName, by)), + _title, cnt, + to(t.createdBy.user filterNot { u => updater == submitter }), + to(t.responsible.user filterNot { u => updater == resp }) + ) + "Task assignment" when(!(origResp == resp)) then ( + subject(l10n("[Task %s] %s assigned to %s by %s", t.numberStr, t.linkName, respN, by)), + _title, cnt, + to(t.createdBy.user filterNot { u => updater == submitter }), + to(origRespU filterNot { u => updater == origResp }) + ) + "Task assigned to you" when(!(origResp == resp) && !(updater == resp)) then ( + subject(l10n("[Task %s] %s assigned to you by %s", t.numberStr, t.linkName, by)), + _title, cnt, + to(t.responsible.user) + ) + } foreach { res => + S notice(l10n("Sent notification to [%s] with subject %s", + res._3 mkString ",", res._2)) + } + } + } +} + +// vim: set ts=2 sw=2 et: + diff -r 26e9d0e4a619 -r ef4e3e0ef83f src/main/scala/fis/pm/ui/TaskSnippet.scala --- a/src/main/scala/fis/pm/ui/TaskSnippet.scala Tue Jun 05 15:40:45 2012 +0200 +++ b/src/main/scala/fis/pm/ui/TaskSnippet.scala Tue Apr 23 10:36:04 2013 +0200 @@ -246,6 +246,17 @@ { a => NavLink(deleteM.toLoc)((t, a)) }) _ } + /** Task notifications. */ + private object notifications extends TaskNotification { + lazy val viewLoc = TaskSnippet.this.viewLoc + def panel(t: Task) = ViewPanel(fields(t)) + def hook(orig: Box[Task], t: Box[Task]): Box[Task] = { + apply(orig, t) + t + } + } + + TaskCrud.afterSave = Full(notifications.hook _) } // vim: set ts=2 sw=2 et: diff -r 26e9d0e4a619 -r ef4e3e0ef83f src/main/scala/net/tz/lift/snippet/package.scala --- a/src/main/scala/net/tz/lift/snippet/package.scala Tue Jun 05 15:40:45 2012 +0200 +++ b/src/main/scala/net/tz/lift/snippet/package.scala Tue Apr 23 10:36:04 2013 +0200 @@ -18,16 +18,20 @@ import net.liftweb.common._ import net.liftweb.http.S import net.liftweb.sitemap.Loc +import net.liftweb.util.DynoVar import net.liftweb.util.Helpers._ import scala.xml.{Elem, NodeSeq, Text} package object snippet { type CssTr = (NodeSeq => NodeSeq) - def a(href: String)(cnt: NodeSeq): Elem = {cnt} + object withBaseUrl extends DynoVar[String] + + def a(href: String)(cnt: NodeSeq): Elem = + {cnt} def a(href: Box[String])(cnt: NodeSeq): NodeSeq = - href.dmap(cnt) { l => {cnt} } + href.dmap(cnt) { l => {cnt} } def locTpl(p: String): Loc.Template = LocTpl(p) } diff -r 26e9d0e4a619 -r ef4e3e0ef83f src/main/webapp/entity/notif.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/entity/notif.html Tue Apr 23 10:36:04 2013 +0200 @@ -0,0 +1,19 @@ + + + +
+
+
+ +
+
+
+
+ +
+
+
+ + + + diff -r 26e9d0e4a619 -r ef4e3e0ef83f src/main/webapp/templates-hidden/_resources.html --- a/src/main/webapp/templates-hidden/_resources.html Tue Jun 05 15:40:45 2012 +0200 +++ b/src/main/webapp/templates-hidden/_resources.html Tue Apr 23 10:36:04 2013 +0200 @@ -18,6 +18,7 @@ Entity type Check File + Sent notification to [%s] with subject %s --> @@ -209,6 +210,9 @@ Locations saved. Create new company? Create new company instead of choosing an existing one. + Project %s modified by %s + Project %s assigned to %s by %s + Project %s assigned to you by %s --> Projects @@ -255,6 +259,9 @@ Task %s deleted. Task %s saved. All Tasks + [Task %s] %s modified by %s + [Task %s] %s assigned to %s by %s + [Task %s] %s assigned to you by %s --> Name diff -r 26e9d0e4a619 -r ef4e3e0ef83f src/main/webapp/templates-hidden/_resources_cs.html --- a/src/main/webapp/templates-hidden/_resources_cs.html Tue Jun 05 15:40:45 2012 +0200 +++ b/src/main/webapp/templates-hidden/_resources_cs.html Tue Apr 23 10:36:04 2013 +0200 @@ -17,6 +17,7 @@ Aktualizoval Označit Soubor + Odeslána zpráva na adresu [%s] s předmětem %s. @@ -199,6 +200,9 @@ Lokality uloženy. Vytvořit novou společnost? Vytvoří se nová společnost místo výběru z existujících. + Projekt %s upraven uživatelem %s + Projekt %s přiřazen %s uživatelem %s + Projekt %s Vám přiřadil %s Název Poznámka @@ -242,6 +246,9 @@ Úkol %s smazán. Úkol %s uložen. Všechny úkoly + [Úkol %s] %s upraven uživatelem %s + [Úkol %s] %s přiřazen %s uživatelem %s + [Úkol %s] %s Vám přiřadil %s Název Popis diff -r 26e9d0e4a619 -r ef4e3e0ef83f src/main/webapp/templates-hidden/notification.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/templates-hidden/notification.html Tue Apr 23 10:36:04 2013 +0200 @@ -0,0 +1,314 @@ + + + + + + + +
+
+
+

+
+
+
+
+ + +