├── .gitignore ├── README.md └── slick-postgresql-redis ├── .gitignore ├── README.md ├── project ├── Build.scala ├── BuildSettings.scala ├── Dependencies.scala ├── build.properties └── plugins.sbt ├── ps-business └── src │ └── main │ └── scala │ └── me │ └── yangbajing │ └── ps │ └── business │ └── service │ ├── CacheService.scala │ └── UserService.scala ├── ps-data └── src │ ├── main │ ├── resources │ │ └── reference.conf │ └── scala │ │ └── me │ │ └── yangbajing │ │ └── ps │ │ └── data │ │ ├── PsSqlDriver.scala │ │ ├── domain │ │ ├── CaseClass.scala │ │ ├── OwnerToken.scala │ │ └── RegisterParam.scala │ │ ├── implicits │ │ ├── JsonImplicits.scala │ │ └── RecordJsonImplicits.scala │ │ ├── record │ │ ├── Credential.scala │ │ ├── DatabaseModule.scala │ │ ├── Schemas.scala │ │ └── User.scala │ │ └── repo │ │ └── UserRepo.scala │ └── test │ └── scala │ └── me │ └── yangbajing │ └── ps │ └── data │ ├── domain │ └── CaseClassTest.scala │ └── record │ └── SchemasTest.scala ├── ps-util └── src │ ├── main │ ├── resources │ │ └── reference.conf │ └── scala │ │ └── me │ │ └── yangbajing │ │ └── ps │ │ ├── setting │ │ └── SettingComponent.scala │ │ ├── types │ │ └── OwnerStatus.scala │ │ └── util │ │ ├── Constants.scala │ │ ├── RandomUtils.scala │ │ ├── Utils.scala │ │ └── enums │ │ └── JsValueEnumUtils.scala │ └── test │ └── scala │ └── me │ └── yangbajing │ ├── UnitPlaySpec.scala │ └── UnitSpec.scala └── ps-web ├── app ├── controllers │ ├── BaseController.scala │ ├── PageController.scala │ ├── UserAction.scala │ ├── WebTools.scala │ └── inapi │ │ └── UserController.scala ├── utils │ ├── ErrorHandler.scala │ └── Filters.scala └── views │ ├── _layout.scala.html │ ├── account │ ├── signin.scala.html │ └── signup.scala.html │ └── home │ └── main.scala.html ├── conf ├── application.conf ├── logback.xml └── routes └── static ├── README ├── gulpfile.js ├── package.json └── src └── site ├── img └── favicon.jpg ├── js ├── sign.js └── static.js └── scss ├── bootstrap ├── _alerts.scss ├── _badges.scss ├── _breadcrumbs.scss ├── _button-groups.scss ├── _buttons.scss ├── _carousel.scss ├── _close.scss ├── _code.scss ├── _component-animations.scss ├── _dropdowns.scss ├── _forms.scss ├── _grid.scss ├── _input-groups.scss ├── _jumbotron.scss ├── _labels.scss ├── _list-group.scss ├── _media.scss ├── _messager.scss ├── _mixins.scss ├── _modals.scss ├── _navbar.scss ├── _navs.scss ├── _normalize.scss ├── _pager.scss ├── _pagination.scss ├── _panels.scss ├── _popovers.scss ├── _print.scss ├── _progress-bars.scss ├── _responsive-embed.scss ├── _responsive-utilities.scss ├── _scaffolding.scss ├── _tables.scss ├── _theme.scss ├── _thumbnails.scss ├── _tooltip.scss ├── _type.scss ├── _utilities.scss ├── _variables.scss ├── _wells.scss ├── bootstrap.scss └── mixins │ ├── _alerts.scss │ ├── _background-variant.scss │ ├── _border-radius.scss │ ├── _buttons.scss │ ├── _center-block.scss │ ├── _clearfix.scss │ ├── _forms.scss │ ├── _gradients.scss │ ├── _grid-framework.scss │ ├── _grid.scss │ ├── _hide-text.scss │ ├── _image.scss │ ├── _labels.scss │ ├── _list-group.scss │ ├── _nav-divider.scss │ ├── _nav-vertical-align.scss │ ├── _opacity.scss │ ├── _pagination.scss │ ├── _panels.scss │ ├── _progress-bar.scss │ ├── _reset-filter.scss │ ├── _resize.scss │ ├── _responsive-visibility.scss │ ├── _size.scss │ ├── _tab-focus.scss │ ├── _table-row.scss │ ├── _text-emphasis.scss │ ├── _text-overflow.scss │ └── _vendor-prefixes.scss └── static.scss /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | .travis.yml 4 | 5 | node_modules/ 6 | 7 | # Ignore Play! working directory # 8 | bin/ 9 | /db 10 | /public/ 11 | .eclipse 12 | /lib/ 13 | /logs/ 14 | /modules 15 | /project/project 16 | /project/target 17 | target/ 18 | tmp/ 19 | test-result 20 | server.pid 21 | *.iml 22 | *.eml 23 | /dist/ 24 | .cache 25 | /ps-web/logs/ 26 | /ps-web/public/ 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # play-seed 2 | 3 | - 作者:杨景(羊八井) 4 | - 邮箱:yangbajing@gmail.com 5 | - 主页:http://www.yangbajing.me 6 | - 微博:http://weibo.com/yangbajing 7 | 8 | 9 | ## 示例工程 10 | 11 | - [slick-postgresql-redis](slick-postgresql-redis) 12 | 13 | -------------------------------------------------------------------------------- /slick-postgresql-redis/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | .travis.yml 4 | 5 | node_modules/ 6 | 7 | # Ignore Play! working directory # 8 | bin/ 9 | /db 10 | /public/ 11 | .eclipse 12 | /lib/ 13 | /logs/ 14 | /modules 15 | /project/project 16 | /project/target 17 | target/ 18 | tmp/ 19 | test-result 20 | server.pid 21 | *.iml 22 | *.eml 23 | /dist/ 24 | .cache 25 | /ps-web/logs/ 26 | /ps-web/public/ 27 | -------------------------------------------------------------------------------- /slick-postgresql-redis/README.md: -------------------------------------------------------------------------------- 1 | # play-seed 2 | 3 | - 作者:杨景(羊八井) 4 | - 邮箱:yangbajing@gmail.com 5 | - 主页:http://www.yangbajing.me 6 | - 微博:http://weibo.com/yangbajing 7 | 8 | - play 2.5 9 | - slick 3.1 10 | - slick-pg 0.14 11 | - scalatest 2.6 12 | - gulp 13 | - redisreact 3.1 14 | - postgresql 9.4 15 | 16 | 实现了简单的用户认证和`session`控制功能。采用`redis`来保存`session`值。自定义`play action`和`play filter`来判断`session`有效性和重设`session`。`session`使用`cookie`实现。 17 | 18 | 数据库层,使用`slick`和`slick-pg`来连接`PostgreSQL`。由`typesafe config`来定义连接参数。 19 | `[ps-util/src/main/resources/reference.conf](https://github.com/yangbajing/play-seed/blob/master/ps-util/src/main/resources/reference.conf)`。 20 | 21 | 定义play监听端口:`PlayKeys.playDefaultPort := 58082`。 22 | 23 | 修改相关配置后,执行如下命令生成数据库实例并启动服务: 24 | 25 | ``` 26 | ./sbt 27 | play-seed-root > ps-data/testOnly me.yangbajing.ps.data.record.SchemasTest 28 | play-seed-root > project ps-web 29 | [ps-web] $ ps-web/run 30 | ``` 31 | 32 | 前端代码使用gulp管理,代码放在`static`目录。编译后的静态文件将保存在`public` 33 | 34 | ``` 35 | npm install 36 | npm run build 37 | ``` 38 | 39 | 访问:[http://localhost:58082/account/signup](http://localhost:58082/account/signup) 注册账号 40 | -------------------------------------------------------------------------------- /slick-postgresql-redis/project/Build.scala: -------------------------------------------------------------------------------- 1 | import _root_.sbt.Keys._ 2 | import _root_.sbt._ 3 | import play.sbt.Play.autoImport._ 4 | import play.sbt.PlayScala 5 | import play.twirl.sbt.Import.TwirlKeys 6 | 7 | object Build extends Build { 8 | 9 | import BuildSettings._ 10 | import Dependencies._ 11 | 12 | override lazy val settings = super.settings :+ { 13 | shellPrompt := (s => Project.extract(s).currentProject.id + " > ") 14 | } 15 | 16 | /////////////////////////////////////////////////////////////// 17 | // playSeed project 18 | /////////////////////////////////////////////////////////////// 19 | lazy val playSeed = Project("play-seed-root", file(".")) 20 | .aggregate(psWeb, psBusiness, psData, psUtil) 21 | 22 | /////////////////////////////////////////////////////////////// 23 | // projects 24 | /////////////////////////////////////////////////////////////// 25 | lazy val psWeb = Project("ps-web", file("ps-web")) 26 | .enablePlugins(PlayScala) 27 | .dependsOn(psBusiness, psData) 28 | .settings(basicSettings: _*) 29 | .settings( 30 | TwirlKeys.templateImports ++= Seq("me.yangbajing.ps.data.record._", "me.yangbajing.ps.data.domain._"), 31 | PlayKeys.playDefaultPort := 58082, 32 | libraryDependencies ++= ( 33 | __compile(_scalaModules) ++ 34 | __compile(_scalaLogging) ++ 35 | __compile(json) ++ 36 | __compile(ws) ++ 37 | __compile(_slf4j) ++ 38 | __compile(_logback)) 39 | ) 40 | 41 | lazy val psBusiness = Project("ps-business", file("ps-business")) 42 | .dependsOn(psData, psUtil % DependsConfigure) 43 | .settings(basicSettings: _*) 44 | .settings( 45 | libraryDependencies ++= ( 46 | __provided(_play) ++ 47 | __compile(_redisClient))) 48 | 49 | lazy val psData = Project("ps-data", file("ps-data")) 50 | .dependsOn(psUtil % DependsConfigure) 51 | .settings(basicSettings: _*) 52 | .settings( 53 | libraryDependencies ++= ( 54 | __provided(json) ++ 55 | __provided(ws) ++ 56 | __provided(_play) ++ 57 | __compile(_postgresql) ++ 58 | __compile(_hikariCP) ++ 59 | __compile(_slick) ++ 60 | __compile(_slickPg) ++ 61 | __compile(_playSlick))) 62 | 63 | lazy val psUtil = Project("ps-util", file("ps-util")) 64 | .settings(basicSettings: _*) 65 | .settings( 66 | libraryDependencies ++= ( 67 | __provided(_commonCodecs) ++ 68 | __provided(_commonsLang3) ++ 69 | __provided(json) ++ 70 | __provided(ws) ++ 71 | __provided(_play) ++ 72 | __compile(_bouncycastle) ++ 73 | __compile(_akkaActor) ++ 74 | __compile(_akkaSlf4j))) 75 | 76 | } 77 | 78 | -------------------------------------------------------------------------------- /slick-postgresql-redis/project/BuildSettings.scala: -------------------------------------------------------------------------------- 1 | import Dependencies._ 2 | import sbt.Keys._ 3 | import sbt._ 4 | 5 | object BuildSettings { 6 | 7 | val DependsConfigure = "compile->compile;test->test" 8 | 9 | def basicSettings = Seq( 10 | version := "2.0.0", 11 | homepage := Some(new URL("https://github.com/yangbajing/play-seed")), 12 | organization := "me.yangbajing", 13 | organizationHomepage := Some(new URL("http://www.yangbajing.me")), 14 | startYear := Some(2015), 15 | scalaVersion := "2.11.8", 16 | scalacOptions := Seq( 17 | "-encoding", "utf8", 18 | //"-Ylog-classpath", 19 | "-feature", 20 | "-unchecked", 21 | "-deprecation"/*, 22 | "-explaintypes", 23 | "-Yno-adapted-args", 24 | "-Ywarn-dead-code", 25 | "-Ywarn-unused"*/ 26 | ), 27 | javacOptions := Seq( 28 | "-encoding", "utf8", 29 | "-deprecation", 30 | "-Xlint:unchecked", 31 | "-Xlint:deprecation" 32 | ), 33 | libraryDependencies ++= ( 34 | __compile(_slf4j) ++ 35 | __compile(_logback) ++ 36 | __compile(_typesafeConfig) ++ 37 | __compile(_scalaLogging) ++ 38 | __compile(_scala) ++ 39 | __test(_playTest) ++ 40 | __test(_scalatestplusPlay) ++ 41 | __test(_scalatest)), 42 | libraryDependencies ~= { 43 | _ map { 44 | case m if m.organization == "com.typesafe.play" => 45 | m.exclude("org.scala-lang", "scala-library"). 46 | // exclude("commons-logging", "commons-logging"). 47 | // exclude("com.google.guava", "guava"). 48 | // exclude("org.asynchttpclient", "async-http-client"). 49 | // exclude("com.typesafe.akka", "akka-actor"). 50 | // exclude("com.typesafe.akka", "akka-slf4j"). 51 | // exclude("org.scala-lang", "scala-compiler"). 52 | // exclude("org.scala-lang", "scala-reflect"). 53 | exclude("org.scala-lang.modules", "scala-xml"). 54 | exclude("org.scala-lang.modules", "scala-parser-combinators"). 55 | excludeAll() 56 | case m => m 57 | } 58 | }, 59 | sources in(Compile, doc) := Seq.empty, 60 | publishArtifact in(Compile, packageDoc) := false, 61 | publishArtifact in packageDoc := false, 62 | fork := true 63 | ) 64 | 65 | lazy val noPublishing = Seq( 66 | publish := (), 67 | publishLocal := () 68 | ) 69 | 70 | } 71 | 72 | -------------------------------------------------------------------------------- /slick-postgresql-redis/project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import play.core.PlayVersion 3 | 4 | object Dependencies { 5 | def __compile(dep: ModuleID): Seq[ModuleID] = __compile(Seq(dep)) 6 | 7 | def __compile(deps: Seq[ModuleID]): Seq[ModuleID] = deps map (_ % "compile") 8 | 9 | def __provided(dep: ModuleID): Seq[ModuleID] = __provided(Seq(dep)) 10 | 11 | def __provided(deps: Seq[ModuleID]): Seq[ModuleID] = deps map (_ % "provided") 12 | 13 | def __test(dep: ModuleID): Seq[ModuleID] = __test(Seq(dep)) 14 | 15 | def __test(deps: Seq[ModuleID]): Seq[ModuleID] = deps map (_ % "test") 16 | 17 | def __runtime(dep: ModuleID): Seq[ModuleID] = __runtime(Seq(dep)) 18 | 19 | def __runtime(deps: Seq[ModuleID]): Seq[ModuleID] = deps map (_ % "runtime") 20 | 21 | def __container(dep: ModuleID): Seq[ModuleID] = __container(Seq(dep)) 22 | 23 | def __container(deps: Seq[ModuleID]): Seq[ModuleID] = deps map (_ % "container") 24 | 25 | val _scalaModules = Seq( 26 | "org.scala-lang.modules" %% "scala-xml" % "1.0.5", 27 | "org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.4" 28 | ).map(_.exclude("org.scala-lang", "scala-library"). 29 | exclude("org.scala-lang", "scala-compiler"). 30 | exclude("org.scala-lang", "scala-reflect")) 31 | 32 | private val verScala = "2.11.8" 33 | val _scala = Seq( 34 | "org.scala-lang" % "scala-library" % verScala, 35 | "org.scala-lang" % "scala-compiler" % verScala, 36 | "org.scala-lang" % "scala-reflect" % verScala 37 | ) 38 | 39 | private val verAkka = "2.4.8" 40 | val _akkaActor = "com.typesafe.akka" %% "akka-actor" % verAkka 41 | val _akkaRemote = "com.typesafe.akka" %% "akka-remote" % verAkka 42 | val _akkaSlf4j = "com.typesafe.akka" %% "akka-slf4j" % verAkka 43 | val _akkaTestkit = Seq( 44 | "com.typesafe.akka" %% "akka-testkit" % verAkka) 45 | 46 | val _akkaStream = "com.typesafe.akka" %% "akka-stream" % verAkka 47 | 48 | val slickVersion = "3.1.1" 49 | val _slick = Seq( 50 | "com.typesafe.slick" %% "slick" % slickVersion, 51 | ("com.typesafe.slick" %% "slick-hikaricp" % slickVersion).exclude("com.zaxxer", "HikariCP-java6") 52 | ) 53 | 54 | val verSlickPg = "0.14.2" 55 | val _slickPg = Seq( 56 | "com.github.tminglei" %% "slick-pg" % verSlickPg, 57 | "com.github.tminglei" %% "slick-pg_date2" % verSlickPg, 58 | ("com.github.tminglei" %% "slick-pg_play-json" % verSlickPg).exclude("com.typesafe.play", "play-json_2.11") 59 | ) 60 | 61 | val _play = "com.typesafe.play" %% "play" % PlayVersion.current 62 | val _playTest = "com.typesafe.play" %% "play-test" % PlayVersion.current 63 | val _scalatestplusPlay = ("org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1").excludeAll(ExclusionRule("com.typesafe.play")) 64 | 65 | val _playSlick = ("com.typesafe.play" %% "play-slick" % "2.0.2").excludeAll(ExclusionRule("com.typesafe.slick")) 66 | 67 | val _scalatest = "org.scalatest" %% "scalatest" % "2.2.6" 68 | 69 | val _redisClient = ("net.debasishg" %% "redisclient" % "3.1").excludeAll(ExclusionRule("com.typesafe.akka")) 70 | 71 | val _typesafeConfig = "com.typesafe" % "config" % "1.3.0" 72 | 73 | val _scalaLogging = ("com.typesafe.scala-logging" %% "scala-logging" % "3.4.0").exclude("org.scala-lang", "scala-library").exclude("org.scala-lang", "scala-reflect") 74 | 75 | val _slf4j = "org.slf4j" % "slf4j-api" % "1.7.12" 76 | 77 | val _logback = "ch.qos.logback" % "logback-classic" % "1.1.3" 78 | 79 | val _bouncycastle = "org.bouncycastle" % "bcprov-jdk15on" % "1.54" 80 | 81 | val _commonsLang3 = "org.apache.commons" % "commons-lang3" % "3.4" 82 | 83 | val _commonCodecs = "commons-codec" % "commons-codec" % "1.10" 84 | 85 | val _postgresql = "org.postgresql" % "postgresql" % "9.4.1209" 86 | 87 | val _hikariCP = "com.zaxxer" % "HikariCP" % "2.4.6" 88 | 89 | val _patchca = "com.github.bingoohuang" % "patchca" % "0.0.1" 90 | } 91 | 92 | -------------------------------------------------------------------------------- /slick-postgresql-redis/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.0.0 2 | -------------------------------------------------------------------------------- /slick-postgresql-redis/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.play" %% "sbt-plugin" % "2.5.4") 2 | 3 | addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.8.2") -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-business/src/main/scala/me/yangbajing/ps/business/service/CacheService.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.business.service 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import akka.actor.ActorSystem 6 | import com.redis.RedisClientPool 7 | import me.yangbajing.ps.setting.SettingComponent 8 | 9 | import scala.concurrent.duration._ 10 | 11 | /** 12 | * 缓存服务 13 | * Created by jingyang on 15/7/17. 14 | */ 15 | @Singleton 16 | class CacheService @Inject()(settingComponent: SettingComponent) 17 | (implicit actorSystem: ActorSystem) { 18 | private val pool = 19 | new RedisClientPool(settingComponent.cache.host, settingComponent.cache.port, settingComponent.cache.database) 20 | 21 | def remove(key: String) = pool.withClient(client => client.del(key)) 22 | 23 | def getString(key: String): Option[String] = pool.withClient(client => client.get(key)) 24 | 25 | def set(key: String, value: String, duration: Duration): Boolean = 26 | pool.withClient(client => client.setex(key, duration.toSeconds.toInt, value)) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-business/src/main/scala/me/yangbajing/ps/business/service/UserService.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.business.service 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import me.yangbajing.ps.data.domain.RegisterParam 6 | import me.yangbajing.ps.data.repo.UserRepo 7 | 8 | import scala.concurrent.{ExecutionContext, Future} 9 | 10 | /** 11 | * 用户服务 12 | * Created by jingyang on 15/7/16. 13 | */ 14 | @Singleton 15 | class UserService @Inject()(userRepo: UserRepo) 16 | (implicit ec: ExecutionContext) { 17 | 18 | def signin(param: RegisterParam): Future[Option[UserRepo.UserInfo]] = { 19 | if (param.email.isDefined) 20 | userRepo.signinByEmail(param.email.get, param.password) 21 | else if (param.phone.isDefined) 22 | userRepo.signinByPhone(param.phone.get, param.password) 23 | else 24 | throw new IllegalArgumentException("email和phone至少传一个") 25 | } 26 | 27 | def register(param: RegisterParam): Future[UserRepo.UserInfo] = { 28 | userRepo.register(param.email, param.phone, param.password) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-data/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | slick.dbs { 2 | default { 3 | driver = "me.yangbajing.ps.data.PsSqlDriver$" 4 | db { 5 | dataSourceClass = "org.postgresql.ds.PGSimpleDataSource" 6 | driver = "org.postgresql.Driver" 7 | properties = { 8 | serverName = "localhost" 9 | databaseName = "play_seed" 10 | user = "playseeduser" 11 | password = "playseedpass" 12 | } 13 | numThreads = 10 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-data/src/main/scala/me/yangbajing/ps/data/PsSqlDriver.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.data 2 | 3 | import com.github.tminglei.slickpg._ 4 | import com.github.tminglei.slickpg.utils.SimpleArrayUtils 5 | import me.yangbajing.ps.types.OwnerStatus 6 | import play.api.libs.json.{JsValue, Json} 7 | import slick.driver.JdbcDriver 8 | 9 | /** 10 | * Slick JDBC Driver 11 | * Created by jingyang on 15/7/16. 12 | */ 13 | 14 | trait CustomColumnTypes { 15 | this: JdbcDriver => 16 | 17 | trait CustomColumnsImplicits { 18 | self: API => 19 | implicit val __ownerStatusColumnType = MappedColumnType.base[OwnerStatus.Value, String](_.toString, OwnerStatus.withName) 20 | } 21 | 22 | } 23 | 24 | trait PsSqlDriver 25 | extends ExPostgresDriver 26 | with PgDate2Support 27 | with PgHStoreSupport 28 | with PgPlayJsonSupport 29 | with PgArraySupport 30 | //with PgRangeSupport 31 | //with PgSearchSupport 32 | //with PgPostGISSupport 33 | with CustomColumnTypes { 34 | 35 | object MyAPI 36 | extends API 37 | with DateTimeImplicits 38 | with ArrayImplicits 39 | with PlayJsonImplicits 40 | with HStoreImplicits 41 | with CustomColumnsImplicits { 42 | implicit val strListTypeMapper = new SimpleArrayJdbcType[String]("text").to(_.toList) 43 | implicit val playJsonArrayTypeMapper = new AdvancedArrayJdbcType[JsValue]( 44 | pgjson, 45 | (s) => SimpleArrayUtils.fromString[JsValue](Json.parse)(s).orNull, 46 | (v) => SimpleArrayUtils.mkString[JsValue](_.toString())(v)) 47 | .to(_.toList) 48 | } 49 | 50 | override def pgjson: String = "jsonb" 51 | 52 | override val api = MyAPI 53 | 54 | val plainAPI = new API with Date2DateTimePlainImplicits 55 | with SimpleArrayPlainImplicits 56 | with PlayJsonPlainImplicits 57 | with SimpleHStorePlainImplicits 58 | with CustomColumnsImplicits 59 | 60 | } 61 | 62 | object PsSqlDriver extends PsSqlDriver 63 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-data/src/main/scala/me/yangbajing/ps/data/domain/CaseClass.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.data.domain 2 | 3 | import play.api.libs.json.Json 4 | 5 | /** 6 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-01-04. 7 | */ 8 | case class CaseClass(a: String, 9 | b: String = "", 10 | c: String = "", 11 | d: String = "", 12 | e: String = "", 13 | f: String = "", 14 | g: String = "", 15 | h: String = "", 16 | i: String = "", 17 | j: String = "", 18 | k: String = "", 19 | // l: String = "", 20 | // m: String = "", 21 | // n: String = "", 22 | // o: String = "", 23 | // p: String = "", 24 | // q: String = "", 25 | // r: String = "", 26 | // s: String = "", 27 | // t: String = "", 28 | // u: String = "", 29 | // v: String = "", 30 | // w: String = "", 31 | // x: String = "", 32 | // y: String = "", 33 | z: String = "") { 34 | def this() { 35 | this("") 36 | } 37 | } 38 | 39 | object CaseClass { 40 | implicit val jsonFormats = Json.format[CaseClass] 41 | } 42 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-data/src/main/scala/me/yangbajing/ps/data/domain/OwnerToken.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.data.domain 2 | 3 | import me.yangbajing.ps.data.implicits.JsonImplicits 4 | import me.yangbajing.ps.util.Constants 5 | import play.api.libs.json.Json 6 | 7 | case class OwnerToken(ownerId: Long, email: Option[String], phone: Option[String], nick: Option[String]) { 8 | def toValue: String = ownerId.toString + Constants.SPLIT_CHAR + 9 | email.getOrElse("") + Constants.SPLIT_CHAR + 10 | phone.getOrElse("") + Constants.SPLIT_CHAR + 11 | nick.getOrElse("") + Constants.SPLIT_CHAR 12 | 13 | override def toString: String = Json.stringify(Json.toJson(this)(JsonImplicits.__userTokenFormats)) 14 | } 15 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-data/src/main/scala/me/yangbajing/ps/data/domain/RegisterParam.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.data.domain 2 | 3 | case class RegisterParam(email: Option[String], phone: Option[String], password: String) 4 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-data/src/main/scala/me/yangbajing/ps/data/implicits/JsonImplicits.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.data.implicits 2 | 3 | import me.yangbajing.ps.data.domain.{RegisterParam, OwnerToken} 4 | import play.api.libs.json.{Format, Json} 5 | 6 | trait JsonImplicits extends RecordJsonImplicits { 7 | implicit def __registerParamFormats: Format[RegisterParam] = Json.format[RegisterParam] 8 | implicit def __userTokenFormats: Format[OwnerToken] = Json.format[OwnerToken] 9 | } 10 | 11 | object JsonImplicits extends JsonImplicits 12 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-data/src/main/scala/me/yangbajing/ps/data/implicits/RecordJsonImplicits.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.data.implicits 2 | 3 | import me.yangbajing.ps.data.record.User 4 | import play.api.libs.json.{Json, Format} 5 | 6 | /** 7 | * play json format 8 | * Created by jingyang on 15/7/16. 9 | */ 10 | trait RecordJsonImplicits { 11 | implicit def __userFormat: Format[User] = Json.format[User] 12 | } 13 | 14 | object RecordJsonImplicits extends RecordJsonImplicits 15 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-data/src/main/scala/me/yangbajing/ps/data/record/Credential.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.data.record 2 | 3 | import java.time.LocalDateTime 4 | 5 | /** 6 | * 保存账号、密码等敏感信息 7 | * Created by jingyang on 15/7/16. 8 | */ 9 | case class Credential(id: Option[Long], 10 | email: Option[String], 11 | phone: Option[String], 12 | salt: Array[Byte], 13 | password: Array[Byte], 14 | created_at: LocalDateTime) 15 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-data/src/main/scala/me/yangbajing/ps/data/record/DatabaseModule.scala: -------------------------------------------------------------------------------- 1 | //package me.yangbajing.ps.data.record 2 | // 3 | //import java.time.LocalDateTime 4 | // 5 | //import me.yangbajing.ps.data.PsSqlDriver$ 6 | //import me.yangbajing.ps.types.OwnerStatus._ 7 | //import play.api.libs.json.JsValue 8 | // 9 | ///** 10 | // * Created by Yang Jing (yangbajing@gmail.com) on 2016-07-27. 11 | // */ 12 | //trait DatabaseModule { 13 | // val driver: PsSqlDriver 14 | // 15 | // import driver.api._ 16 | // 17 | // class TableUser(tag: Tag) extends Table[User](tag, "t_user") { 18 | // val id = column[Long]("id", O.PrimaryKey) 19 | // val attrs = column[JsValue]("attrs") 20 | // val status = column[OwnerStatus]("status") 21 | // val created_at = column[LocalDateTime]("created_at") 22 | // 23 | // def * = (id, attrs, status, created_at) <> (User.tupled, User.unapply) 24 | // } 25 | // 26 | // class TableCredential(tag: Tag) extends Table[Credential](tag, "t_credential") { 27 | // val id = column[Long]("id", O.PrimaryKey, O.AutoInc) 28 | // val email = column[Option[String]]("email") 29 | // val phone = column[Option[String]]("phone") 30 | // val salt = column[Array[Byte]]("salt") 31 | // val password = column[Array[Byte]]("password") 32 | // val created_at = column[LocalDateTime]("created_at") 33 | // 34 | // def * = (id.?, email, phone, salt, password, created_at) <> (Credential.tupled, Credential.unapply) 35 | // } 36 | // 37 | // def tCredential = TableQuery[TableCredential] 38 | // 39 | // def tUser = TableQuery[TableUser] 40 | // 41 | // def schema = 42 | // tCredential.schema ++ 43 | // tUser.schema 44 | //} 45 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-data/src/main/scala/me/yangbajing/ps/data/record/Schemas.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.data.record 2 | 3 | import java.time.LocalDateTime 4 | import java.util.concurrent.TimeUnit 5 | import javax.inject.{Inject, Singleton} 6 | 7 | import me.yangbajing.ps.data.PsSqlDriver 8 | import me.yangbajing.ps.types.OwnerStatus.OwnerStatus 9 | import play.api.db.slick.DatabaseConfigProvider 10 | import play.api.libs.json.JsValue 11 | 12 | import scala.concurrent.Await 13 | import scala.concurrent.duration.{Duration, FiniteDuration} 14 | 15 | @Singleton 16 | class Schemas @Inject()(databaseConfigProvider: DatabaseConfigProvider) /* extends DatabaseModule*/ { 17 | val databaseConfig = databaseConfigProvider.get[PsSqlDriver] 18 | val driver: PsSqlDriver = databaseConfig.driver 19 | 20 | import driver.api._ 21 | 22 | def exec[R](action: DBIOAction[R, NoStream, Nothing])(implicit duration: FiniteDuration = Duration(10, TimeUnit.SECONDS)) = { 23 | Await.result(databaseConfig.db.run(action), duration) 24 | } 25 | 26 | def run[R](action: DBIOAction[R, NoStream, Nothing]) = { 27 | databaseConfig.db.run(action) 28 | } 29 | 30 | class TableUser(tag: Tag) extends Table[User](tag, "t_user") { 31 | val id = column[Long]("id", O.PrimaryKey) 32 | val attrs = column[JsValue]("attrs") 33 | val status = column[OwnerStatus]("status") 34 | val created_at = column[LocalDateTime]("created_at") 35 | 36 | def * = (id, attrs, status, created_at) <> (User.tupled, User.unapply) 37 | } 38 | 39 | class TableCredential(tag: Tag) extends Table[Credential](tag, "t_credential") { 40 | val id = column[Long]("id", O.PrimaryKey, O.AutoInc) 41 | val email = column[Option[String]]("email") 42 | val phone = column[Option[String]]("phone") 43 | val salt = column[Array[Byte]]("salt") 44 | val password = column[Array[Byte]]("password") 45 | val created_at = column[LocalDateTime]("created_at") 46 | 47 | def * = (id.?, email, phone, salt, password, created_at) <> (Credential.tupled, Credential.unapply) 48 | } 49 | 50 | def tCredential = TableQuery[TableCredential] 51 | 52 | def tUser = TableQuery[TableUser] 53 | 54 | def schema = 55 | tCredential.schema ++ 56 | tUser.schema 57 | 58 | } 59 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-data/src/main/scala/me/yangbajing/ps/data/record/User.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.data.record 2 | 3 | import java.time.LocalDateTime 4 | 5 | import me.yangbajing.ps.data.implicits.RecordJsonImplicits 6 | import me.yangbajing.ps.types.OwnerStatus._ 7 | import play.api.libs.json.{JsValue, Json} 8 | 9 | /** 10 | * 用户 11 | * Created by jingyang on 15/7/16. 12 | */ 13 | case class User(id: Long, 14 | attrs: JsValue, 15 | status: OwnerStatus, 16 | created_at: LocalDateTime) { 17 | def nick = attrs.\("nick").asOpt[String].getOrElse("") 18 | 19 | override def toString: String = Json.stringify(Json.toJson(this)(RecordJsonImplicits.__userFormat)) 20 | } 21 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-data/src/main/scala/me/yangbajing/ps/data/repo/UserRepo.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.data.repo 2 | 3 | import java.time.LocalDateTime 4 | import javax.inject.Inject 5 | 6 | import me.yangbajing.ps.data.record.{Credential, Schemas, User} 7 | import me.yangbajing.ps.types.OwnerStatus 8 | import me.yangbajing.ps.util.Utils 9 | import play.api.libs.json.{JsObject, JsValue} 10 | 11 | import scala.concurrent.{ExecutionContext, Future} 12 | 13 | /** 14 | * User数据库访问 15 | * Created by jingyang on 15/7/16. 16 | */ 17 | object UserRepo { 18 | type UserInfo = (User, Option[String], Option[String]) 19 | } 20 | 21 | class UserRepo @Inject()(schemas: Schemas)(implicit ec: ExecutionContext) { 22 | 23 | import schemas.driver.api._ 24 | import schemas._ 25 | 26 | import UserRepo.UserInfo 27 | 28 | def findOneById(id: Long): Future[Option[UserInfo]] = { 29 | val q = for { 30 | c <- tCredential if c.id === id 31 | u <- tUser if u.id === c.id 32 | } yield (u, c.email, c.phone) 33 | run(q.result).map(_.headOption) 34 | } 35 | 36 | def update(id: Long, attrs: JsValue): Future[Int] = { 37 | val action = tUser.filter(_.id === id).map(_.attrs).update(attrs).transactionally 38 | run(action) 39 | } 40 | 41 | def register(email: Option[String], phone: Option[String], pwd: String): Future[UserInfo] = { 42 | require(email.isDefined || phone.isDefined, "email和phone至少指定一个") 43 | 44 | val (salt, password) = Utils.generatePassword(pwd) 45 | val c = Credential(None, email, phone, salt, password, LocalDateTime.now()) 46 | val u = for { 47 | cid <- (tCredential returning tCredential.map(_.id)) += c 48 | ret <- tUser += User(cid, JsObject(Nil), OwnerStatus.ACTIVE, c.created_at) 49 | } yield (User(cid, JsObject(Nil), OwnerStatus.ACTIVE, c.created_at), email, phone) 50 | run(u.transactionally) 51 | } 52 | 53 | def signinByEmail(email: String, pwd: String) = { 54 | signinBy(Some(email), None, pwd) 55 | } 56 | 57 | def signinByPhone(phone: String, pwd: String) = { 58 | signinBy(None, Some(phone), pwd) 59 | } 60 | 61 | private def signinBy(email: Option[String], phone: Option[String], pwd: String): Future[Option[UserInfo]] = { 62 | require(email.isDefined || phone.isDefined, "email和phone至少指定一个") 63 | 64 | val q = for { 65 | c <- tCredential if c.email === email || c.phone === phone 66 | u <- tUser if u.id === c.id 67 | } yield (u, c) 68 | 69 | run(q.result).map { 70 | case (u, c) +: _ => 71 | require(java.util.Arrays.equals(Utils.generatePassword(c.salt, pwd), c.password), "密码不匹配") 72 | Some((u, c.email, c.phone)) 73 | 74 | case _ => 75 | None 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-data/src/test/scala/me/yangbajing/ps/data/domain/CaseClassTest.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.data.domain 2 | 3 | import me.yangbajing.UnitSpec 4 | import play.api.libs.json.Json 5 | 6 | /** 7 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-01-04. 8 | */ 9 | class CaseClassTest extends UnitSpec { 10 | "CaseClassTest" should { 11 | val cc = CaseClass("哈哈") 12 | 13 | "json marshal" in { 14 | val jv = Json.toJson(cc) 15 | jv.\("a").as[String] mustBe cc.a 16 | } 17 | 18 | "json unmarshal" in { 19 | val s = """{"b":"BB","c":"CC}""" 20 | val jv = Json.parse(s) 21 | jv.\("a").asOpt[String] mustBe None 22 | jv.\("c").as[String] mustBe "CC" 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-data/src/test/scala/me/yangbajing/ps/data/record/SchemasTest.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.data.record 2 | 3 | import me.yangbajing.UnitPlaySpec 4 | import org.scalatestplus.play.OneAppPerSuite 5 | 6 | /** 7 | * Schemas Test 8 | * Created by jingyang on 15/7/17. 9 | */ 10 | class SchemasTest extends UnitPlaySpec with OneAppPerSuite { 11 | "Schemas ddl" should { 12 | val schemas = app.injector.instanceOf[Schemas] 13 | import schemas.driver.api._ 14 | 15 | "drop tables" in { 16 | val result = schemas.run(schemas.schema.drop.transactionally).futureValue 17 | result.mustBe(()) 18 | } 19 | 20 | "create tables" in { 21 | val result = schemas.run(schemas.schema.create.transactionally).futureValue 22 | result.mustBe(()) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-util/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loggers = ["akka.event.slf4j.Slf4jLogger"] 3 | loglevel = INFO 4 | fork-join-executor { 5 | # The parallelism factor is used to determine thread pool size using the 6 | # following formula: ceil(available processors * factor). Resulting size 7 | # is then bounded by the parallelism-min and parallelism-max values. 8 | parallelism-factor = 3.0 9 | 10 | # Min number of threads to cap factor-based parallelism number to 11 | parallelism-min = 8 12 | 13 | # Max number of threads to cap factor-based parallelism number to 14 | parallelism-max = 64 15 | } 16 | } 17 | 18 | play-seed { 19 | filter-after-token { 20 | match-all = false 21 | paths = ["/inapi"] 22 | exclude-paths = [ 23 | "/inapi/user/signin", 24 | "/inapi/user/signup" 25 | ] 26 | } 27 | 28 | cache { 29 | host = "localhost" 30 | port = 6379 31 | database = 0 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-util/src/main/scala/me/yangbajing/ps/setting/SettingComponent.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.setting 2 | 3 | import com.typesafe.config.{ConfigFactory, Config} 4 | import scala.collection.JavaConverters._ 5 | import scala.util.Try 6 | 7 | import javax.inject.Singleton 8 | 9 | /** 10 | * 配置文件 11 | * Created by jingyang on 15/7/16. 12 | */ 13 | @Singleton 14 | class SettingComponent { 15 | val cache = { 16 | val conf = ConfigFactory.load().getConfig("play-seed.cache") 17 | CacheSetting( 18 | getString(conf, "host").get, 19 | getInt(conf, "port").get, 20 | getInt(conf, "database").get) 21 | } 22 | 23 | val filterAfterToken = { 24 | val conf = ConfigFactory.load().getConfig("play-seed.filter-after-token") 25 | FilterAfterToken( 26 | getBoolean(conf, "match-all", false), 27 | getStringList(conf, "paths"), 28 | getStringList(conf, "exclude-paths")) 29 | } 30 | 31 | private def getStringList(conf: Config, name: String): Seq[String] = try { 32 | conf.getStringList(name).asScala 33 | } catch { 34 | case _: Exception => 35 | Nil 36 | } 37 | 38 | private def getString(conf: Config, name: String, default: => String): String = getString(conf, name).getOrElse(default) 39 | 40 | private def getString(conf: Config, name: String): Try[String] = Try(conf.getString(name)) 41 | 42 | private def getBoolean(conf: Config, name: String, default: => Boolean): Boolean = 43 | getBoolean(conf, name).getOrElse(default) 44 | 45 | private def getBoolean(conf: Config, name: String): Try[Boolean] = Try(conf.getBoolean(name)) 46 | 47 | private def getInt(conf: Config, name: String, default: => Int): Int = getInt(conf, name).getOrElse(default) 48 | 49 | private def getInt(conf: Config, name: String): Try[Int] = Try(conf.getInt(name)) 50 | } 51 | 52 | case class Setting(db: Config) 53 | 54 | case class FilterAfterToken(matchAll: Boolean, paths: Seq[String], excludePaths: Seq[String]) 55 | 56 | case class CacheSetting(host: String, port: Int, database: Int) 57 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-util/src/main/scala/me/yangbajing/ps/types/OwnerStatus.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.types 2 | 3 | import me.yangbajing.ps.util.enums.JsValueEnumUtils 4 | import play.api.libs.json.Format 5 | 6 | /** 7 | * 用户状态 8 | * Created by jingyang on 15/7/16. 9 | */ 10 | object OwnerStatus extends Enumeration { 11 | type OwnerStatus = Value 12 | 13 | val ACTIVE = Value("A") 14 | val INACTIVE = Value("I") 15 | val DELETE = Value("D") 16 | 17 | implicit def __ownerStatusFormats: Format[OwnerStatus] = JsValueEnumUtils.enumFormat(OwnerStatus) 18 | } 19 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-util/src/main/scala/me/yangbajing/ps/util/Constants.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.util 2 | 3 | import java.nio.charset.Charset 4 | 5 | /** 6 | * 常量 7 | * Created by jingyang on 15/7/16. 8 | */ 9 | object Constants { 10 | val COOKIE_MAX_AGE = 30 // 秒 11 | 12 | val CACHE_KEY_TOKEN_PREFIX = "ps:to:" 13 | 14 | val NICK = "nick" 15 | 16 | val SPLIT_CHAR = '-' 17 | 18 | val SALT_LENGTH = 12 19 | 20 | val CHARSET = Charset.forName("UTF-8") 21 | 22 | val PS_TOKEN_OWNER = "ps-to" 23 | } 24 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-util/src/main/scala/me/yangbajing/ps/util/RandomUtils.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.util 2 | 3 | import java.security.SecureRandom 4 | 5 | /** 6 | * 随机资源工具 7 | * Created by jingyang on 15/7/16. 8 | */ 9 | object RandomUtils { 10 | private val LOW = 33 11 | private val HIGH = 127 12 | private val RANDOM_STRING = (0 to 9).mkString + ('a' to 'z').mkString + ('A' to 'Z').mkString + ",.?!~`@#$%^&*()_-+=|[]{};:<>/" 13 | private val RANDOM_STRING_LENGTH = RANDOM_STRING.length 14 | 15 | val random = new SecureRandom() 16 | 17 | def randomString(size: Int): String = { 18 | assert(size > 0, s"size: $size must be > 0") 19 | (0 until size).map(_ => RANDOM_STRING.charAt(random.nextInt(RANDOM_STRING_LENGTH))).mkString 20 | } 21 | 22 | def nextPrintableChar(): Char = { 23 | (random.nextInt(HIGH - LOW) + LOW).toChar 24 | } 25 | 26 | def randomNextInt(begin: Int, bound: Int) = { 27 | random.nextInt(bound - begin) + begin 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-util/src/main/scala/me/yangbajing/ps/util/Utils.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.util 2 | 3 | import org.apache.commons.codec.digest.DigestUtils 4 | 5 | /** 6 | * 工具 7 | * Created by jingyang on 15/7/16. 8 | */ 9 | object Utils { 10 | def option[T <: CharSequence](v: T): Option[T] = Option(v).filterNot(_.length() == 0) 11 | 12 | @inline 13 | def generatePassword(pwd: String): (Array[Byte], Array[Byte]) = 14 | generatePassword(pwd.getBytes(Constants.CHARSET)) 15 | 16 | def generatePassword(pwd: Array[Byte]): (Array[Byte], Array[Byte]) = { 17 | val salt = Array.ofDim[Byte](Constants.SALT_LENGTH) 18 | RandomUtils.random.nextBytes(salt) 19 | val digest = DigestUtils.getSha256Digest 20 | digest.update(salt) 21 | digest.update(pwd) 22 | (salt, digest.digest()) 23 | } 24 | 25 | @inline 26 | def generatePassword(salt: Array[Byte], pwd: String): Array[Byte] = 27 | generatePassword(salt, pwd.getBytes(Constants.CHARSET)) 28 | 29 | def generatePassword(salt: Array[Byte], pwd: Array[Byte]): Array[Byte] = { 30 | val digest = DigestUtils.getSha256Digest 31 | digest.update(salt) 32 | digest.update(pwd) 33 | digest.digest() 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-util/src/main/scala/me/yangbajing/ps/util/enums/JsValueEnumUtils.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing.ps.util.enums 2 | 3 | import play.api.libs.json._ 4 | import scala.language.implicitConversions 5 | 6 | /** 7 | * play json enum utils 8 | * Created by jingyang on 15/7/16. 9 | */ 10 | object JsValueEnumUtils { 11 | 12 | def enumReads[E <: Enumeration](enum: E): Reads[E#Value] = 13 | new Reads[E#Value] { 14 | def reads(json: JsValue): JsResult[E#Value] = json match { 15 | case JsString(s) => { 16 | try { 17 | JsSuccess(enum.withName(s)) 18 | } catch { 19 | case _: NoSuchElementException => 20 | JsError(s"Enumeration expected of type: '${enum.getClass}', but it does not appear to contain the value: '$s'") 21 | } 22 | } 23 | case _ => JsError("String value expected") 24 | } 25 | } 26 | 27 | def enumReadsId[E <: Enumeration](enum: E): Reads[E#Value] = 28 | new Reads[E#Value] { 29 | def reads(json: JsValue): JsResult[E#Value] = json match { 30 | case JsNumber(s) => { 31 | try { 32 | JsSuccess(enum(s.toInt)) 33 | } catch { 34 | case _: NoSuchElementException => 35 | JsError(s"Enumeration expected of type: '${enum.getClass}', but it does not appear to contain the value: '$s'") 36 | } 37 | } 38 | case _ => JsError("String value expected") 39 | } 40 | } 41 | 42 | implicit def enumWrites[E <: Enumeration]: Writes[E#Value] = 43 | new Writes[E#Value] { 44 | def writes(v: E#Value): JsValue = JsString(v.toString) 45 | } 46 | 47 | implicit def enumFormat[E <: Enumeration](enum: E): Format[E#Value] = { 48 | Format(enumReads(enum), enumWrites) 49 | } 50 | 51 | def enumWritesNumber[E <: Enumeration]: Writes[E#Value] = 52 | new Writes[E#Value] { 53 | def writes(v: E#Value): JsValue = JsNumber(v.id) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-util/src/test/scala/me/yangbajing/UnitPlaySpec.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing 2 | 3 | import org.scalatest.concurrent.ScalaFutures 4 | import org.scalatest.time.{Millis, Seconds, Span} 5 | import org.scalatestplus.play.PlaySpec 6 | 7 | /** 8 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-07-27. 9 | */ 10 | trait UnitPlaySpec extends PlaySpec with ScalaFutures { 11 | 12 | override implicit def patienceConfig: PatienceConfig = 13 | PatienceConfig(timeout = scaled(Span(90, Seconds)), interval = scaled(Span(100, Millis))) 14 | 15 | } 16 | 17 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-util/src/test/scala/me/yangbajing/UnitSpec.scala: -------------------------------------------------------------------------------- 1 | package me.yangbajing 2 | 3 | import org.scalatest._ 4 | import org.scalatest.concurrent.ScalaFutures 5 | import org.scalatest.time.{Millis, Seconds, Span} 6 | 7 | trait UnitSpec extends WordSpec with MustMatchers with OptionValues with ScalaFutures { 8 | 9 | override implicit def patienceConfig: PatienceConfig = 10 | PatienceConfig(timeout = scaled(Span(90, Seconds)), interval = scaled(Span(100, Millis))) 11 | 12 | } 13 | 14 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/app/controllers/BaseController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import com.typesafe.scalalogging.LazyLogging 4 | import me.yangbajing.ps.data.implicits.JsonImplicits 5 | import play.api.libs.concurrent.Execution 6 | import play.api.mvc.Controller 7 | 8 | trait BaseController extends Controller with JsonImplicits with LazyLogging { 9 | this: Controller => 10 | 11 | implicit def __ec = Execution.Implicits.defaultContext 12 | } 13 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/app/controllers/PageController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import play.api.Environment 6 | import play.api.http.Writeable 7 | import play.api.mvc.{Action, Controller} 8 | 9 | /** 10 | * 页面 11 | * Created by jingyang on 15/7/17. 12 | */ 13 | @Singleton 14 | class PageController @Inject()(webTools: WebTools, 15 | environment: Environment, 16 | userAction: UserAction 17 | ) extends Controller with BaseController { 18 | 19 | def signin = signPage(views.html.account.signin()) 20 | 21 | def signup = signPage(views.html.account.signup()) 22 | 23 | def homeMain = userAction { request => 24 | Ok(views.html.home.main(request.userToken)) 25 | } 26 | 27 | private def signPage[C](content: C)(implicit writeable: Writeable[C]) = Action { implicit request => 28 | webTools.getUserToken 29 | .map { 30 | case Right(_) => 31 | Redirect(controllers.routes.PageController.homeMain()) 32 | case Left(key) => 33 | Ok(content).withCookies(webTools.discardingSession(key)) 34 | } 35 | .getOrElse(Ok(content)) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/app/controllers/UserAction.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.Inject 4 | 5 | import me.yangbajing.ps.business.service.CacheService 6 | import me.yangbajing.ps.data.domain.OwnerToken 7 | import play.api.mvc._ 8 | 9 | import scala.concurrent.Future 10 | 11 | case class UserTokenRequest[A](userToken: OwnerToken, request: Request[A]) extends WrappedRequest[A](request) 12 | 13 | class UserAction @Inject()(cacheService: CacheService, 14 | webTools: WebTools 15 | ) extends ActionBuilder[UserTokenRequest] with ActionRefiner[Request, UserTokenRequest] { 16 | 17 | override protected def refine[A](request: Request[A]): Future[Either[Result, UserTokenRequest[A]]] = { 18 | val result = webTools.getUserToken(request) 19 | .map { 20 | case Right(token) => 21 | Right(UserTokenRequest(token, request)) 22 | case Left(key) => 23 | cacheService.remove(key) 24 | Left(Results.Redirect(controllers.routes.PageController.signin())) 25 | } 26 | .getOrElse(Left(Results.Redirect(controllers.routes.PageController.signin()))) 27 | Future.successful(result) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/app/controllers/WebTools.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import java.util.concurrent.TimeUnit 4 | import javax.inject.{Inject, Singleton} 5 | 6 | import me.yangbajing.ps.business.service.CacheService 7 | import me.yangbajing.ps.data.domain.OwnerToken 8 | import me.yangbajing.ps.util.Constants 9 | import org.apache.commons.codec.digest.DigestUtils 10 | import play.api.libs.concurrent.Execution.Implicits.defaultContext 11 | import play.api.mvc.{Cookie, DiscardingCookie, RequestHeader} 12 | 13 | import scala.concurrent.Future 14 | import scala.concurrent.duration.Duration 15 | import scala.util.Random 16 | 17 | /** 18 | * cache放到redis里。 19 | */ 20 | @Singleton 21 | class WebTools @Inject()(cacheService: CacheService) { 22 | /** 23 | * 24 | * @param request 25 | * @return 26 | * case Some(_) => 27 | * future map { 28 | * case Right(_) => 成功获取到OwnerToken 29 | * case Left(key) => key无效 30 | * } 31 | * case None => token key's value 未找到 32 | */ 33 | def getUserToken(implicit request: RequestHeader): Option[Either[String, OwnerToken]] = { 34 | val str = request.cookies.find(_.name == Constants.PS_TOKEN_OWNER).map(_.value) orElse 35 | request.headers.get(Constants.PS_TOKEN_OWNER) orElse 36 | request.getQueryString(Constants.PS_TOKEN_OWNER) 37 | 38 | str.map { key => 39 | cacheService.getString(Constants.CACHE_KEY_TOKEN_PREFIX + key) match { 40 | case Some(value) => { 41 | val vc = value.split(Constants.SPLIT_CHAR).toVector 42 | val token = OwnerToken(vc(0).toLong, vecGet(vc, 1), vecGet(vc, 2), vecGet(vc, 3)) 43 | Right(token) 44 | } 45 | case None => 46 | Left(key) 47 | } 48 | } 49 | } 50 | 51 | @inline 52 | private def vecGet(vc: Vector[String], idx: Int): Option[String] = { 53 | if (vc.length > idx) Some(vc(idx)) else None 54 | } 55 | 56 | def createCookie(name: String, value: String): Cookie = { 57 | Cookie(name, value, Some(Constants.COOKIE_MAX_AGE), httpOnly = true) 58 | } 59 | 60 | def createSession(ownerToken: OwnerToken) = { 61 | val key = ownerToken.ownerId.toString + Constants.SPLIT_CHAR + 62 | DigestUtils.sha1Hex(Random.nextInt() + Random.nextString(12) + (System.currentTimeMillis() / 1000)) 63 | 64 | // XXX 设置缓存最长时间为一天,登录Session状态满一天后强制退出 65 | cacheService.set(Constants.CACHE_KEY_TOKEN_PREFIX + key, ownerToken.toValue, Duration(1, TimeUnit.DAYS)) 66 | 67 | createCookie(Constants.PS_TOKEN_OWNER, key) 68 | } 69 | 70 | def discardingSession(key: String = null): Cookie = { 71 | if (key == null) { 72 | cacheService.remove(key) 73 | } 74 | DiscardingCookie(Constants.PS_TOKEN_OWNER).toCookie 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/app/controllers/inapi/UserController.scala: -------------------------------------------------------------------------------- 1 | package controllers.inapi 2 | 3 | import javax.inject.Inject 4 | 5 | import controllers.{BaseController, WebTools} 6 | import me.yangbajing.ps.business.service.UserService 7 | import me.yangbajing.ps.data.domain.{OwnerToken, RegisterParam} 8 | import me.yangbajing.ps.util.Constants 9 | import play.api.libs.json.{JsObject, Json} 10 | import play.api.mvc.Action 11 | 12 | /** 13 | * User API 14 | * Created by jingyang on 15/7/16. 15 | */ 16 | class UserController @Inject()(userService: UserService, 17 | webTools: WebTools) extends BaseController { 18 | 19 | def signup = Action.async(parse.json.map(_.as[RegisterParam])) { request => 20 | userService.register(request.body).map { case (user, email, phone) => 21 | val bo = Json.toJson(user).asInstanceOf[JsObject] ++ Json.obj("email" -> email, "phone" -> phone) 22 | Ok(bo) 23 | } 24 | } 25 | 26 | def signin() = Action.async(parse.json.map(_.as[RegisterParam])) { request => 27 | userService.signin(request.body).map { 28 | case Some((user, email, phone)) => 29 | val bo = Json.toJson(user).asInstanceOf[JsObject] ++ Json.obj("email" -> email, "phone" -> phone) 30 | val cookie = webTools.createSession(OwnerToken(user.id, email, phone, (user.attrs \ Constants.NICK).asOpt[String])) 31 | Ok(bo).withCookies(cookie) 32 | case None => 33 | NotFound 34 | }.recover { 35 | case e: Exception => 36 | logger.debug(e.toString, e) 37 | Unauthorized 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/app/utils/ErrorHandler.scala: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import javax.inject.{Inject, Provider} 4 | 5 | import com.typesafe.scalalogging.StrictLogging 6 | import controllers.WebTools 7 | import play.api.http.DefaultHttpErrorHandler 8 | import play.api.libs.json.Json 9 | import play.api.mvc.{RequestHeader, Result, Results} 10 | import play.api.routing.Router 11 | import play.api.{Configuration, Environment, OptionalSourceMapper} 12 | 13 | import scala.concurrent.Future 14 | 15 | /** 16 | * Created by Yang Jing (yangbajing@gmail.com) on 2016-03-07. 17 | */ 18 | class ErrorHandler @Inject()(webTools: WebTools, 19 | env: Environment, 20 | config: Configuration, 21 | sourceMapper: OptionalSourceMapper, 22 | router: Provider[Router] 23 | ) extends DefaultHttpErrorHandler(env, config, sourceMapper, router) with StrictLogging { 24 | 25 | override def onServerError(request: RequestHeader, ex: Throwable): Future[Result] = { 26 | request.path match { 27 | case s if s.startsWith("/inapi/") => 28 | logger.error(ex.toString, ex) 29 | Future.successful { 30 | Results.InternalServerError(Json.obj("code" -> -1, "message" -> ex.getLocalizedMessage)). 31 | withCookies(webTools.discardingSession()) 32 | } 33 | 34 | case _ => 35 | super.onServerError(request, ex) 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/app/utils/Filters.scala: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import javax.inject.Inject 4 | 5 | import akka.stream.Materializer 6 | import com.typesafe.scalalogging.StrictLogging 7 | import controllers.WebTools 8 | import me.yangbajing.ps.setting.SettingComponent 9 | import me.yangbajing.ps.util.Constants 10 | import play.api.http.HttpFilters 11 | import play.api.mvc.{EssentialFilter, Filter, RequestHeader, Result} 12 | 13 | import scala.concurrent.{ExecutionContext, Future} 14 | 15 | class Filters @Inject()(authFilter: FilterAfterForToken) extends HttpFilters { 16 | override def filters: Seq[EssentialFilter] = 17 | Seq(authFilter) 18 | } 19 | 20 | class FilterAfterForToken @Inject()(webTools: WebTools, 21 | settingComponent: SettingComponent, 22 | implicit val mat: Materializer, 23 | implicit val ec: ExecutionContext 24 | ) extends Filter with StrictLogging { 25 | val matchAll = settingComponent.filterAfterToken.matchAll 26 | val paths = settingComponent.filterAfterToken.paths 27 | val excludePaths = settingComponent.filterAfterToken.excludePaths 28 | 29 | override def apply(nextFilter: (RequestHeader) => Future[Result])(rh: RequestHeader): Future[Result] = { 30 | nextFilter(rh).flatMap { result => 31 | logger.debug("current path: " + rh.path) 32 | rh.path match { 33 | case path if !excludePaths.contains(path) && paths.exists(apiPath => path.startsWith(apiPath)) => 34 | val tokenOpt = webTools.getUserToken(rh) 35 | logger.debug("before tokenFilter: " + tokenOpt) 36 | 37 | if (matchAll) { 38 | if (tokenOpt.isEmpty) 39 | throw new IllegalAccessException("token not exists") 40 | } 41 | 42 | Future { 43 | tokenOpt 44 | .map { 45 | case Right(token) => 46 | // XXX 重设token 47 | result.withCookies(webTools.createCookie(Constants.PS_TOKEN_OWNER, token.toValue)) 48 | 49 | case Left(key) => 50 | result.withCookies(webTools.discardingSession(key)) 51 | } 52 | .getOrElse(result) 53 | } 54 | 55 | case _ => 56 | Future.successful(result) 57 | } 58 | } 59 | } 60 | 61 | } 62 | 63 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/app/views/_layout.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String, 2 | head: Html = Html(""), 3 | foot: Html = Html(""), 4 | keywords: String = "play,scala,slick,slick-pg,postgresql,akka", 5 | description: String = "play seed")(body: Html) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | @title 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | @head 38 | 42 | 43 | 44 | @body 45 | @foot 46 | 47 | 48 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/app/views/account/signin.scala.html: -------------------------------------------------------------------------------- 1 | @() 2 | @foot = { 3 | 4 | } 5 | @views.html._layout("登陆", foot = foot) { 6 |
7 |
8 |
9 |
10 |
11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 |
19 |

20 | 21 |

22 | 没有账号,请注册 23 |

24 |
25 |
26 |
27 |
28 |
29 | } 30 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/app/views/account/signup.scala.html: -------------------------------------------------------------------------------- 1 | @() 2 | @foot = { 3 | 4 | } 5 | @views.html._layout("注册", foot = foot) { 6 |
7 |
8 |
9 |
10 |
11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 |

24 | 25 |

26 | 已有账号 27 |

28 |
29 |
30 |
31 |
32 |
33 | } 34 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/app/views/home/main.scala.html: -------------------------------------------------------------------------------- 1 | @(user: me.yangbajing.ps.data.domain.OwnerToken) 2 | @views.html._layout("用户主页") { 3 |
4 |

@user.toString

5 |
6 | } -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/conf/application.conf: -------------------------------------------------------------------------------- 1 | play.crypto.secret = "QY#q!^N|3]HzkzVdHouAeUE=cEZb+k`Hy;]N1`3nHLv'vhF=2J-K>zq-+V13./5m" 2 | play { 3 | http { 4 | errorHandler = "utils.ErrorHandler" 5 | filters = "utils.Filters" 6 | server.port = 58082 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/conf/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ${user.dir}/logs/application.log 6 | 7 | 8 | ${user.dir}/logs/application-log-%d{yyyy-MM-dd}.gz 9 | 10 | 30 11 | 12 | 13 | %date{yyyy-MM-dd HH:mm:ss ZZZZ} [%level] from %logger in %thread - %message%n%xException 14 | 15 | 16 | 17 | 18 | ${user.dir}/logs/access.log 19 | 20 | 21 | ${user.dir}/logs/access-log-%d{yyyy-MM-dd}.gz 22 | 23 | 7 24 | 25 | 26 | %date{yyyy-MM-dd HH:mm:ss ZZZZ} %message 27 | 28 | false 29 | 30 | 31 | 32 | 33 | 34 | %date{HH:mm:ss.SSS} %coloredLevel %logger{30} - %message%n%xException{10} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/conf/routes: -------------------------------------------------------------------------------- 1 | GET /assets/*file @controllers.Assets.at(path = "/public", file) 2 | GET /account/signin @controllers.PageController.signin() 3 | GET /account/signup @controllers.PageController.signup() 4 | GET /home @controllers.PageController.homeMain() 5 | POST /inapi/user/signup @controllers.inapi.UserController.signup() 6 | POST /inapi/user/signin @controllers.inapi.UserController.signin() 7 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/README: -------------------------------------------------------------------------------- 1 | # play-seed 前端代码 -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/gulpfile.js: -------------------------------------------------------------------------------- 1 | var del = require('del'), 2 | argv = require('yargs').argv, 3 | gulp = require('gulp'), 4 | replace = require('gulp-replace'), 5 | minifyCss = require('gulp-minify-css'), 6 | rename = require('gulp-rename'), 7 | sass = require('gulp-sass'), 8 | browserify = require('gulp-browserify'), 9 | uglify = require('gulp-uglify'), 10 | concat = require('gulp-concat'), 11 | twig = require('gulp-twig'); 12 | 13 | var DEV_DOMAIN = 'localhost:9000', 14 | PROD_DOMAIN = 'play-seed.yangbajing.me'; 15 | var DOMAIN = DEV_DOMAIN; 16 | var PUBLIC = '../public'; 17 | 18 | if (argv.prod || argv.test) { 19 | process.env.NODE_ENV = 'production'; 20 | DOMAIN = PROD_DOMAIN; 21 | } 22 | 23 | /********************************************************************* 24 | * vendor 25 | ********************************************************************/ 26 | gulp.task('build:vendor:all', function () { 27 | return gulp.src(['src/vendor/**/*'], {base: 'src/vendor/'}) 28 | .pipe(gulp.dest(PUBLIC + '/vendor')); 29 | }); 30 | gulp.task('build:vendor', ['build:vendor:all']); 31 | gulp.task('watch:vendor', function () { 32 | gulp.watch('src/vendor/**/*', ['build:vendor']); 33 | }); 34 | 35 | 36 | /********************************************************************* 37 | * site 38 | *********************************************************************/ 39 | gulp.task('build:site:js', function () { 40 | return gulp.src('src/site/js/**/*.js') 41 | .pipe(replace('{{psDomain}}', DOMAIN)) 42 | .pipe(browserify()) 43 | .pipe(gulp.dest(PUBLIC + '/js/')); 44 | }); 45 | 46 | //gulp.task('build:site:js:sign', function () { 47 | // return gulp.src('src/site/js/sign.js') 48 | // .pipe(browserify({transform: 'reactify'})) 49 | // .pipe(concat('sign.js')) 50 | // .pipe(gulp.dest(PUBLIC + '/js/')); 51 | //}); 52 | 53 | gulp.task('build:site:css', function () { 54 | return gulp.src('src/site/scss/*.scss') 55 | .pipe(sass()) 56 | .pipe(gulp.dest(PUBLIC + '/css')); 57 | }); 58 | 59 | gulp.task('build:site:img', function () { 60 | return gulp.src('src/site/img/**/*', {base: 'src/site/'}) 61 | .pipe(gulp.dest(PUBLIC + '/')); 62 | }); 63 | 64 | gulp.task('build:site', [/*'build:site:js:site', 'build:site:js:sign', 'build:site:js:topic', 'build:site:js:discovery',*/ 'build:site:css', 'build:site:img', 'build:site:js']); 65 | 66 | gulp.task('watch:site', function () { 67 | gulp.watch('src/site/**/*', ['build:site']); 68 | }); 69 | 70 | 71 | /********************************************************************* 72 | * utility 73 | ********************************************************************/ 74 | gulp.task('clean', function (cb) { 75 | del([PUBLIC + '/**/*'], {force: true}, cb); 76 | }); 77 | 78 | gulp.task('build', ['build:vendor', 'build:site'/*, 'build:site:views', 'build:app'*/]); 79 | 80 | gulp.task('watch', ['watch:vendor', 'watch:site'/*, 'watch:site:views', 'watch:app'*/]); 81 | 82 | gulp.task('min', function (cb) { 83 | gulp.src([ 84 | PUBLIC + '/**/*.css' 85 | ], {base: PUBLIC + '/'}) 86 | .pipe(minifyCss()) 87 | .pipe(gulp.dest('build/')); 88 | 89 | gulp.src([ 90 | PUBLIC + '/**/*.js' 91 | ], {base: PUBLIC + '/'}) 92 | .pipe(uglify()) 93 | .pipe(gulp.dest('build/')); 94 | } 95 | ); 96 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "play-seed", 3 | "version": "0.0.1", 4 | "description": "play-seed assets(js, css, img, font)", 5 | "repository": "https://github.com/yangbajing/play-seed", 6 | "engines": { 7 | "node": ">=0.12.0" 8 | }, 9 | "scripts": { 10 | "clean": "./node_modules/.bin/gulp clean", 11 | "build": "./node_modules/.bin/gulp clean && ./node_modules/.bin/gulp build", 12 | "build-test": "./node_modules/.bin/gulp clean && ./node_modules/.bin/gulp build --test && ./node_modules/.bin/gulp min", 13 | "build-prod": "./node_modules/.bin/gulp clean && ./node_modules/.bin/gulp build --prod && ./node_modules/.bin/gulp min", 14 | "watch": "./node_modules/.bin/gulp watch", 15 | "wb-start": "webpack-dev-server --hot --progress --colors", 16 | "wb-build": "webpack --progress --colors" 17 | }, 18 | "author": "yangbajing@gmail.com", 19 | "dependencies": { 20 | "bson-objectid": "1.1.1", 21 | "crypto-js": "3.1.4", 22 | "react": "0.13.3", 23 | "react-router": "0.13.3" 24 | }, 25 | "devDependencies": { 26 | "del": "1.2.0", 27 | "gulp": "3.9.0", 28 | "gulp-browserify": "0.5.1", 29 | "gulp-concat": "2.6.0", 30 | "gulp-if": "1.2.5", 31 | "gulp-minify-css": "1.2.0", 32 | "gulp-rename": "1.2.2", 33 | "gulp-replace": "0.5.3", 34 | "gulp-sass": "2.0.4", 35 | "gulp-twig": "0.3.0", 36 | "gulp-uglify": "1.2.0", 37 | "reactify": "1.1.1", 38 | "yargs": "3.15.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/src/site/img/favicon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangjing/play-seed/ed7e4610c9816233b19b7c05b0a84e177eba8418/slick-postgresql-redis/ps-web/static/src/site/img/favicon.jpg -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/src/site/js/sign.js: -------------------------------------------------------------------------------- 1 | jQuery(function () { 2 | 3 | jQuery('#signin-form').on('submit', function (evt) { 4 | evt.preventDefault(); 5 | var thiz = jQuery(this); 6 | var payload = PS.form.serialize(thiz); 7 | payload.password = PS.util.sha1Hex(payload.password); 8 | 9 | PS.ajax.post('/inapi/user/signin', payload, 'json') 10 | .done(function (data, status, jqXhr) { 11 | setTimeout(function () { 12 | window.location.href = '/home'; 13 | }, 2000); 14 | thiz.find('.text-info').html('登录成功,2秒后跳转'); 15 | }) 16 | .fail(function (data, status, jqXhr) { 17 | alert('登录失败'); 18 | }); 19 | }); 20 | 21 | jQuery('#signup-form').on('submit', function (evt) { 22 | evt.preventDefault(); 23 | var thiz = jQuery(this); 24 | var payload = PS.form.serialize(thiz); 25 | if (payload.password.length < 6 || payload.password.length > 32) { 26 | alert('密码为6至32个字符'); 27 | return; 28 | } 29 | if (payload.password !== payload.passwordRepeat) { 30 | alert('两次密码不匹配'); 31 | return; 32 | } 33 | payload.password = PS.util.sha1Hex(payload.password); 34 | payload.passwordRepeat = undefined; 35 | 36 | PS.ajax.post('/inapi/user/signup', payload, 'json') 37 | .done(function (data, status, jqXhr) { 38 | setTimeout(function () { 39 | window.location.href = '/account/signin'; 40 | }, 2000); 41 | thiz.find('.text-info').html('注册成功,2秒后跳转到登录页面'); 42 | }) 43 | .fail(function (data, status, jqXhr) { 44 | alert('注册失败'); 45 | }); 46 | }) 47 | 48 | }); 49 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/src/site/scss/bootstrap/_alerts.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Alerts 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base styles 7 | // ------------------------- 8 | 9 | .alert { 10 | padding: $alert-padding; 11 | margin-bottom: $line-height-computed; 12 | border: 1px solid transparent; 13 | border-radius: $alert-border-radius; 14 | 15 | // Headings for larger alerts 16 | h4 { 17 | margin-top: 0; 18 | // Specified for the h4 to prevent conflicts of changing $headings-color 19 | color: inherit; 20 | } 21 | 22 | // Provide class for links that match alerts 23 | .alert-link { 24 | font-weight: $alert-link-font-weight; 25 | } 26 | 27 | // Improve alignment and spacing of inner content 28 | > p, 29 | > ul { 30 | margin-bottom: 0; 31 | } 32 | 33 | > p + p { 34 | margin-top: 5px; 35 | } 36 | } 37 | 38 | // Dismissible alerts 39 | // 40 | // Expand the right padding and account for the close button's positioning. 41 | 42 | .alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0. 43 | .alert-dismissible { 44 | padding-right: ($alert-padding + 20); 45 | 46 | // Adjust close link position 47 | .close { 48 | position: relative; 49 | top: -2px; 50 | right: -21px; 51 | color: inherit; 52 | } 53 | } 54 | 55 | // Alternate styles 56 | // 57 | // Generate contextual modifier classes for colorizing the alert. 58 | 59 | .alert-success { 60 | @include alert-variant($alert-success-bg, $alert-success-border, $alert-success-text); 61 | } 62 | 63 | .alert-info { 64 | @include alert-variant($alert-info-bg, $alert-info-border, $alert-info-text); 65 | } 66 | 67 | .alert-warning { 68 | @include alert-variant($alert-warning-bg, $alert-warning-border, $alert-warning-text); 69 | } 70 | 71 | .alert-danger { 72 | @include alert-variant($alert-danger-bg, $alert-danger-border, $alert-danger-text); 73 | } 74 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/src/site/scss/bootstrap/_badges.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Badges 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base class 7 | .badge { 8 | display: inline-block; 9 | min-width: 10px; 10 | padding: 3px 7px; 11 | font-size: $font-size-small; 12 | font-weight: $badge-font-weight; 13 | color: $badge-color; 14 | line-height: $badge-line-height; 15 | vertical-align: baseline; 16 | white-space: nowrap; 17 | text-align: center; 18 | background-color: $badge-bg; 19 | border-radius: $badge-border-radius; 20 | 21 | // Empty badges collapse automatically (not available in IE8) 22 | &:empty { 23 | display: none; 24 | } 25 | 26 | // Quick fix for badges in buttons 27 | .btn & { 28 | position: relative; 29 | top: -1px; 30 | } 31 | 32 | .btn-xs &, 33 | .btn-group-xs > .btn & { 34 | top: 0; 35 | padding: 1px 5px; 36 | } 37 | 38 | // [converter] extracted a& to a.badge 39 | 40 | // Account for badges in navs 41 | .list-group-item.active > &, 42 | .nav-pills > .active > a > & { 43 | color: $badge-active-color; 44 | background-color: $badge-active-bg; 45 | } 46 | 47 | .list-group-item > & { 48 | float: right; 49 | } 50 | 51 | .list-group-item > & + & { 52 | margin-right: 5px; 53 | } 54 | 55 | .nav-pills > li > a > & { 56 | margin-left: 3px; 57 | } 58 | } 59 | 60 | // Hover state, but only for links 61 | a.badge { 62 | &:hover, 63 | &:focus { 64 | color: $badge-link-hover-color; 65 | text-decoration: none; 66 | cursor: pointer; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/src/site/scss/bootstrap/_breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Breadcrumbs 3 | // -------------------------------------------------- 4 | 5 | 6 | .breadcrumb { 7 | padding: $breadcrumb-padding-vertical $breadcrumb-padding-horizontal; 8 | margin-bottom: $line-height-computed; 9 | list-style: none; 10 | background-color: $breadcrumb-bg; 11 | border-radius: $border-radius-base; 12 | 13 | > li { 14 | display: inline-block; 15 | 16 | + li:before { 17 | content: "#{$breadcrumb-separator}\00a0"; // Unicode space added since inline-block means non-collapsing white-space 18 | padding: 0 5px; 19 | color: $breadcrumb-color; 20 | } 21 | } 22 | 23 | > .active { 24 | color: $breadcrumb-active-color; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/src/site/scss/bootstrap/_button-groups.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Button groups 3 | // -------------------------------------------------- 4 | 5 | // Make the div behave like a button 6 | .btn-group, 7 | .btn-group-vertical { 8 | position: relative; 9 | display: inline-block; 10 | vertical-align: middle; // match .btn alignment given font-size hack above 11 | > .btn { 12 | position: relative; 13 | float: left; 14 | // Bring the "active" button to the front 15 | &:hover, 16 | &:focus, 17 | &:active, 18 | &.active { 19 | z-index: 2; 20 | } 21 | } 22 | } 23 | 24 | // Prevent double borders when buttons are next to each other 25 | .btn-group { 26 | .btn + .btn, 27 | .btn + .btn-group, 28 | .btn-group + .btn, 29 | .btn-group + .btn-group { 30 | margin-left: -1px; 31 | } 32 | } 33 | 34 | // Optional: Group multiple button groups together for a toolbar 35 | .btn-toolbar { 36 | margin-left: -5px; // Offset the first child's margin 37 | @include clearfix; 38 | 39 | .btn-group, 40 | .input-group { 41 | float: left; 42 | } 43 | > .btn, 44 | > .btn-group, 45 | > .input-group { 46 | margin-left: 5px; 47 | } 48 | } 49 | 50 | .btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { 51 | border-radius: 0; 52 | } 53 | 54 | // Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match 55 | .btn-group > .btn:first-child { 56 | margin-left: 0; 57 | &:not(:last-child):not(.dropdown-toggle) { 58 | @include border-right-radius(0); 59 | } 60 | } 61 | // Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it 62 | .btn-group > .btn:last-child:not(:first-child), 63 | .btn-group > .dropdown-toggle:not(:first-child) { 64 | @include border-left-radius(0); 65 | } 66 | 67 | // Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group) 68 | .btn-group > .btn-group { 69 | float: left; 70 | } 71 | .btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { 72 | border-radius: 0; 73 | } 74 | .btn-group > .btn-group:first-child:not(:last-child) { 75 | > .btn:last-child, 76 | > .dropdown-toggle { 77 | @include border-right-radius(0); 78 | } 79 | } 80 | .btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { 81 | @include border-left-radius(0); 82 | } 83 | 84 | // On active and open, don't show outline 85 | .btn-group .dropdown-toggle:active, 86 | .btn-group.open .dropdown-toggle { 87 | outline: 0; 88 | } 89 | 90 | 91 | // Sizing 92 | // 93 | // Remix the default button sizing classes into new ones for easier manipulation. 94 | 95 | .btn-group-xs > .btn { @extend .btn-xs; } 96 | .btn-group-sm > .btn { @extend .btn-sm; } 97 | .btn-group-lg > .btn { @extend .btn-lg; } 98 | 99 | 100 | // Split button dropdowns 101 | // ---------------------- 102 | 103 | // Give the line between buttons some depth 104 | .btn-group > .btn + .dropdown-toggle { 105 | padding-left: 8px; 106 | padding-right: 8px; 107 | } 108 | .btn-group > .btn-lg + .dropdown-toggle { 109 | padding-left: 12px; 110 | padding-right: 12px; 111 | } 112 | 113 | // The clickable button for toggling the menu 114 | // Remove the gradient and set the same inset shadow as the :active state 115 | .btn-group.open .dropdown-toggle { 116 | @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); 117 | 118 | // Show no shadow for `.btn-link` since it has no other button styles. 119 | &.btn-link { 120 | @include box-shadow(none); 121 | } 122 | } 123 | 124 | 125 | // Reposition the caret 126 | .btn .caret { 127 | margin-left: 0; 128 | } 129 | // Carets in other button sizes 130 | .btn-lg .caret { 131 | border-width: $caret-width-large $caret-width-large 0; 132 | border-bottom-width: 0; 133 | } 134 | // Upside down carets for .dropup 135 | .dropup .btn-lg .caret { 136 | border-width: 0 $caret-width-large $caret-width-large; 137 | } 138 | 139 | 140 | // Vertical button groups 141 | // ---------------------- 142 | 143 | .btn-group-vertical { 144 | > .btn, 145 | > .btn-group, 146 | > .btn-group > .btn { 147 | display: block; 148 | float: none; 149 | width: 100%; 150 | max-width: 100%; 151 | } 152 | 153 | // Clear floats so dropdown menus can be properly placed 154 | > .btn-group { 155 | @include clearfix; 156 | > .btn { 157 | float: none; 158 | } 159 | } 160 | 161 | > .btn + .btn, 162 | > .btn + .btn-group, 163 | > .btn-group + .btn, 164 | > .btn-group + .btn-group { 165 | margin-top: -1px; 166 | margin-left: 0; 167 | } 168 | } 169 | 170 | .btn-group-vertical > .btn { 171 | &:not(:first-child):not(:last-child) { 172 | border-radius: 0; 173 | } 174 | &:first-child:not(:last-child) { 175 | border-top-right-radius: $border-radius-base; 176 | @include border-bottom-radius(0); 177 | } 178 | &:last-child:not(:first-child) { 179 | border-bottom-left-radius: $border-radius-base; 180 | @include border-top-radius(0); 181 | } 182 | } 183 | .btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { 184 | border-radius: 0; 185 | } 186 | .btn-group-vertical > .btn-group:first-child:not(:last-child) { 187 | > .btn:last-child, 188 | > .dropdown-toggle { 189 | @include border-bottom-radius(0); 190 | } 191 | } 192 | .btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { 193 | @include border-top-radius(0); 194 | } 195 | 196 | 197 | // Justified button groups 198 | // ---------------------- 199 | 200 | .btn-group-justified { 201 | display: table; 202 | width: 100%; 203 | table-layout: fixed; 204 | border-collapse: separate; 205 | > .btn, 206 | > .btn-group { 207 | float: none; 208 | display: table-cell; 209 | width: 1%; 210 | } 211 | > .btn-group .btn { 212 | width: 100%; 213 | } 214 | 215 | > .btn-group .dropdown-menu { 216 | left: auto; 217 | } 218 | } 219 | 220 | 221 | // Checkbox and radio options 222 | // 223 | // In order to support the browser's form validation feedback, powered by the 224 | // `required` attribute, we have to "hide" the inputs via `clip`. We cannot use 225 | // `display: none;` or `visibility: hidden;` as that also hides the popover. 226 | // Simply visually hiding the inputs via `opacity` would leave them clickable in 227 | // certain cases which is prevented by using `clip` and `pointer-events`. 228 | // This way, we ensure a DOM element is visible to position the popover from. 229 | // 230 | // See https://github.com/twbs/bootstrap/pull/12794 and 231 | // https://github.com/twbs/bootstrap/pull/14559 for more information. 232 | 233 | [data-toggle="buttons"] { 234 | > .btn, 235 | > .btn-group > .btn { 236 | input[type="radio"], 237 | input[type="checkbox"] { 238 | position: absolute; 239 | clip: rect(0,0,0,0); 240 | pointer-events: none; 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/src/site/scss/bootstrap/_buttons.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Buttons 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base styles 7 | // -------------------------------------------------- 8 | 9 | .btn { 10 | display: inline-block; 11 | margin-bottom: 0; // For input.btn 12 | font-weight: $btn-font-weight; 13 | text-align: center; 14 | vertical-align: middle; 15 | touch-action: manipulation; 16 | cursor: pointer; 17 | background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 18 | border: 1px solid transparent; 19 | white-space: nowrap; 20 | @include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, $border-radius-base); 21 | @include user-select(none); 22 | 23 | &, 24 | &:active, 25 | &.active { 26 | &:focus, 27 | &.focus { 28 | @include tab-focus; 29 | } 30 | } 31 | 32 | &:hover, 33 | &:focus, 34 | &.focus { 35 | color: $btn-default-color; 36 | text-decoration: none; 37 | } 38 | 39 | &:active, 40 | &.active { 41 | outline: 0; 42 | background-image: none; 43 | @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); 44 | } 45 | 46 | &.disabled, 47 | &[disabled], 48 | fieldset[disabled] & { 49 | cursor: $cursor-disabled; 50 | pointer-events: none; // Future-proof disabling of clicks 51 | @include opacity(.65); 52 | @include box-shadow(none); 53 | } 54 | } 55 | 56 | 57 | // Alternate buttons 58 | // -------------------------------------------------- 59 | 60 | .btn-default { 61 | @include button-variant($btn-default-color, $btn-default-bg, $btn-default-border); 62 | } 63 | .btn-primary { 64 | @include button-variant($btn-primary-color, $btn-primary-bg, $btn-primary-border); 65 | } 66 | // Success appears as green 67 | .btn-success { 68 | @include button-variant($btn-success-color, $btn-success-bg, $btn-success-border); 69 | } 70 | // Info appears as blue-green 71 | .btn-info { 72 | @include button-variant($btn-info-color, $btn-info-bg, $btn-info-border); 73 | } 74 | // Warning appears as orange 75 | .btn-warning { 76 | @include button-variant($btn-warning-color, $btn-warning-bg, $btn-warning-border); 77 | } 78 | // Danger and error appear as red 79 | .btn-danger { 80 | @include button-variant($btn-danger-color, $btn-danger-bg, $btn-danger-border); 81 | } 82 | 83 | 84 | // Link buttons 85 | // ------------------------- 86 | 87 | // Make a button look and behave like a link 88 | .btn-link { 89 | color: $link-color; 90 | font-weight: normal; 91 | border-radius: 0; 92 | 93 | &, 94 | &:active, 95 | &.active, 96 | &[disabled], 97 | fieldset[disabled] & { 98 | background-color: transparent; 99 | @include box-shadow(none); 100 | } 101 | &, 102 | &:hover, 103 | &:focus, 104 | &:active { 105 | border-color: transparent; 106 | } 107 | &:hover, 108 | &:focus { 109 | color: $link-hover-color; 110 | text-decoration: $link-hover-decoration; 111 | background-color: transparent; 112 | } 113 | &[disabled], 114 | fieldset[disabled] & { 115 | &:hover, 116 | &:focus { 117 | color: $btn-link-disabled-color; 118 | text-decoration: none; 119 | } 120 | } 121 | } 122 | 123 | 124 | // Button Sizes 125 | // -------------------------------------------------- 126 | 127 | .btn-lg { 128 | // line-height: ensure even-numbered height of button next to large input 129 | @include button-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $border-radius-large); 130 | } 131 | .btn-sm { 132 | // line-height: ensure proper height of button next to small input 133 | @include button-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $border-radius-small); 134 | } 135 | .btn-xs { 136 | @include button-size($padding-xs-vertical, $padding-xs-horizontal, $font-size-small, $line-height-small, $border-radius-small); 137 | } 138 | 139 | 140 | // Block button 141 | // -------------------------------------------------- 142 | 143 | .btn-block { 144 | display: block; 145 | width: 100%; 146 | } 147 | 148 | // Vertically space out multiple block buttons 149 | .btn-block + .btn-block { 150 | margin-top: 5px; 151 | } 152 | 153 | // Specificity overrides 154 | input[type="submit"], 155 | input[type="reset"], 156 | input[type="button"] { 157 | &.btn-block { 158 | width: 100%; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/src/site/scss/bootstrap/_carousel.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Carousel 3 | // -------------------------------------------------- 4 | 5 | 6 | // Wrapper for the slide container and indicators 7 | .carousel { 8 | position: relative; 9 | } 10 | 11 | .carousel-inner { 12 | position: relative; 13 | overflow: hidden; 14 | width: 100%; 15 | 16 | > .item { 17 | display: none; 18 | position: relative; 19 | @include transition(.6s ease-in-out left); 20 | 21 | // Account for jankitude on images 22 | > img, 23 | > a > img { 24 | @include img-responsive; 25 | line-height: 1; 26 | } 27 | 28 | // WebKit CSS3 transforms for supported devices 29 | @media all and (transform-3d), (-webkit-transform-3d) { 30 | @include transition-transform(0.6s ease-in-out); 31 | @include backface-visibility(hidden); 32 | @include perspective(1000); 33 | 34 | &.next, 35 | &.active.right { 36 | @include translate3d(100%, 0, 0); 37 | left: 0; 38 | } 39 | &.prev, 40 | &.active.left { 41 | @include translate3d(-100%, 0, 0); 42 | left: 0; 43 | } 44 | &.next.left, 45 | &.prev.right, 46 | &.active { 47 | @include translate3d(0, 0, 0); 48 | left: 0; 49 | } 50 | } 51 | } 52 | 53 | > .active, 54 | > .next, 55 | > .prev { 56 | display: block; 57 | } 58 | 59 | > .active { 60 | left: 0; 61 | } 62 | 63 | > .next, 64 | > .prev { 65 | position: absolute; 66 | top: 0; 67 | width: 100%; 68 | } 69 | 70 | > .next { 71 | left: 100%; 72 | } 73 | > .prev { 74 | left: -100%; 75 | } 76 | > .next.left, 77 | > .prev.right { 78 | left: 0; 79 | } 80 | 81 | > .active.left { 82 | left: -100%; 83 | } 84 | > .active.right { 85 | left: 100%; 86 | } 87 | 88 | } 89 | 90 | // Left/right controls for nav 91 | // --------------------------- 92 | 93 | .carousel-control { 94 | position: absolute; 95 | top: 0; 96 | left: 0; 97 | bottom: 0; 98 | width: $carousel-control-width; 99 | @include opacity($carousel-control-opacity); 100 | font-size: $carousel-control-font-size; 101 | color: $carousel-control-color; 102 | text-align: center; 103 | text-shadow: $carousel-text-shadow; 104 | // We can't have this transition here because WebKit cancels the carousel 105 | // animation if you trip this while in the middle of another animation. 106 | 107 | // Set gradients for backgrounds 108 | &.left { 109 | @include gradient-horizontal($start-color: rgba(0,0,0,.5), $end-color: rgba(0,0,0,.0001)); 110 | } 111 | &.right { 112 | left: auto; 113 | right: 0; 114 | @include gradient-horizontal($start-color: rgba(0,0,0,.0001), $end-color: rgba(0,0,0,.5)); 115 | } 116 | 117 | // Hover/focus state 118 | &:hover, 119 | &:focus { 120 | outline: 0; 121 | color: $carousel-control-color; 122 | text-decoration: none; 123 | @include opacity(.9); 124 | } 125 | 126 | // Toggles 127 | .icon-prev, 128 | .icon-next, 129 | .glyphicon-chevron-left, 130 | .glyphicon-chevron-right { 131 | position: absolute; 132 | top: 50%; 133 | z-index: 5; 134 | display: inline-block; 135 | } 136 | .icon-prev, 137 | .glyphicon-chevron-left { 138 | left: 50%; 139 | margin-left: -10px; 140 | } 141 | .icon-next, 142 | .glyphicon-chevron-right { 143 | right: 50%; 144 | margin-right: -10px; 145 | } 146 | .icon-prev, 147 | .icon-next { 148 | width: 20px; 149 | height: 20px; 150 | margin-top: -10px; 151 | line-height: 1; 152 | font-family: serif; 153 | } 154 | 155 | 156 | .icon-prev { 157 | &:before { 158 | content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039) 159 | } 160 | } 161 | .icon-next { 162 | &:before { 163 | content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A) 164 | } 165 | } 166 | } 167 | 168 | // Optional indicator pips 169 | // 170 | // Add an unordered list with the following class and add a list item for each 171 | // slide your carousel holds. 172 | 173 | .carousel-indicators { 174 | position: absolute; 175 | bottom: 10px; 176 | left: 50%; 177 | z-index: 15; 178 | width: 60%; 179 | margin-left: -30%; 180 | padding-left: 0; 181 | list-style: none; 182 | text-align: center; 183 | 184 | li { 185 | display: inline-block; 186 | width: 10px; 187 | height: 10px; 188 | margin: 1px; 189 | text-indent: -999px; 190 | border: 1px solid $carousel-indicator-border-color; 191 | border-radius: 10px; 192 | cursor: pointer; 193 | 194 | // IE8-9 hack for event handling 195 | // 196 | // Internet Explorer 8-9 does not support clicks on elements without a set 197 | // `background-color`. We cannot use `filter` since that's not viewed as a 198 | // background color by the browser. Thus, a hack is needed. 199 | // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Internet_Explorer 200 | // 201 | // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we 202 | // set alpha transparency for the best results possible. 203 | background-color: #000 \9; // IE8 204 | background-color: rgba(0,0,0,0); // IE9 205 | } 206 | .active { 207 | margin: 0; 208 | width: 12px; 209 | height: 12px; 210 | background-color: $carousel-indicator-active-bg; 211 | } 212 | } 213 | 214 | // Optional captions 215 | // ----------------------------- 216 | // Hidden by default for smaller viewports 217 | .carousel-caption { 218 | position: absolute; 219 | left: 15%; 220 | right: 15%; 221 | bottom: 20px; 222 | z-index: 10; 223 | padding-top: 20px; 224 | padding-bottom: 20px; 225 | color: $carousel-caption-color; 226 | text-align: center; 227 | text-shadow: $carousel-text-shadow; 228 | & .btn { 229 | text-shadow: none; // No shadow for button elements in carousel-caption 230 | } 231 | } 232 | 233 | 234 | // Scale up controls for tablets and up 235 | @media screen and (min-width: $screen-sm-min) { 236 | 237 | // Scale up the controls a smidge 238 | .carousel-control { 239 | .glyphicon-chevron-left, 240 | .glyphicon-chevron-right, 241 | .icon-prev, 242 | .icon-next { 243 | width: 30px; 244 | height: 30px; 245 | margin-top: -15px; 246 | font-size: 30px; 247 | } 248 | .glyphicon-chevron-left, 249 | .icon-prev { 250 | margin-left: -15px; 251 | } 252 | .glyphicon-chevron-right, 253 | .icon-next { 254 | margin-right: -15px; 255 | } 256 | } 257 | 258 | // Show and left align the captions 259 | .carousel-caption { 260 | left: 20%; 261 | right: 20%; 262 | padding-bottom: 30px; 263 | } 264 | 265 | // Move up the indicators 266 | .carousel-indicators { 267 | bottom: 20px; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/src/site/scss/bootstrap/_close.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Close icons 3 | // -------------------------------------------------- 4 | 5 | 6 | .close { 7 | float: right; 8 | font-size: ($font-size-base * 1.5); 9 | font-weight: $close-font-weight; 10 | line-height: 1; 11 | color: $close-color; 12 | text-shadow: $close-text-shadow; 13 | @include opacity(.2); 14 | 15 | &:hover, 16 | &:focus { 17 | color: $close-color; 18 | text-decoration: none; 19 | cursor: pointer; 20 | @include opacity(.5); 21 | } 22 | 23 | // [converter] extracted button& to button.close 24 | } 25 | 26 | // Additional properties for button version 27 | // iOS requires the button element instead of an anchor tag. 28 | // If you want the anchor version, it requires `href="#"`. 29 | // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile 30 | button.close { 31 | padding: 0; 32 | cursor: pointer; 33 | background: transparent; 34 | border: 0; 35 | -webkit-appearance: none; 36 | } 37 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/src/site/scss/bootstrap/_code.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Code (inline and block) 3 | // -------------------------------------------------- 4 | 5 | 6 | // Inline and block code styles 7 | code, 8 | kbd, 9 | pre, 10 | samp { 11 | font-family: $font-family-monospace; 12 | } 13 | 14 | // Inline code 15 | code { 16 | padding: 2px 4px; 17 | font-size: 90%; 18 | color: $code-color; 19 | background-color: $code-bg; 20 | border-radius: $border-radius-base; 21 | } 22 | 23 | // User input typically entered via keyboard 24 | kbd { 25 | padding: 2px 4px; 26 | font-size: 90%; 27 | color: $kbd-color; 28 | background-color: $kbd-bg; 29 | border-radius: $border-radius-small; 30 | box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); 31 | 32 | kbd { 33 | padding: 0; 34 | font-size: 100%; 35 | font-weight: bold; 36 | box-shadow: none; 37 | } 38 | } 39 | 40 | // Blocks of code 41 | pre { 42 | display: block; 43 | padding: (($line-height-computed - 1) / 2); 44 | margin: 0 0 ($line-height-computed / 2); 45 | font-size: ($font-size-base - 1); // 14px to 13px 46 | line-height: $line-height-base; 47 | word-break: break-all; 48 | word-wrap: break-word; 49 | color: $pre-color; 50 | background-color: $pre-bg; 51 | border: 1px solid $pre-border-color; 52 | border-radius: $border-radius-base; 53 | 54 | // Account for some code outputs that place code tags in pre tags 55 | code { 56 | padding: 0; 57 | font-size: inherit; 58 | color: inherit; 59 | white-space: pre-wrap; 60 | background-color: transparent; 61 | border-radius: 0; 62 | } 63 | } 64 | 65 | // Enable scrollable blocks of code 66 | .pre-scrollable { 67 | max-height: $pre-scrollable-max-height; 68 | overflow-y: scroll; 69 | } 70 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/src/site/scss/bootstrap/_component-animations.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Component animations 3 | // -------------------------------------------------- 4 | 5 | // Heads up! 6 | // 7 | // We don't use the `.opacity()` mixin here since it causes a bug with text 8 | // fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552. 9 | 10 | .fade { 11 | opacity: 0; 12 | @include transition(opacity .15s linear); 13 | &.in { 14 | opacity: 1; 15 | } 16 | } 17 | 18 | .collapse { 19 | display: none; 20 | 21 | &.in { display: block; } 22 | // [converter] extracted tr&.in to tr.collapse.in 23 | // [converter] extracted tbody&.in to tbody.collapse.in 24 | } 25 | 26 | tr.collapse.in { display: table-row; } 27 | 28 | tbody.collapse.in { display: table-row-group; } 29 | 30 | .collapsing { 31 | position: relative; 32 | height: 0; 33 | overflow: hidden; 34 | @include transition-property(height, visibility); 35 | @include transition-duration(.35s); 36 | @include transition-timing-function(ease); 37 | } 38 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/src/site/scss/bootstrap/_dropdowns.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Dropdown menus 3 | // -------------------------------------------------- 4 | 5 | 6 | // Dropdown arrow/caret 7 | .caret { 8 | display: inline-block; 9 | width: 0; 10 | height: 0; 11 | margin-left: 2px; 12 | vertical-align: middle; 13 | border-top: $caret-width-base dashed; 14 | border-right: $caret-width-base solid transparent; 15 | border-left: $caret-width-base solid transparent; 16 | } 17 | 18 | // The dropdown wrapper (div) 19 | .dropup, 20 | .dropdown { 21 | position: relative; 22 | } 23 | 24 | // Prevent the focus on the dropdown toggle when closing dropdowns 25 | .dropdown-toggle:focus { 26 | outline: 0; 27 | } 28 | 29 | // The dropdown menu (ul) 30 | .dropdown-menu { 31 | position: absolute; 32 | top: 100%; 33 | left: 0; 34 | z-index: $zindex-dropdown; 35 | display: none; // none by default, but block on "open" of the menu 36 | float: left; 37 | min-width: 160px; 38 | padding: 5px 0; 39 | margin: 2px 0 0; // override default ul 40 | list-style: none; 41 | font-size: $font-size-base; 42 | text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) 43 | background-color: $dropdown-bg; 44 | border: 1px solid $dropdown-fallback-border; // IE8 fallback 45 | border: 1px solid $dropdown-border; 46 | border-radius: $border-radius-base; 47 | @include box-shadow(0 6px 12px rgba(0,0,0,.175)); 48 | background-clip: padding-box; 49 | 50 | // Aligns the dropdown menu to right 51 | // 52 | // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]` 53 | &.pull-right { 54 | right: 0; 55 | left: auto; 56 | } 57 | 58 | // Dividers (basically an hr) within the dropdown 59 | .divider { 60 | @include nav-divider($dropdown-divider-bg); 61 | } 62 | 63 | // Links within the dropdown menu 64 | > li > a { 65 | display: block; 66 | padding: 3px 20px; 67 | clear: both; 68 | font-weight: normal; 69 | line-height: $line-height-base; 70 | color: $dropdown-link-color; 71 | white-space: nowrap; // prevent links from randomly breaking onto new lines 72 | } 73 | } 74 | 75 | // Hover/Focus state 76 | .dropdown-menu > li > a { 77 | &:hover, 78 | &:focus { 79 | text-decoration: none; 80 | color: $dropdown-link-hover-color; 81 | background-color: $dropdown-link-hover-bg; 82 | } 83 | } 84 | 85 | // Active state 86 | .dropdown-menu > .active > a { 87 | &, 88 | &:hover, 89 | &:focus { 90 | color: $dropdown-link-active-color; 91 | text-decoration: none; 92 | outline: 0; 93 | background-color: $dropdown-link-active-bg; 94 | } 95 | } 96 | 97 | // Disabled state 98 | // 99 | // Gray out text and ensure the hover/focus state remains gray 100 | 101 | .dropdown-menu > .disabled > a { 102 | &, 103 | &:hover, 104 | &:focus { 105 | color: $dropdown-link-disabled-color; 106 | } 107 | 108 | // Nuke hover/focus effects 109 | &:hover, 110 | &:focus { 111 | text-decoration: none; 112 | background-color: transparent; 113 | background-image: none; // Remove CSS gradient 114 | @include reset-filter; 115 | cursor: $cursor-disabled; 116 | } 117 | } 118 | 119 | // Open state for the dropdown 120 | .open { 121 | // Show the menu 122 | > .dropdown-menu { 123 | display: block; 124 | } 125 | 126 | // Remove the outline when :focus is triggered 127 | > a { 128 | outline: 0; 129 | } 130 | } 131 | 132 | // Menu positioning 133 | // 134 | // Add extra class to `.dropdown-menu` to flip the alignment of the dropdown 135 | // menu with the parent. 136 | .dropdown-menu-right { 137 | left: auto; // Reset the default from `.dropdown-menu` 138 | right: 0; 139 | } 140 | // With v3, we enabled auto-flipping if you have a dropdown within a right 141 | // aligned nav component. To enable the undoing of that, we provide an override 142 | // to restore the default dropdown menu alignment. 143 | // 144 | // This is only for left-aligning a dropdown menu within a `.navbar-right` or 145 | // `.pull-right` nav component. 146 | .dropdown-menu-left { 147 | left: 0; 148 | right: auto; 149 | } 150 | 151 | // Dropdown section headers 152 | .dropdown-header { 153 | display: block; 154 | padding: 3px 20px; 155 | font-size: $font-size-small; 156 | line-height: $line-height-base; 157 | color: $dropdown-header-color; 158 | white-space: nowrap; // as with > li > a 159 | } 160 | 161 | // Backdrop to catch body clicks on mobile, etc. 162 | .dropdown-backdrop { 163 | position: fixed; 164 | left: 0; 165 | right: 0; 166 | bottom: 0; 167 | top: 0; 168 | z-index: ($zindex-dropdown - 10); 169 | } 170 | 171 | // Right aligned dropdowns 172 | .pull-right > .dropdown-menu { 173 | right: 0; 174 | left: auto; 175 | } 176 | 177 | // Allow for dropdowns to go bottom up (aka, dropup-menu) 178 | // 179 | // Just add .dropup after the standard .dropdown class and you're set, bro. 180 | // TODO: abstract this so that the navbar fixed styles are not placed here? 181 | 182 | .dropup, 183 | .navbar-fixed-bottom .dropdown { 184 | // Reverse the caret 185 | .caret { 186 | border-top: 0; 187 | border-bottom: $caret-width-base solid; 188 | content: ""; 189 | } 190 | // Different positioning for bottom up menu 191 | .dropdown-menu { 192 | top: auto; 193 | bottom: 100%; 194 | margin-bottom: 2px; 195 | } 196 | } 197 | 198 | 199 | // Component alignment 200 | // 201 | // Reiterate per navbar.less and the modified component alignment there. 202 | 203 | @media (min-width: $grid-float-breakpoint) { 204 | .navbar-right { 205 | .dropdown-menu { 206 | right: 0; left: auto; 207 | } 208 | // Necessary for overrides of the default right aligned menu. 209 | // Will remove come v4 in all likelihood. 210 | .dropdown-menu-left { 211 | left: 0; right: auto; 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/src/site/scss/bootstrap/_grid.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Grid system 3 | // -------------------------------------------------- 4 | 5 | 6 | // Container widths 7 | // 8 | // Set the container width, and override it for fixed navbars in media queries. 9 | 10 | .container { 11 | @include container-fixed; 12 | 13 | @media (min-width: $screen-sm-min) { 14 | width: $container-sm; 15 | } 16 | @media (min-width: $screen-md-min) { 17 | width: $container-md; 18 | } 19 | @media (min-width: $screen-lg-min) { 20 | width: $container-lg; 21 | } 22 | } 23 | 24 | 25 | // Fluid container 26 | // 27 | // Utilizes the mixin meant for fixed width containers, but without any defined 28 | // width for fluid, full width layouts. 29 | 30 | .container-fluid { 31 | @include container-fixed; 32 | } 33 | 34 | 35 | // Row 36 | // 37 | // Rows contain and clear the floats of your columns. 38 | 39 | .row { 40 | @include make-row; 41 | } 42 | 43 | 44 | // Columns 45 | // 46 | // Common styles for small and large grid columns 47 | 48 | @include make-grid-columns; 49 | 50 | 51 | // Extra small grid 52 | // 53 | // Columns, offsets, pushes, and pulls for extra small devices like 54 | // smartphones. 55 | 56 | @include make-grid(xs); 57 | 58 | 59 | // Small grid 60 | // 61 | // Columns, offsets, pushes, and pulls for the small device range, from phones 62 | // to tablets. 63 | 64 | @media (min-width: $screen-sm-min) { 65 | @include make-grid(sm); 66 | } 67 | 68 | 69 | // Medium grid 70 | // 71 | // Columns, offsets, pushes, and pulls for the desktop device range. 72 | 73 | @media (min-width: $screen-md-min) { 74 | @include make-grid(md); 75 | } 76 | 77 | 78 | // Large grid 79 | // 80 | // Columns, offsets, pushes, and pulls for the large desktop device range. 81 | 82 | @media (min-width: $screen-lg-min) { 83 | @include make-grid(lg); 84 | } 85 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/src/site/scss/bootstrap/_input-groups.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Input groups 3 | // -------------------------------------------------- 4 | 5 | // Base styles 6 | // ------------------------- 7 | .input-group { 8 | position: relative; // For dropdowns 9 | display: table; 10 | border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table 11 | 12 | // Undo padding and float of grid classes 13 | &[class*="col-"] { 14 | float: none; 15 | padding-left: 0; 16 | padding-right: 0; 17 | } 18 | 19 | .form-control { 20 | // Ensure that the input is always above the *appended* addon button for 21 | // proper border colors. 22 | position: relative; 23 | z-index: 2; 24 | 25 | // IE9 fubars the placeholder attribute in text inputs and the arrows on 26 | // select elements in input groups. To fix it, we float the input. Details: 27 | // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855 28 | float: left; 29 | 30 | width: 100%; 31 | margin-bottom: 0; 32 | } 33 | } 34 | 35 | // Sizing options 36 | // 37 | // Remix the default form control sizing classes into new ones for easier 38 | // manipulation. 39 | 40 | .input-group-lg > .form-control, 41 | .input-group-lg > .input-group-addon, 42 | .input-group-lg > .input-group-btn > .btn { 43 | @extend .input-lg; 44 | } 45 | .input-group-sm > .form-control, 46 | .input-group-sm > .input-group-addon, 47 | .input-group-sm > .input-group-btn > .btn { 48 | @extend .input-sm; 49 | } 50 | 51 | 52 | // Display as table-cell 53 | // ------------------------- 54 | .input-group-addon, 55 | .input-group-btn, 56 | .input-group .form-control { 57 | display: table-cell; 58 | 59 | &:not(:first-child):not(:last-child) { 60 | border-radius: 0; 61 | } 62 | } 63 | // Addon and addon wrapper for buttons 64 | .input-group-addon, 65 | .input-group-btn { 66 | width: 1%; 67 | white-space: nowrap; 68 | vertical-align: middle; // Match the inputs 69 | } 70 | 71 | // Text input groups 72 | // ------------------------- 73 | .input-group-addon { 74 | padding: $padding-base-vertical $padding-base-horizontal; 75 | font-size: $font-size-base; 76 | font-weight: normal; 77 | line-height: 1; 78 | color: $input-color; 79 | text-align: center; 80 | background-color: $input-group-addon-bg; 81 | border: 1px solid $input-group-addon-border-color; 82 | border-radius: $border-radius-base; 83 | 84 | // Sizing 85 | &.input-sm { 86 | padding: $padding-small-vertical $padding-small-horizontal; 87 | font-size: $font-size-small; 88 | border-radius: $border-radius-small; 89 | } 90 | &.input-lg { 91 | padding: $padding-large-vertical $padding-large-horizontal; 92 | font-size: $font-size-large; 93 | border-radius: $border-radius-large; 94 | } 95 | 96 | // Nuke default margins from checkboxes and radios to vertically center within. 97 | input[type="radio"], 98 | input[type="checkbox"] { 99 | margin-top: 0; 100 | } 101 | } 102 | 103 | // Reset rounded corners 104 | .input-group .form-control:first-child, 105 | .input-group-addon:first-child, 106 | .input-group-btn:first-child > .btn, 107 | .input-group-btn:first-child > .btn-group > .btn, 108 | .input-group-btn:first-child > .dropdown-toggle, 109 | .input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), 110 | .input-group-btn:last-child > .btn-group:not(:last-child) > .btn { 111 | @include border-right-radius(0); 112 | } 113 | .input-group-addon:first-child { 114 | border-right: 0; 115 | } 116 | .input-group .form-control:last-child, 117 | .input-group-addon:last-child, 118 | .input-group-btn:last-child > .btn, 119 | .input-group-btn:last-child > .btn-group > .btn, 120 | .input-group-btn:last-child > .dropdown-toggle, 121 | .input-group-btn:first-child > .btn:not(:first-child), 122 | .input-group-btn:first-child > .btn-group:not(:first-child) > .btn { 123 | @include border-left-radius(0); 124 | } 125 | .input-group-addon:last-child { 126 | border-left: 0; 127 | } 128 | 129 | // Button input groups 130 | // ------------------------- 131 | .input-group-btn { 132 | position: relative; 133 | // Jankily prevent input button groups from wrapping with `white-space` and 134 | // `font-size` in combination with `inline-block` on buttons. 135 | font-size: 0; 136 | white-space: nowrap; 137 | 138 | // Negative margin for spacing, position for bringing hovered/focused/actived 139 | // element above the siblings. 140 | > .btn { 141 | position: relative; 142 | + .btn { 143 | margin-left: -1px; 144 | } 145 | // Bring the "active" button to the front 146 | &:hover, 147 | &:focus, 148 | &:active { 149 | z-index: 2; 150 | } 151 | } 152 | 153 | // Negative margin to only have a 1px border between the two 154 | &:first-child { 155 | > .btn, 156 | > .btn-group { 157 | margin-right: -1px; 158 | } 159 | } 160 | &:last-child { 161 | > .btn, 162 | > .btn-group { 163 | margin-left: -1px; 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/src/site/scss/bootstrap/_jumbotron.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Jumbotron 3 | // -------------------------------------------------- 4 | 5 | 6 | .jumbotron { 7 | padding: $jumbotron-padding ($jumbotron-padding / 2); 8 | margin-bottom: $jumbotron-padding; 9 | color: $jumbotron-color; 10 | background-color: $jumbotron-bg; 11 | 12 | h1, 13 | .h1 { 14 | color: $jumbotron-heading-color; 15 | } 16 | 17 | p { 18 | margin-bottom: ($jumbotron-padding / 2); 19 | font-size: $jumbotron-font-size; 20 | font-weight: 200; 21 | } 22 | 23 | > hr { 24 | border-top-color: darken($jumbotron-bg, 10%); 25 | } 26 | 27 | .container &, 28 | .container-fluid & { 29 | border-radius: $border-radius-large; // Only round corners at higher resolutions if contained in a container 30 | } 31 | 32 | .container { 33 | max-width: 100%; 34 | } 35 | 36 | @media screen and (min-width: $screen-sm-min) { 37 | padding: ($jumbotron-padding * 1.6) 0; 38 | 39 | .container &, 40 | .container-fluid & { 41 | padding-left: ($jumbotron-padding * 2); 42 | padding-right: ($jumbotron-padding * 2); 43 | } 44 | 45 | h1, 46 | .h1 { 47 | font-size: ($font-size-base * 4.5); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/src/site/scss/bootstrap/_labels.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Labels 3 | // -------------------------------------------------- 4 | 5 | .label { 6 | display: inline; 7 | padding: .2em .6em .3em; 8 | font-size: 75%; 9 | font-weight: bold; 10 | line-height: 1; 11 | color: $label-color; 12 | text-align: center; 13 | white-space: nowrap; 14 | vertical-align: baseline; 15 | border-radius: .25em; 16 | 17 | // [converter] extracted a& to a.label 18 | 19 | // Empty labels collapse automatically (not available in IE8) 20 | &:empty { 21 | display: none; 22 | } 23 | 24 | // Quick fix for labels in buttons 25 | .btn & { 26 | position: relative; 27 | top: -1px; 28 | } 29 | } 30 | 31 | // Add hover effects, but only for links 32 | a.label { 33 | &:hover, 34 | &:focus { 35 | color: $label-link-hover-color; 36 | text-decoration: none; 37 | cursor: pointer; 38 | } 39 | } 40 | 41 | // Colors 42 | // Contextual variations (linked labels get darker on :hover) 43 | 44 | .label-default { 45 | @include label-variant($label-default-bg); 46 | } 47 | 48 | .label-primary { 49 | @include label-variant($label-primary-bg); 50 | } 51 | 52 | .label-success { 53 | @include label-variant($label-success-bg); 54 | } 55 | 56 | .label-info { 57 | @include label-variant($label-info-bg); 58 | } 59 | 60 | .label-warning { 61 | @include label-variant($label-warning-bg); 62 | } 63 | 64 | .label-danger { 65 | @include label-variant($label-danger-bg); 66 | } 67 | -------------------------------------------------------------------------------- /slick-postgresql-redis/ps-web/static/src/site/scss/bootstrap/_list-group.scss: -------------------------------------------------------------------------------- 1 | // 2 | // List groups 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base class 7 | // 8 | // Easily usable on