--- 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
--- /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 @@
+<configuration scan="true">
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <!-- encoders are by default assigned the type
+ ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
+ <encoder>
+ <pattern>%d [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <root level="debug">
+ <appender-ref ref="STDOUT" />
+ </root>
+
+ <logger name="fis.db.SquerylTxMgr" level="ERROR"/>
+
+</configuration>
--- /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 <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.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:
--- 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
}
--- /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 <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.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:
--- 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:
--- /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 <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.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:
--- 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:
--- /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 <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.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:
+
--- 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:
--- 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 = <a href={href}>{cnt}</a>
+ object withBaseUrl extends DynoVar[String]
+
+ def a(href: String)(cnt: NodeSeq): Elem =
+ <a href={(withBaseUrl.get openOr "") + href}>{cnt}</a>
def a(href: Box[String])(cnt: NodeSeq): NodeSeq =
- href.dmap(cnt) { l => <a href={l}>{cnt}</a> }
+ href.dmap(cnt) { l => <a href={(withBaseUrl.get openOr "") + l}>{cnt}</a> }
def locTpl(p: String): Loc.Template = LocTpl(p)
}
--- /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 @@
+<!DOCTYPE html>
+<html>
+ <body class="lift:content_id=main">
+ <div id="main" class="lift:surround?with=notification;at=content">
+ <div class="row">
+ <div class="span12">
+ <span class="lift:note"></span>
+ </div>
+ </div> <!-- /row -->
+ <div class="row">
+ <div class="span12">
+ <span class="lift:panel"></span>
+ </div>
+ </div> <!-- /row -->
+ </div>
+ </body>
+</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
-->
<!-- authn -->
@@ -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
-->
<res name="Projects" lang="en" default="true">Projects</res>
<!-- project fields -->
@@ -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
-->
<!-- task fields -->
<res name="task.name" lang="en" default="true">Name</res>
--- 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 @@
<res name="updatedBy" lang="cs">Aktualizoval</res>
<res name="Check" lang="cs">Označit</res>
<res name="File" lang="cs">Soubor</res>
+ <res name="Sent notification to [%s] with subject %s" lang="cs">Odeslána zpráva na adresu [%s] s předmětem %s.</res>
<!-- authn -->
@@ -199,6 +200,9 @@
<res name="Locations saved." lang="cs">Lokality uloženy.</res>
<res name="Create new company?" lang="cs">Vytvořit novou společnost?</res>
<res name="Create new company instead of choosing an existing one." lang="cs">Vytvoří se nová společnost místo výběru z existujících.</res>
+ <res name="Project %s modified by %s" lang="cs">Projekt %s upraven uživatelem %s</res>
+ <res name="Project %s assigned to %s by %s" lang="cs">Projekt %s přiřazen %s uživatelem %s</res>
+ <res name="Project %s assigned to you by %s" lang="cs">Projekt %s Vám přiřadil %s</res>
<!-- project fields -->
<res name="project.name" lang="cs">Název</res>
<res name="project.note" lang="cs">Poznámka</res>
@@ -242,6 +246,9 @@
<res name="Task %s deleted." lang="cs">Úkol %s smazán.</res>
<res name="Task %s saved." lang="cs">Úkol %s uložen.</res>
<res name="All Tasks" lang="cs">Všechny úkoly</res>
+ <res name="[Task %s] %s modified by %s" lang="cs">[Úkol %s] %s upraven uživatelem %s</res>
+ <res name="[Task %s] %s assigned to %s by %s" lang="cs">[Úkol %s] %s přiřazen %s uživatelem %s</res>
+ <res name="[Task %s] %s assigned to you by %s" lang="cs">[Úkol %s] %s Vám přiřadil %s</res>
<!-- task fields -->
<res name="task.name" lang="cs">Název</res>
<res name="task.note" lang="cs">Popis</res>
--- /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 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:lift="http://liftweb.net/">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <style type="text/css">
+ /*** bootstrap.css ***/
+ body {
+ margin: 0;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 13px;
+ line-height: 18px;
+ color: #333333;
+ background-color: #ffffff;
+ }
+ a {
+ color: #0088cc;
+ text-decoration: none;
+ }
+ a:hover {
+ color: #005580;
+ text-decoration: underline;
+ }
+ .row {
+ margin-left: -20px;
+ *zoom: 1;
+ }
+ .row:before,
+ .row:after {
+ display: table;
+ content: "";
+ }
+ .row:after {
+ clear: both;
+ }
+ [class*="span"] {
+ float: left;
+ margin-left: 20px;
+ }
+ .container,
+ .navbar-fixed-top .container,
+ .navbar-fixed-bottom .container {
+ width: 940px;
+ }
+ .span12 {
+ width: 940px;
+ }
+ .span11 {
+ width: 860px;
+ }
+ .span10 {
+ width: 780px;
+ }
+ .span9 {
+ width: 700px;
+ }
+ .span8 {
+ width: 620px;
+ }
+ .span7 {
+ width: 540px;
+ }
+ .span6 {
+ width: 460px;
+ }
+ .span5 {
+ width: 380px;
+ }
+ .span4 {
+ width: 300px;
+ }
+ .span3 {
+ width: 220px;
+ }
+ .span2 {
+ width: 140px;
+ }
+ .span1 {
+ width: 60px;
+ }
+ .offset12 {
+ margin-left: 980px;
+ }
+ .offset11 {
+ margin-left: 900px;
+ }
+ .offset10 {
+ margin-left: 820px;
+ }
+ .offset9 {
+ margin-left: 740px;
+ }
+ .offset8 {
+ margin-left: 660px;
+ }
+ .offset7 {
+ margin-left: 580px;
+ }
+ .offset6 {
+ margin-left: 500px;
+ }
+ .offset5 {
+ margin-left: 420px;
+ }
+ .offset4 {
+ margin-left: 340px;
+ }
+ .offset3 {
+ margin-left: 260px;
+ }
+ .offset2 {
+ margin-left: 180px;
+ }
+ .offset1 {
+ margin-left: 100px;
+ }
+ .row-fluid {
+ width: 100%;
+ *zoom: 1;
+ }
+ .row-fluid:before,
+ .row-fluid:after {
+ display: table;
+ content: "";
+ }
+ .row-fluid:after {
+ clear: both;
+ }
+ .row-fluid > [class*="span"] {
+ float: left;
+ margin-left: 2.127659574%;
+ }
+ .row-fluid > [class*="span"]:first-child {
+ margin-left: 0;
+ }
+ .row-fluid > .span12 {
+ width: 99.99999998999999%;
+ }
+ .row-fluid > .span11 {
+ width: 91.489361693%;
+ }
+ .row-fluid > .span10 {
+ width: 82.97872339599999%;
+ }
+ .row-fluid > .span9 {
+ width: 74.468085099%;
+ }
+ .row-fluid > .span8 {
+ width: 65.95744680199999%;
+ }
+ .row-fluid > .span7 {
+ width: 57.446808505%;
+ }
+ .row-fluid > .span6 {
+ width: 48.93617020799999%;
+ }
+ .row-fluid > .span5 {
+ width: 40.425531911%;
+ }
+ .row-fluid > .span4 {
+ width: 31.914893614%;
+ }
+ .row-fluid > .span3 {
+ width: 23.404255317%;
+ }
+ .row-fluid > .span2 {
+ width: 14.89361702%;
+ }
+ .row-fluid > .span1 {
+ width: 6.382978723%;
+ }
+ .container {
+ margin-left: auto;
+ margin-right: auto;
+ *zoom: 1;
+ }
+ .container:before,
+ .container:after {
+ display: table;
+ content: "";
+ }
+ .container:after {
+ clear: both;
+ }
+ .container-fluid {
+ padding-left: 20px;
+ padding-right: 20px;
+ *zoom: 1;
+ }
+ .container-fluid:before,
+ .container-fluid:after {
+ display: table;
+ content: "";
+ }
+ .container-fluid:after {
+ clear: both;
+ }
+ p {
+ margin: 0 0 9px;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 13px;
+ line-height: 18px;
+ }
+ p small {
+ font-size: 11px;
+ color: #999999;
+ }
+ .lead {
+ margin-bottom: 18px;
+ font-size: 20px;
+ font-weight: 200;
+ line-height: 27px;
+ }
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ margin: 0;
+ font-family: inherit;
+ font-weight: bold;
+ color: inherit;
+ text-rendering: optimizelegibility;
+ }
+ h1 small,
+ h2 small,
+ h3 small,
+ h4 small,
+ h5 small,
+ h6 small {
+ font-weight: normal;
+ color: #999999;
+ }
+ h1 {
+ font-size: 30px;
+ line-height: 36px;
+ }
+ h1 small {
+ font-size: 18px;
+ }
+ h2 {
+ font-size: 24px;
+ line-height: 36px;
+ }
+ h2 small {
+ font-size: 18px;
+ }
+ h3 {
+ line-height: 27px;
+ font-size: 18px;
+ }
+ h3 small {
+ font-size: 14px;
+ }
+ h4,
+ h5,
+ h6 {
+ line-height: 18px;
+ }
+ h4 {
+ font-size: 14px;
+ }
+ h4 small {
+ font-size: 12px;
+ }
+ h5 {
+ font-size: 12px;
+ }
+ h6 {
+ font-size: 11px;
+ color: #999999;
+ text-transform: uppercase;
+ }
+
+ /*** base.css ***/
+ /* attribute panel */
+ .attr-name {
+ font-weight: bold;
+ width: 150px;
+ text-align: justify;
+ vertical-align: top;
+ }
+
+ .attr-name-wide {
+ font-weight: bold;
+ width: 230px;
+ text-align: justify;
+ }
+
+ .attr-value {
+ }
+
+ td.td-right {
+ text-align: right;
+ }
+
+ /* task */
+ .delayed {
+ color: #b94a48;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+ <h2><span class="lift:title"></span></h2>
+ </div>
+ </div>
+ <div id="content"></div>
+ </div> <!-- /container -->
+ </body>
+</html>
+