From 2d4f3f7fe3a5e38cb959eea139160caf273227ac Mon Sep 17 00:00:00 2001 From: Peter Becich Date: Sun, 15 May 2022 11:25:47 -0700 Subject: [PATCH 01/43] delete unused GitHub Action; fix CI badge --- .github/workflows/ci.yml.deactivated | 74 ---------------------------- README.md | 2 +- 2 files changed, 1 insertion(+), 75 deletions(-) delete mode 100644 .github/workflows/ci.yml.deactivated diff --git a/.github/workflows/ci.yml.deactivated b/.github/workflows/ci.yml.deactivated deleted file mode 100644 index a6a0d2285..000000000 --- a/.github/workflows/ci.yml.deactivated +++ /dev/null @@ -1,74 +0,0 @@ -# modified from https://github.com/jgm/pandoc/blob/master/.github/workflows/ci.yml -name: CI - -on: - push: - branches: - - '**' - paths-ignore: [] - pull_request: - paths-ignore: [] - -jobs: - linux: - - runs-on: ubuntu-20.04 - strategy: - fail-fast: false - matrix: - versions: - - ghc: '9.2.2' - cabal: '3.6' - - ghc: '9.0.2' - cabal: '3.6' - - ghc: '8.10.7' - cabal: '3.6' - - ghc: '8.8.4' - cabal: '3.6' - steps: - - uses: actions/checkout@v2 - - - name: Install recent cabal/ghc - uses: haskell/actions/setup@v1 - with: - ghc-version: ${{ matrix.versions.ghc }} - cabal-version: ${{ matrix.versions.cabal }} - - # declare/restore cached things - # caching doesn't work for scheduled runs yet - # https://github.com/actions/cache/issues/63 - - - name: Cache cabal global package db - id: cabal-global - uses: actions/cache@v2 - with: - path: | - ~/.cabal - key: ${{ runner.os }}-${{ matrix.versions.ghc }}-${{ matrix.versions.cabal }}-cabal-global-${{ hashFiles('cabal.project') }} - - - name: Cache cabal work - id: cabal-local - uses: actions/cache@v2 - with: - path: | - dist-newstyle - key: ${{ runner.os }}-${{ matrix.versions.ghc }}-${{ matrix.versions.cabal }}-cabal-local - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install libbrotli-dev - cabal update - cabal build --dependencies-only --enable-tests --disable-optimization - - - name: Build - run: | - cabal build --enable-tests --disable-optimization 2>&1 | tee build.log - - - name: Test - run: | - cabal test --enable-tests --disable-optimization - - - name: Haddock - run: | - cabal haddock --enable-tests --disable-optimization diff --git a/README.md b/README.md index 1aff796e8..552dfa64b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # hackage-server [![Build Status](https://travis-ci.org/haskell/hackage-server.png?branch=master)](https://travis-ci.org/haskell/hackage-server) -[![Build status](https://github.com/haskell/hackage-server/actions/workflows/ci.yml/badge.svg)](https://github.com/haskell/hackage-server/actions/workflows/ci.yml) +[![Build status](https://github.com/haskell/hackage-server/actions/workflows/haskell-ci.yml/badge.svg)](https://github.com/haskell/hackage-server/actions/workflows/haskell-ci.yml) [![Build status](https://github.com/haskell/hackage-server/actions/workflows/nix-shell.yml/badge.svg)](https://github.com/haskell/hackage-server/actions/workflows/nix-shell.yml) This is the `hackage-server` code. This is what powers , and many other private hackage instances. The `master` branch is suitable for general usage. Specific policy and documentation for the central hackage instance exists in the `central-server` branch. From b21f35be8debdd7f6bcbd503e49ca8c73a41985d Mon Sep 17 00:00:00 2001 From: Andreas Abel Date: Thu, 19 May 2022 18:23:28 +0200 Subject: [PATCH 02/43] Fix #1076: separate validators from UI and doctest them (#1077) --- hackage-server.cabal | 1 + src/Distribution/Server/Util/Validators.hs | 43 ++--- .../Server/Util/Validators/Internal.hs | 153 ++++++++++++++++++ 3 files changed, 168 insertions(+), 29 deletions(-) create mode 100644 src/Distribution/Server/Util/Validators/Internal.hs diff --git a/hackage-server.cabal b/hackage-server.cabal index af9413f62..d56c46c79 100644 --- a/hackage-server.cabal +++ b/hackage-server.cabal @@ -247,6 +247,7 @@ library lib-server Distribution.Server.Util.Parse Distribution.Server.Util.ServeTarball Distribution.Server.Util.Validators + Distribution.Server.Util.Validators.Internal -- [unused] Distribution.Server.Util.TarIndex Distribution.Server.Util.GZip Distribution.Server.Util.ContentType diff --git a/src/Distribution/Server/Util/Validators.hs b/src/Distribution/Server/Util/Validators.hs index 2c247bbbf..1a16869a9 100644 --- a/src/Distribution/Server/Util/Validators.hs +++ b/src/Distribution/Server/Util/Validators.hs @@ -4,41 +4,26 @@ module Distribution.Server.Util.Validators , guardValidLookingEmail ) where -import Data.Char (isSpace, isPrint) -import qualified Data.Text as T +import Data.Text (Text) +import Distribution.Pretty (prettyShow) import Distribution.Server.Framework -import Distribution.Server.Users.Types (isValidUserNameChar) +import Distribution.Server.Util.Validators.Internal (validName, validUserName, validEmail) -guardValidLookingName :: T.Text -> ServerPartE () -guardValidLookingName str = either errBadUserName return $ do - guard (T.length str <= 70) ?! "Sorry, we didn't expect names to be longer than 70 characters." - guard (T.all isPrint str) ?! "Unexpected character in name, please use only printable Unicode characters." +guardValidLookingName :: Text -> ServerPartE () +guardValidLookingName = + either (errBadUserName . prettyShow) return . validName -guardValidLookingUserName :: T.Text -> ServerPartE () -guardValidLookingUserName str = either errBadRealName return $ do - guard (T.length str <= 50) ?! "Sorry, we didn't expect login names to be longer than 50 characters." - guard (T.all isValidUserNameChar str) ?! "Sorry, login names have to be ASCII characters only or _, no spaces or other symbols." +guardValidLookingUserName :: Text -> ServerPartE () +guardValidLookingUserName = + either (errBadRealName . prettyShow) return . validUserName -- Make sure this roughly corresponds to the frontend validation in user-details-form.html.st -guardValidLookingEmail :: T.Text -> ServerPartE () -guardValidLookingEmail str = either errBadEmail return $ do - guard (T.length str <= 100) ?! "Sorry, we didn't expect email addresses to be longer than 100 characters." - guard (T.all isPrint str) ?! "Unexpected character in email address, please use only printable Unicode characters." - guard hasAtSomewhere ?! "Oops, that doesn't look like an email address." - guard (T.all (not.isSpace) str) ?! "Oops, no spaces in email addresses please." - guard (T.all (not.isAngle) str) ?! "Please use just the email address, not \"name\" style." - where - isAngle c = c == '<' || c == '>' - hasAtSomewhere = case T.span (/= '@') str of - (before, rest) - | Just (_, after) <- T.uncons rest -> - T.length before >= 1 - && T.length after > 0 - && not ('@' `T.elem` after) - _ -> False +guardValidLookingEmail :: Text -> ServerPartE () +guardValidLookingEmail = + either (errBadEmail . prettyShow) return . validEmail errBadUserName, errBadRealName, errBadEmail :: String -> ServerPartE a -errBadUserName err = errBadRequest "Problem with login name" [MText err] -errBadRealName err = errBadRequest "Problem with name" [MText err] +errBadUserName err = errBadRequest "Problem with login name" [MText err] +errBadRealName err = errBadRequest "Problem with name" [MText err] errBadEmail err = errBadRequest "Problem with email address" [MText err] diff --git a/src/Distribution/Server/Util/Validators/Internal.hs b/src/Distribution/Server/Util/Validators/Internal.hs new file mode 100644 index 000000000..67e7893bf --- /dev/null +++ b/src/Distribution/Server/Util/Validators/Internal.hs @@ -0,0 +1,153 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | Purely functional version of "Distribution.Server.Util.Validators" +-- for testing the validators. + +module Distribution.Server.Util.Validators.Internal where + +import Control.Monad (unless) +import Control.Monad.Except (MonadError(..)) + +import Data.Char (isSpace, isPrint) +import Data.Text (Text) +import qualified Data.Text as T + +import Distribution.Pretty (Pretty(..)) +import Distribution.Server.Users.Types (isValidUserNameChar) + +-- Set up doctest to deal with text literals. + +-- $setup +-- >>> :set -XOverloadedStrings + +-- | Basic sanity checking on names. +-- +-- >>> validName "Innocent User" +-- Right () +-- +-- >>> validName "Mr. X is the greatest super duper dude of all!" +-- Right () +-- +-- >>> validName "I am also a developer, maintainer, blogger, for Haskell, Hackage, Cabal, Stackage" +-- Left NameTooLong +-- +-- >>> validName "My name has beeps \BEL, newlines \n, and \t tabs" +-- Left NameNotPrintable +-- +validName :: Text -> Either InvalidName () +validName str = do + unless (T.length str <= 70) $ throwError NameTooLong + unless (T.all isPrint str) $ throwError NameNotPrintable + +-- | Errors produced by 'validName' check. + +data InvalidName + = NameTooLong -- ^ More than 70 characters long. + | NameNotPrintable -- ^ Contains unprintable characters. + deriving (Eq, Show) + +instance Pretty InvalidName where + pretty = \case + NameTooLong -> "Sorry, we didn't expect names to be longer than 70 characters." + NameNotPrintable -> "Unexpected character in name, please use only printable Unicode characters." + +-- | Basic sanity checking on user names. +-- +-- >>> validUserName "innocent_user_42" +-- Right () +-- +-- >>> validUserName "mr_X_stretches_the_Limit_of_50_characters_01234567" +-- Right () +-- +-- >>> validUserName "01234" +-- Right () +-- +-- >>> validUserName "dashes-not-allowed" +-- Left UserNameInvalidChar +-- +-- >>> validUserName "questions_not_allowed?" +-- Left UserNameInvalidChar +-- +-- >>> validUserName "my_Ego_busts_the_Limit_of_50_characters_01234567890" +-- Left UserNameTooLong +-- +validUserName :: T.Text -> Either InvalidUserName () +validUserName str = do + unless (T.length str <= 50) $ throwError UserNameTooLong + unless (T.all isValidUserNameChar str) $ throwError UserNameInvalidChar + +-- | Errors produced by 'validUserName' check. + +data InvalidUserName + = UserNameTooLong -- ^ More than 50 characters long. + | UserNameInvalidChar -- ^ Contains character not matching 'isValidUserNameChar'. + deriving (Eq, Show) + +instance Pretty InvalidUserName where + pretty = \case + UserNameTooLong -> "Sorry, we didn't expect login names to be longer than 50 characters." + UserNameInvalidChar -> "Sorry, login names have to be ASCII characters only or _, no spaces or other symbols." + +-- | Basic sanity checking in email. +-- +-- >>> validEmail "Emmanuel.Lauterbach@phantasy-promi.darknet.de" +-- Right () +-- +-- >>> validEmail "gerd.lauchkopf+spam@posteo.de" +-- Right () +-- +-- >>> validEmail "Emmanuel.Lauterbachs.Cousin@mailrelay.tor.amazon-aws.bill-me.cold-fusion.bogus-domain.phantasy-promi.darknet.de" +-- Left EmailTooLong +-- +-- >>> validEmail "\BELlingcat@a\nonymous.\to" +-- Left EmailNotPrintable +-- +-- >>> validEmail "ich-im-aether" +-- Left EmailBadFormat +-- +-- >>> validEmail "ich@guuugle@kom" +-- Left EmailBadFormat +-- +-- >>> validEmail "Windows User @ Company . com" +-- Left EmailHasSpace +-- +-- >>> validEmail "Name" +-- Left EmailHasAngle +-- +validEmail :: T.Text -> Either InvalidEmail () +validEmail str = do + unless (T.length str <= 100) $ throwError EmailTooLong + unless (T.all isPrint str) $ throwError EmailNotPrintable + unless hasAtSomewhere $ throwError EmailBadFormat + unless (T.all (not.isSpace) str) $ throwError EmailHasSpace + unless (T.all (not.isAngle) str) $ throwError EmailHasAngle + where + isAngle c = c == '<' || c == '>' + hasAtSomewhere = case T.break (== '@') str of + (before, rest) + | Just (_, after) <- T.uncons rest -> + not $ or + [ T.null before + , T.null after + , '@' `T.elem` after + ] + | otherwise -> False + +-- | Errors produced by 'validEmail' check. + +data InvalidEmail + = EmailTooLong -- ^ More than 100 characters long. + | EmailNotPrintable -- ^ Contains unprintable characters. + | EmailBadFormat -- ^ Doesn't have exactly one @ sign. + | EmailHasSpace -- ^ Contains spaces. + | EmailHasAngle -- ^ Contains angle brackets. + deriving (Eq, Show) + +instance Pretty InvalidEmail where + pretty = \case + EmailTooLong -> "Sorry, we didn't expect email addresses to be longer than 100 characters." + EmailNotPrintable -> "Unexpected character in email address, please use only printable Unicode characters." + EmailBadFormat -> "Oops, that doesn't look like an email address." + EmailHasSpace -> "Oops, no spaces in email addresses please." + EmailHasAngle -> "Please use just the email address, not \"name\" style." From c2a34d73c8b1a52ebe6170ea983b661af2dbce36 Mon Sep 17 00:00:00 2001 From: Peter Becich Date: Sat, 4 Jun 2022 15:57:51 -0700 Subject: [PATCH 03/43] Cachix caching for nix-shell GitHub Action (#1081) Squashed commit of the following: commit be261f9005d7f0382bd28e384542e141d47c4c35 Author: Peter Becich Date: Wed Jun 1 20:17:02 2022 -0700 Revert "test" This reverts commit 678a1edff947da92cc8494ff332ec156b8a78149. commit 678a1edff947da92cc8494ff332ec156b8a78149 Author: Peter Becich Date: Wed Jun 1 19:47:10 2022 -0700 test commit 7c8b2ee87a8c58bde35de9e6e9a8dcd77b864ec4 Author: Peter Becich Date: Wed May 25 18:55:42 2022 -0700 use Cachix in GitHub Action commit b99b637c04b1991aaf14c5c7708feec6bb059648 Author: Peter Becich Date: Tue May 24 00:17:53 2022 -0700 attempt to fix Github Actions caching commit 49f09ed99637250bb68d09a4786f9e721ef15f30 Author: Peter Becich Date: Mon May 23 19:40:27 2022 -0700 attempt to cache `/nix` in Nix Shell Github Action https://github.com/cachix/install-nix-action/issues/56#issuecomment-1030697681 commit 5a5b3100656c247606c60ba95d87640f69882db7 Author: Peter Becich Date: Mon May 23 19:35:18 2022 -0700 attempt to use cache in Nix Shell Github Action https://github.com/cachix/install-nix-action#how-do-i-add-a-binary-cache --- .github/workflows/nix-shell.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/nix-shell.yml b/.github/workflows/nix-shell.yml index 746f43f1d..06b6e2781 100644 --- a/.github/workflows/nix-shell.yml +++ b/.github/workflows/nix-shell.yml @@ -1,13 +1,7 @@ -# https://nix.dev/tutorials/continuous-integration-github-actions name: "Test nix-shell" on: - push: - branches: - - '**' - paths-ignore: [] - pull_request: - paths-ignore: [] - + - push + - pull_request jobs: nix-shell: runs-on: ubuntu-latest @@ -16,4 +10,12 @@ jobs: - uses: cachix/install-nix-action@v16 with: nix_path: nixpkgs=channel:nixos-21.11 + extra_nix_config: | + trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= + substituters = https://hydra.iohk.io https://cache.nixos.org/ + - uses: cachix/cachix-action@v10 + with: + # https://nix.dev/tutorials/continuous-integration-github-actions#setting-up-github-actions + name: hackage-server + authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - run: nix-shell --pure --run "cabal update && cabal build all --enable-tests" From c9952843dcdb02df7ef965c0368923f37af2f326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9cate=20Moonlight?= Date: Tue, 7 Jun 2022 17:39:26 +0200 Subject: [PATCH 04/43] Add uploaded_at field in package api (#1080) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At present time, the information returned by the Package JSON API amounts to: ```json { "author": "Hécate Moonlight", "copyright": "", "description": "The 'Display' typeclass provides a solution for user-facing output that does not have to abide by the rules of the Show typeclass.", "homepage": "https://github.com/haskell-text/text-display#readme", "license": "MIT", "metadata_revision": 0, "synopsis": "A typeclass for user-facing output" } ``` This PR aims to implement support for the package upload timestamp in this payload. The final result is this: ```json { "author": "Hécate Moonlight", "copyright": "", "description": "The 'Display' typeclass provides a solution for user-facing output that does not have to abide by the rules of the Show typeclass.", "homepage": "https://github.com/haskell-text/text-display#readme", "license": "MIT", "metadata_revision": 0, "synopsis": "A typeclass for user-facing output", "uploaded_at": "2022-05-22T22:24:48.997120639Z" } ``` --- .../Server/Features/PackageInfoJSON.hs | 12 +++++++++--- .../Server/Features/PackageInfoJSON/State.hs | 16 ++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Distribution/Server/Features/PackageInfoJSON.hs b/src/Distribution/Server/Features/PackageInfoJSON.hs index 6fdd96e36..3daf2f1ef 100644 --- a/src/Distribution/Server/Features/PackageInfoJSON.hs +++ b/src/Distribution/Server/Features/PackageInfoJSON.hs @@ -53,6 +53,7 @@ import Distribution.Utils.ShortText (fromShortText) import Data.Foldable (toList) import Data.Traversable (for) import qualified Data.List as List +import Data.Time (UTCTime) data PackageInfoJSONFeature = PackageInfoJSONFeature { @@ -130,12 +131,14 @@ initPackageInfoJSONFeature env = do -- | Pure function for extracting basic package info from a Cabal file getBasicDescription - :: CabalFileText + :: UTCTime + -- ^ Time of upload + -> CabalFileText -> Int -- ^ Metadata revision. This will be added to the resulting -- @PackageBasicDescription@ -> Either String PackageBasicDescription -getBasicDescription (CabalFileText cf) metadataRev = +getBasicDescription uploadedAt (CabalFileText cf) metadataRev = let parseResult = PkgDescr.parseGenericPackageDescription (BS.toStrict cf) in case PkgDescr.runParseResult parseResult of (_, Right pkg) -> let @@ -148,6 +151,7 @@ getBasicDescription (CabalFileText cf) metadataRev = PkgDescr.licenseRaw pkgd pbd_homepage = T.pack . fromShortText $ PkgDescr.homepage pkgd pbd_metadata_revision = metadataRev + pbd_uploaded_at = uploadedAt in return $ PackageBasicDescription {..} (_, Left (_, perrs)) -> @@ -201,6 +205,7 @@ servePackageBasicDescription resource preferred packageInfoState dpath = do pkg <- lookupPackageId resource pkgid let metadataRevs = fst <$> pkgMetadataRevisions pkg + uploadInfos = snd <$> pkgMetadataRevisions pkg nMetadata = Vector.length metadataRevs metadataInd = fromMaybe (nMetadata - 1) metadataRev @@ -212,7 +217,8 @@ servePackageBasicDescription resource preferred packageInfoState dpath = do ) let cabalFile = metadataRevs Vector.! metadataInd - pkgDescr = getBasicDescription cabalFile metadataInd + uploadedAt = fst $ uploadInfos Vector.! metadataInd + pkgDescr = getBasicDescription uploadedAt cabalFile metadataInd case pkgDescr of Left e -> Framework.errInternalError [Framework.MText e] Right d -> return d diff --git a/src/Distribution/Server/Features/PackageInfoJSON/State.hs b/src/Distribution/Server/Features/PackageInfoJSON/State.hs index 54227d206..53adfa242 100644 --- a/src/Distribution/Server/Features/PackageInfoJSON/State.hs +++ b/src/Distribution/Server/Features/PackageInfoJSON/State.hs @@ -23,7 +23,7 @@ import Data.Monoid (Sum(..)) import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.SafeCopy (SafeCopy(..), base, contain, - deriveSafeCopy) + deriveSafeCopy, safeGet, safePut) import Data.Serialize (Get, get, getListOf, getTwoOf, put, putListOf, putTwoOf) import Data.Typeable (Typeable) @@ -31,6 +31,7 @@ import Data.Word (Word8) import Distribution.License (licenseToSPDX) import Distribution.Text (display, simpleParse) import GHC.Generics (Generic) +import Data.Time (UTCTime) import Distribution.SPDX.License (License) import Distribution.Package (PackageIdentifier, PackageName) @@ -40,8 +41,7 @@ import qualified Distribution.Parsec as Parsec import qualified Distribution.Server.Features.PreferredVersions as Preferred import Distribution.Server.Framework.MemSize (MemSize, - memSize, - memSize7) + memSize, memSize8) -- | Basic information about a package. These values are @@ -54,10 +54,10 @@ data PackageBasicDescription = PackageBasicDescription , pbd_author :: !T.Text , pbd_homepage :: !T.Text , pbd_metadata_revision :: !Int + , pbd_uploaded_at :: !UTCTime } deriving (Eq, Show, Generic) instance SafeCopy PackageBasicDescription where - putCopy PackageBasicDescription{..} = contain $ do put (Pretty.prettyShow pbd_license) put $ T.encodeUtf8 pbd_copyright @@ -66,6 +66,7 @@ instance SafeCopy PackageBasicDescription where put $ T.encodeUtf8 pbd_author put $ T.encodeUtf8 pbd_homepage put pbd_metadata_revision + safePut pbd_uploaded_at getCopy = contain $ do licenseStr <- get @@ -78,6 +79,7 @@ instance SafeCopy PackageBasicDescription where pbd_author <- T.decodeUtf8 <$> get pbd_homepage <- T.decodeUtf8 <$> get pbd_metadata_revision <- get + pbd_uploaded_at <- safeGet return PackageBasicDescription{..} @@ -93,6 +95,7 @@ instance Aeson.ToJSON PackageBasicDescription where , Key.fromString "author" .= pbd_author , Key.fromString "homepage" .= pbd_homepage , Key.fromString "metadata_revision" .= pbd_metadata_revision + , Key.fromString "uploaded_at" .= pbd_uploaded_at ] @@ -110,6 +113,7 @@ instance Aeson.FromJSON PackageBasicDescription where pbd_author <- obj .: Key.fromString "author" pbd_homepage <- obj .: Key.fromString "homepage" pbd_metadata_revision <- obj .: Key.fromString "metadata_revision" + pbd_uploaded_at <- obj .: Key.fromString "uploaded_at" return $ PackageBasicDescription {..} @@ -225,8 +229,8 @@ deriveSafeCopy 0 'base ''PackageInfoState instance MemSize PackageBasicDescription where memSize PackageBasicDescription{..} = - memSize7 (Pretty.prettyShow pbd_license) pbd_copyright pbd_synopsis - pbd_description pbd_author pbd_homepage pbd_metadata_revision + memSize8 (Pretty.prettyShow pbd_license) pbd_copyright pbd_synopsis + pbd_description pbd_author pbd_homepage pbd_metadata_revision pbd_uploaded_at instance MemSize PackageVersions where memSize (PackageVersions ps) = getSum $ From 12481221fc2a16ad353a769d27514d3f7e9ddaab Mon Sep 17 00:00:00 2001 From: Matthew Pickering Date: Thu, 23 Jun 2022 18:47:48 +0100 Subject: [PATCH 05/43] package page: Include virtual-modules in module tree (#1085) The virtual-modules field is used by ghc-prim to provide a magic module which doesn't exist on disk but still has documentation and so-on. By including it here the module appears in the module list on the package homepage. --- src/Distribution/Server/Packages/Render.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Distribution/Server/Packages/Render.hs b/src/Distribution/Server/Packages/Render.hs index 09c0577da..8b40303e5 100644 --- a/src/Distribution/Server/Packages/Render.hs +++ b/src/Distribution/Server/Packages/Render.hs @@ -149,6 +149,7 @@ doPackageRender users info = PackageRender = let mod_ix = mkForest $ exposedModules lib -- Assumes that there is an HTML per reexport ++ map moduleReexportName (reexportedModules lib) + ++ virtualModules (libBuildInfo lib) sig_ix = mkForest $ signatures lib mkForest = moduleForest . map (\m -> (m, moduleHasDocs docindex m)) in Just (ModSigIndex { modIndex = mod_ix, sigIndex = sig_ix }) From ae4f14e9d6c66bc0bcd6cd274eea280e0ea5ae25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CB=8Cbod=CA=B2=C9=AA=CB=88=C9=A1r=CA=B2im?= Date: Thu, 30 Jun 2022 01:59:57 +0100 Subject: [PATCH 06/43] Allow hashable-1.4 and text-2.0 (#1089) --- hackage-server.cabal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hackage-server.cabal b/hackage-server.cabal index d56c46c79..c32615ea9 100644 --- a/hackage-server.cabal +++ b/hackage-server.cabal @@ -109,7 +109,7 @@ common defaults , mtl ^>= 2.2.1 , pretty >= 1.1 && < 1.2 , process >= 1.6 && < 1.7 - , text ^>= 1.2.5.0 + , text ^>= 1.2.5.0 || ^>= 2.0 , time >= 1.9 && < 1.13 , transformers >= 0.5 && < 0.6 , unix >= 2.7 && < 2.8 @@ -394,7 +394,7 @@ library lib-server , hackage-security-HTTP ^>= 0.1.1 , haddock-library > 1.7 && < 2 , happstack-server ^>= 7.7.1 - , hashable ^>= 1.3 + , hashable ^>= 1.3 || ^>= 1.4 , hslogger ^>= 1.3.1 , lifted-base ^>= 0.2.1 , mime-mail ^>= 0.5 From 9fe9494f3a5e471df84c21894929319b1328cd30 Mon Sep 17 00:00:00 2001 From: Alias Qli <2576814881@qq.com> Date: Wed, 13 Jul 2022 00:08:00 +0800 Subject: [PATCH 07/43] Divide sitemap into parts --- src/Distribution/Server/Features/Sitemap.hs | 66 ++++++++++++++----- .../Server/Features/Sitemap/Functions.hs | 17 +++++ 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/src/Distribution/Server/Features/Sitemap.hs b/src/Distribution/Server/Features/Sitemap.hs index 198452c39..429adc67b 100644 --- a/src/Distribution/Server/Features/Sitemap.hs +++ b/src/Distribution/Server/Features/Sitemap.hs @@ -25,7 +25,21 @@ import Data.ByteString.Lazy (ByteString) import Data.Time.Clock (UTCTime(..), getCurrentTime) import Data.Time.Calendar (showGregorian) import Network.URI +import Control.DeepSeq +import Text.Read +import Data.List.Split +data Sitemap + = Sitemap + { sitemapIndex :: XMLResponse + , sitemaps :: [XMLResponse] + } + +instance NFData Sitemap where + rnf (Sitemap i s) = rnf i `seq` rnf s + +instance MemSize Sitemap where + memSize (Sitemap i s) = memSize2 i s data SitemapFeature = SitemapFeature { sitemapFeatureInterface :: HackageFeature @@ -67,8 +81,8 @@ sitemapFeature :: ServerEnv -> DocumentationFeature -> TagsFeature -> UTCTime - -> AsyncCache XMLResponse - -> (SitemapFeature, IO XMLResponse) + -> AsyncCache Sitemap + -> (SitemapFeature, IO Sitemap) sitemapFeature ServerEnv{..} CoreFeature{..} DocumentationFeature{..} @@ -79,50 +93,70 @@ sitemapFeature ServerEnv{..} where sitemapFeatureInterface = (emptyHackageFeature "sitemap") { - featureResources = [ xmlSitemapResource ] + featureResources = [ xmlSitemapIndexResource, xmlSitemapResource ] , featureState = [] - , featureDesc = "Provides a sitemap.xml for search engines" + , featureDesc = "Provides sitemap for search engines" , featureCaches = [ CacheComponent { - cacheDesc = "sitemap.xml", + cacheDesc = "sitemap", getCacheMemSize = memSize <$> readAsyncCache sitemapCache } ] , featurePostInit = do syncAsyncCache sitemapCache addCronJob serverCron CronJob { - cronJobName = "regenerate the cached sitemap.xml", + cronJobName = "regenerate the cached sitemap", cronJobFrequency = DailyJobFrequency, cronJobOneShot = False, cronJobAction = prodAsyncCache sitemapCache "cron" } } + xmlSitemapIndexResource :: Resource + xmlSitemapIndexResource = (resourceAt "/sitemap_index.xml") { + resourceDesc = [(GET, "The dynamically generated sitemap index, in XML format")] + , resourceGet = [("xml", serveSitemapIndex)] + } + xmlSitemapResource :: Resource - xmlSitemapResource = (resourceAt "/sitemap.xml") { + xmlSitemapResource = (resourceAt "/sitemap/:filename") { resourceDesc = [(GET, "The dynamically generated sitemap, in XML format")] , resourceGet = [("xml", serveSitemap)] } - serveSitemap :: DynamicPath -> ServerPartE Response - serveSitemap _ = do - sitemapXML <- liftIO $ readAsyncCache sitemapCache + serveSitemapIndex :: DynamicPath -> ServerPartE Response + serveSitemapIndex _ = do + Sitemap{..} <- liftIO $ readAsyncCache sitemapCache cacheControlWithoutETag [Public, maxAgeDays 1] - return (toResponse sitemapXML) + return (toResponse sitemapIndex) + + serveSitemap :: DynamicPath -> ServerPartE Response + serveSitemap dpath = + case lookup "filename" dpath of + Just filename + | [basename, "xml"] <- splitOn "." filename + , Just i <- readMaybe basename -> do + Sitemap{..} <- liftIO $ readAsyncCache sitemapCache + guard (i < length sitemaps) + cacheControlWithoutETag [Public, maxAgeDays 1] + return (toResponse (sitemaps !! i)) + _ -> mzero -- Generates a list of sitemap entries corresponding to hackage pages, then -- builds and returns an XML sitemap. - updateSitemapCache :: IO XMLResponse + updateSitemapCache :: IO Sitemap updateSitemapCache = do alltags <- queryGetTagList pkgIndex <- queryGetPackageIndex docIndex <- queryDocumentationIndex - let sitemap = generateSitemap serverBaseURI pageBuildDate + let sitemaps = generateSitemap serverBaseURI pageBuildDate (map fst alltags) pkgIndex docIndex - return (XMLResponse sitemap) + uriScheme i = "/sitemap/" <> show i <> ".xml" + sitemapIndex = renderSitemapIndex serverBaseURI (map uriScheme [0..(length sitemaps - 1)]) + return $ Sitemap (XMLResponse sitemapIndex) (map XMLResponse sitemaps) pageBuildDate :: T.Text pageBuildDate = T.pack (showGregorian (utctDay initTime)) @@ -132,9 +166,9 @@ generateSitemap :: URI -> [Tag] -> PackageIndex.PackageIndex PkgInfo -> Map.Map PackageId a - -> ByteString + -> [ByteString] generateSitemap serverBaseURI pageBuildDate alltags pkgIndex docIndex = - renderSitemap serverBaseURI allEntries + renderSitemap serverBaseURI <$> chunksOf 50000 allEntries where -- Combine and build sitemap allEntries = miscEntries diff --git a/src/Distribution/Server/Features/Sitemap/Functions.hs b/src/Distribution/Server/Features/Sitemap/Functions.hs index c13208c15..7eae7f0ec 100644 --- a/src/Distribution/Server/Features/Sitemap/Functions.hs +++ b/src/Distribution/Server/Features/Sitemap/Functions.hs @@ -23,6 +23,7 @@ module Distribution.Server.Features.Sitemap.Functions ( SitemapEntry , ChangeFreq(..) + , renderSitemapIndex , renderSitemap , urlsToSitemapEntries , pathsAndDatesToSitemapEntries @@ -47,6 +48,22 @@ data SitemapEntry = SitemapEntry { data ChangeFreq = Monthly | Weekly | Daily +-- | Generate a sitemap index file from each sitemap uri. +renderSitemapIndex :: URI -> [String] -> ByteString +renderSitemapIndex serverBaseURI sitemaps = + xrender $ + doc defaultDocInfo $ + xelem "sitemapindex" $ + xattr "xmlns" "http://www.sitemaps.org/schemas/sitemap/0.9" + <#> map renderLink sitemaps + where + serverBaseURI' = T.pack (show serverBaseURI) + renderLink :: String -> Xml Elem + renderLink uri = xelem "sitemap" $ + xelems [ + xelem "loc" (xtext (serverBaseURI' <> T.pack (uri))) + ] + -- | Primary function - generates the XML file from a list of Nodes. renderSitemap :: URI -> [SitemapEntry] -> ByteString renderSitemap serverBaseURI entries = From b9330e07d3f1d38a23da367310afbcea7a0c6d5e Mon Sep 17 00:00:00 2001 From: Alias Qli <2576814881@qq.com> Date: Sat, 16 Jul 2022 16:28:36 +0800 Subject: [PATCH 08/43] Add sitemap link for subdirectories --- src/Distribution/Server/Features.hs | 1 + src/Distribution/Server/Features/Sitemap.hs | 68 +++++++++++++++------ 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/Distribution/Server/Features.hs b/src/Distribution/Server/Features.hs index f8a8e362e..54c584c50 100644 --- a/src/Distribution/Server/Features.hs +++ b/src/Distribution/Server/Features.hs @@ -337,6 +337,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do coreFeature documentationCoreFeature tagsFeature + tarIndexCacheFeature packageFeedFeature <- mkPackageFeedFeature coreFeature diff --git a/src/Distribution/Server/Features/Sitemap.hs b/src/Distribution/Server/Features/Sitemap.hs index 429adc67b..9227d70e7 100644 --- a/src/Distribution/Server/Features/Sitemap.hs +++ b/src/Distribution/Server/Features/Sitemap.hs @@ -1,4 +1,5 @@ {-# LANGUAGE RecordWildCards, NamedFieldPuns, RecursiveDo #-} +{-# LANGUAGE TupleSections #-} module Distribution.Server.Features.Sitemap ( SitemapFeature(..) @@ -28,6 +29,10 @@ import Network.URI import Control.DeepSeq import Text.Read import Data.List.Split +import Distribution.Server.Framework.BlobStorage +import Distribution.Server.Features.TarIndexCache +import qualified Data.TarIndex as Tar +import System.FilePath (takeExtension) data Sitemap = Sitemap @@ -52,6 +57,7 @@ initSitemapFeature :: ServerEnv -> IO ( CoreFeature -> DocumentationFeature -> TagsFeature + -> TarIndexCacheFeature -> IO SitemapFeature) initSitemapFeature env@ServerEnv{ serverCacheDelay, @@ -60,10 +66,11 @@ initSitemapFeature env@ServerEnv{ serverCacheDelay, return $ \coref@CoreFeature{..} docsCore@DocumentationFeature{..} - tagsf@TagsFeature{..} -> do + tagsf@TagsFeature{..} + tarf@TarIndexCacheFeature{..} -> do rec let (feature, updateSitemapCache) = - sitemapFeature env coref docsCore tagsf + sitemapFeature env coref docsCore tagsf tarf initTime sitemapCache sitemapCache <- newAsyncCacheNF updateSitemapCache @@ -80,6 +87,7 @@ sitemapFeature :: ServerEnv -> CoreFeature -> DocumentationFeature -> TagsFeature + -> TarIndexCacheFeature -> UTCTime -> AsyncCache Sitemap -> (SitemapFeature, IO Sitemap) @@ -87,6 +95,7 @@ sitemapFeature ServerEnv{..} CoreFeature{..} DocumentationFeature{..} TagsFeature{..} + TarIndexCacheFeature{cachedTarIndex} initTime sitemapCache = (SitemapFeature{..}, updateSitemapCache) @@ -151,10 +160,10 @@ sitemapFeature ServerEnv{..} pkgIndex <- queryGetPackageIndex docIndex <- queryDocumentationIndex - let sitemaps = generateSitemap serverBaseURI pageBuildDate + sitemaps <- generateSitemap serverBaseURI pageBuildDate (map fst alltags) - pkgIndex docIndex - uriScheme i = "/sitemap/" <> show i <> ".xml" + pkgIndex docIndex cachedTarIndex + let uriScheme i = "/sitemap/" <> show i <> ".xml" sitemapIndex = renderSitemapIndex serverBaseURI (map uriScheme [0..(length sitemaps - 1)]) return $ Sitemap (XMLResponse sitemapIndex) (map XMLResponse sitemaps) @@ -165,19 +174,21 @@ generateSitemap :: URI -> T.Text -> [Tag] -> PackageIndex.PackageIndex PkgInfo - -> Map.Map PackageId a - -> [ByteString] -generateSitemap serverBaseURI pageBuildDate alltags pkgIndex docIndex = - renderSitemap serverBaseURI <$> chunksOf 50000 allEntries + -> Map.Map PackageId BlobId + -> (BlobId -> IO Tar.TarIndex) + -> IO [ByteString] +generateSitemap serverBaseURI pageBuildDate alltags pkgIndex docIndex cachedTarIndex = do + versionedDocSubEntries <- versionedDocSubEntriesIO + let -- Combine and build sitemap + allEntries = miscEntries + ++ tagEntries + ++ nameEntries + ++ nameVersEntries + ++ baseDocEntries + ++ versionedDocEntries + ++ versionedDocSubEntries + pure $ renderSitemap serverBaseURI <$> chunksOf 50000 allEntries where - -- Combine and build sitemap - allEntries = miscEntries - ++ tagEntries - ++ nameEntries - ++ nameVersEntries - ++ baseDocEntries - ++ versionedDocEntries - -- Misc. pages -- e.g. ["http://myhackage.com/index", ...] miscEntries = urlsToSitemapEntries miscPages pageBuildDate Weekly 0.75 @@ -258,3 +269,26 @@ generateSitemap serverBaseURI pageBuildDate alltags pkgIndex docIndex = , Map.member (packageId pkg) docIndex ] pageBuildDate Monthly 0.25 + + -- Versioned doc pages in subdirectories + -- versionedSubDocURIs :: [path :: String] + -- e.g. ["http://myhackage.com/packages/mypackage-1.0.2/docs/Lib.html", ...] + versionedDocSubEntriesIO = do + let pkgs = [ (pkg , blob) + | pkg <- concat pkgss + , Just blob <- [Map.lookup (packageId pkg) docIndex] + ] + pkgIndices <- traverse (\(pkg, blob) -> (pkg,) <$> cachedTarIndex blob) pkgs + pure $ urlsToSitemapEntries + [ prefixPkgURI ++ display (packageId pkg) ++ "/docs" ++ fp + | (pkg, tarIndex) <- pkgIndices + , Just tar <- [Tar.lookup tarIndex ""] + , fp <- entryToPaths "/" tar + , takeExtension fp == ".html" + ] + pageBuildDate Monthly 0.25 + + entryToPaths :: FilePath -> Tar.TarIndexEntry -> [FilePath] + entryToPaths _ (Tar.TarFileEntry _) = [] + entryToPaths base (Tar.TarDir content) = map ((base ) . fst) content ++ + [ file | (folder, entry) <- content, file <- entryToPaths (base folder) entry ] From 10d14a21377196546d089d3c094a49e55f72c0ff Mon Sep 17 00:00:00 2001 From: Andreas Abel Date: Thu, 21 Jul 2022 12:33:17 +0200 Subject: [PATCH 09/43] Fix `non-canonical-return` warnings --- src/Distribution/Server/Features/Security/Migration.hs | 4 ++-- src/Distribution/Server/Framework/BackupRestore.hs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Distribution/Server/Features/Security/Migration.hs b/src/Distribution/Server/Features/Security/Migration.hs index 5fb09f054..b9ee61f50 100644 --- a/src/Distribution/Server/Features/Security/Migration.hs +++ b/src/Distribution/Server/Features/Security/Migration.hs @@ -206,11 +206,11 @@ data Migrated a = Migrated MigrationStats a | AlreadyMigrated a deriving (Functor) instance Applicative Migrated where - pure = return + pure = AlreadyMigrated f <*> x = do f' <- f ; x' <- x ; return $ f' x' instance Monad Migrated where - return = AlreadyMigrated + return = pure AlreadyMigrated a >>= f = f a Migrated stats a >>= f = case f a of diff --git a/src/Distribution/Server/Framework/BackupRestore.hs b/src/Distribution/Server/Framework/BackupRestore.hs index 64e5c0bfa..d2158f74f 100644 --- a/src/Distribution/Server/Framework/BackupRestore.hs +++ b/src/Distribution/Server/Framework/BackupRestore.hs @@ -251,7 +251,7 @@ data Restore a = RestoreDone a | RestoreFindBlob BlobId (Bool -> Restore a) instance Monad Restore where - return = RestoreDone + return = pure RestoreDone x >>= g = g x RestoreFail err >>= _ = RestoreFail err RestoreAddBlob bs f >>= g = RestoreAddBlob bs $ \bid -> f bid >>= g @@ -270,7 +270,7 @@ instance Functor Restore where fmap = liftM instance Applicative Restore where - pure = return + pure = RestoreDone mf <*> mx = do f <- mf ; x <- mx ; return (f x) runRestore :: BlobStores -> Restore a -> IO (Either String a) From 1daad17a9defc56211db49ebd0dc7a0c9ab10870 Mon Sep 17 00:00:00 2001 From: Andreas Abel Date: Thu, 21 Jul 2022 13:03:26 +0200 Subject: [PATCH 10/43] Bump CI to GHC 9.2.3 and restrict to master branch --- .github/workflows/haskell-ci.yml | 32 +++++++++++++++++++------------- .github/workflows/nix-shell.yml | 10 ++++++++-- cabal.haskell-ci | 2 ++ hackage-server.cabal | 2 +- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/.github/workflows/haskell-ci.yml b/.github/workflows/haskell-ci.yml index 0aa3a3c9e..84b405fcf 100644 --- a/.github/workflows/haskell-ci.yml +++ b/.github/workflows/haskell-ci.yml @@ -8,18 +8,24 @@ # # For more information, see https://github.com/haskell-CI/haskell-ci # -# version: 0.14.3.20220416 +# version: 0.15.20220710 # -# REGENDATA ("0.14.3.20220416",["github","hackage-server.cabal"]) +# REGENDATA ("0.15.20220710",["github","hackage-server.cabal"]) # name: Haskell-CI on: - - push - - pull_request + push: + branches: + - master + - ci* + pull_request: + branches: + - master + - ci* jobs: linux: name: Haskell-CI - Linux - ${{ matrix.compiler }} - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 timeout-minutes: 60 container: @@ -28,9 +34,9 @@ jobs: strategy: matrix: include: - - compiler: ghc-9.2.2 + - compiler: ghc-9.2.3 compilerKind: ghc - compilerVersion: 9.2.2 + compilerVersion: 9.2.3 setup-method: ghcup allow-failure: false - compiler: ghc-9.0.2 @@ -56,10 +62,10 @@ jobs: apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common libtinfo5 if [ "${{ matrix.setup-method }}" = ghcup ]; then mkdir -p "$HOME/.ghcup/bin" - curl -sL https://downloads.haskell.org/ghcup/0.1.17.5/x86_64-linux-ghcup-0.1.17.5 > "$HOME/.ghcup/bin/ghcup" + curl -sL https://downloads.haskell.org/ghcup/0.1.17.8/x86_64-linux-ghcup-0.1.17.8 > "$HOME/.ghcup/bin/ghcup" chmod a+x "$HOME/.ghcup/bin/ghcup" - "$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" - "$HOME/.ghcup/bin/ghcup" install cabal 3.6.2.0 + "$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" || (cat "$HOME"/.ghcup/logs/*.* && false) + "$HOME/.ghcup/bin/ghcup" install cabal 3.6.2.0 || (cat "$HOME"/.ghcup/logs/*.* && false) apt-get update apt-get install -y libbrotli-dev else @@ -67,9 +73,9 @@ jobs: apt-get update apt-get install -y "$HCNAME" libbrotli-dev mkdir -p "$HOME/.ghcup/bin" - curl -sL https://downloads.haskell.org/ghcup/0.1.17.5/x86_64-linux-ghcup-0.1.17.5 > "$HOME/.ghcup/bin/ghcup" + curl -sL https://downloads.haskell.org/ghcup/0.1.17.8/x86_64-linux-ghcup-0.1.17.8 > "$HOME/.ghcup/bin/ghcup" chmod a+x "$HOME/.ghcup/bin/ghcup" - "$HOME/.ghcup/bin/ghcup" install cabal 3.6.2.0 + "$HOME/.ghcup/bin/ghcup" install cabal 3.6.2.0 || (cat "$HOME"/.ghcup/logs/*.* && false) fi env: HCKIND: ${{ matrix.compilerKind }} @@ -212,7 +218,7 @@ jobs: ${CABAL} -vnormal check - name: haddock run: | - $CABAL v2-haddock $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all + $CABAL v2-haddock --haddock-all $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all - name: unconstrained build run: | rm -f cabal.project.local diff --git a/.github/workflows/nix-shell.yml b/.github/workflows/nix-shell.yml index 06b6e2781..80cb6f1ac 100644 --- a/.github/workflows/nix-shell.yml +++ b/.github/workflows/nix-shell.yml @@ -1,7 +1,13 @@ name: "Test nix-shell" on: - - push - - pull_request + push: + branches: + - master + - ci* + pull_request: + branches: + - master + - ci* jobs: nix-shell: runs-on: ubuntu-latest diff --git a/cabal.haskell-ci b/cabal.haskell-ci index bc8774bef..8b3444e8f 100644 --- a/cabal.haskell-ci +++ b/cabal.haskell-ci @@ -1,3 +1,5 @@ +branches: master ci* + installed: +all -Cabal -text -parsec -- -- irc-channels works with GHA, but why send to a channel diff --git a/hackage-server.cabal b/hackage-server.cabal index c32615ea9..ba6b86341 100644 --- a/hackage-server.cabal +++ b/hackage-server.cabal @@ -27,7 +27,7 @@ copyright: 2008-2015 Duncan Coutts, license: BSD-3-Clause license-file: LICENSE -tested-with: GHC == { 9.2.2, 9.0.2, 8.10.7, 8.8.4 } +tested-with: GHC == { 9.2.3, 9.0.2, 8.10.7, 8.8.4 } data-dir: datafiles data-files: From 53295949e966567f5977ea1d99880b52cfee2630 Mon Sep 17 00:00:00 2001 From: Alias Qli <2576814881@qq.com> Date: Fri, 22 Jul 2022 04:15:10 +0800 Subject: [PATCH 11/43] Check authorisation (#1111) --- src/Distribution/Server/Features/UserDetails.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Distribution/Server/Features/UserDetails.hs b/src/Distribution/Server/Features/UserDetails.hs index 04b5e750a..6240e9e8c 100644 --- a/src/Distribution/Server/Features/UserDetails.hs +++ b/src/Distribution/Server/Features/UserDetails.hs @@ -330,6 +330,7 @@ userDetailsFeature templates userDetailsState UserFeature{..} CoreFeature{..} Up handlerGetUserNameContactHtml :: DynamicPath -> ServerPartE Response handlerGetUserNameContactHtml dpath = do (uid, uinfo) <- lookupUserNameFull =<< userNameInPath dpath + guardAuthorised_ [IsUserId uid, InGroup adminGroup] template <- getTemplate templates "user-details-form.html" udetails <- queryUserDetails uid showConfirmationOfSave <- not . null <$> queryString (lookBSs "showConfirmationOfSave") From 969915eaf797170275453665ca938a0ac9de3c57 Mon Sep 17 00:00:00 2001 From: Alias Qli <2576814881@qq.com> Date: Sat, 23 Jul 2022 17:09:36 +0800 Subject: [PATCH 12/43] Dynamically add css piece --- hackage-server.cabal | 1 + .../Server/Features/Documentation.hs | 12 +++++++-- .../Server/Features/PackageCandidates.hs | 2 +- .../Server/Features/PackageContents.hs | 2 +- src/Distribution/Server/Util/ServeTarball.hs | 27 ++++++++++++------- 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/hackage-server.cabal b/hackage-server.cabal index ba6b86341..c7e27f29a 100644 --- a/hackage-server.cabal +++ b/hackage-server.cabal @@ -404,6 +404,7 @@ library lib-server , semigroups ^>= 0.19 , split ^>= 0.2 , stm ^>= 2.5.0 + , stringsearch ^>= 0.3.6.6 , tagged ^>= 0.8.5 , xhtml ^>= 3000.2 , xmlgen ^>= 0.6 diff --git a/src/Distribution/Server/Features/Documentation.hs b/src/Distribution/Server/Features/Documentation.hs index 98dcfbd81..9fbbb14bb 100644 --- a/src/Distribution/Server/Features/Documentation.hs +++ b/src/Distribution/Server/Features/Documentation.hs @@ -32,7 +32,9 @@ import Distribution.Package import qualified Distribution.Parsec as P import qualified Data.ByteString.Char8 as C -import qualified Data.ByteString.Lazy as BSL +import qualified Data.ByteString.Lazy.Char8 as BSL +import qualified Data.ByteString.Lazy.Search as BSL +import qualified Data.ByteString.Char8 as BS import qualified Data.Map as Map import Data.Function (fix) @@ -283,7 +285,13 @@ documentationFeature name let maxAge = documentationCacheTime age ServerTarball.serveTarball (display pkgid ++ " documentation") [{-no index-}] (display pkgid ++ "-docs") - tarball index [Public, maxAge] etag + tarball index [Public, maxAge] etag (Just rewriteDocs) + + rewriteDocs :: BSL.ByteString -> BSL.ByteString + rewriteDocs dochtml = case BSL.breakFindAfter (BS.pack "") dochtml of + ((h,t),True) -> h `BSL.append` extraCss `BSL.append` t + _ -> dochtml + where extraCss = BSL.pack "" -- The cache time for documentation starts at ten minutes and -- increases exponentially for four days, when it cuts off at diff --git a/src/Distribution/Server/Features/PackageCandidates.hs b/src/Distribution/Server/Features/PackageCandidates.hs index d222beb3f..4b0e2e819 100644 --- a/src/Distribution/Server/Features/PackageCandidates.hs +++ b/src/Distribution/Server/Features/PackageCandidates.hs @@ -611,7 +611,7 @@ candidatesFeature ServerEnv{serverBlobStore = store} Right (fp, etag, index) -> serveTarball (display (packageId pkg) ++ " candidate source tarball") ["index.html"] (display (packageId pkg)) fp index - [Public, maxAgeMinutes 5] etag + [Public, maxAgeMinutes 5] etag Nothing unpackUtf8 :: BS.ByteString -> String unpackUtf8 = T.unpack diff --git a/src/Distribution/Server/Features/PackageContents.hs b/src/Distribution/Server/Features/PackageContents.hs index 7770ffecf..a3f5a2382 100644 --- a/src/Distribution/Server/Features/PackageContents.hs +++ b/src/Distribution/Server/Features/PackageContents.hs @@ -208,7 +208,7 @@ packageContentsFeature CoreFeature{ coreResource = CoreResource{ Right (fp, etag, index) -> serveTarball (display (packageId pkg) ++ " source tarball") [] (display (packageId pkg)) fp index - [Public, maxAgeDays 30] etag + [Public, maxAgeDays 30] etag Nothing unpackUtf8 :: BS.ByteString -> String unpackUtf8 = T.unpack diff --git a/src/Distribution/Server/Util/ServeTarball.hs b/src/Distribution/Server/Util/ServeTarball.hs index b4391f6d2..5d975f490 100644 --- a/src/Distribution/Server/Util/ServeTarball.hs +++ b/src/Distribution/Server/Util/ServeTarball.hs @@ -52,8 +52,9 @@ serveTarball :: (MonadIO m, MonadPlus m) -> TarIndex -- index for tarball -> [CacheControl] -> ETag -- the etag + -> Maybe (BS.ByteString -> BS.ByteString) -- optional transform to files -> ServerPartT m Response -serveTarball descr indices tarRoot tarball tarIndex cacheCtls etag = do +serveTarball descr indices tarRoot tarball tarIndex cacheCtls etag transform = do rq <- askRq action GET $ remainingPath $ \paths -> do @@ -74,7 +75,7 @@ serveTarball descr indices tarRoot tarball tarIndex cacheCtls etag = do Just (TarIndex.TarFileEntry off) -> do cacheControl cacheCtls etag - tfe <- liftIO $ serveTarEntry tarball off path + tfe <- liftIO $ serveTarEntry_ transform tarball off path ok (toResponse tfe) _ -> mzero @@ -116,22 +117,30 @@ renderDirIndex descr topdir topentries = loadTarEntry :: FilePath -> TarIndex.TarEntryOffset -> IO (Either String (Tar.FileSize, BS.ByteString)) -loadTarEntry tarfile off = do +loadTarEntry = loadTarEntry_ Nothing + +loadTarEntry_ :: Maybe (BS.ByteString -> BS.ByteString) -> FilePath -> TarIndex.TarEntryOffset -> IO (Either String (Tar.FileSize, BS.ByteString)) +loadTarEntry_ transform tarfile off = do htar <- openFile tarfile ReadMode hSeek htar AbsoluteSeek (fromIntegral $ off * 512) header <- BS.hGet htar 512 case Tar.read header of (Tar.Next Tar.Entry{Tar.entryContent = Tar.NormalFile _ size} _) -> do body <- BS.hGet htar (fromIntegral size) - return $ Right (size, body) + case transform of + Just f -> let x = f body in return $ Right (BS.length x, x) + Nothing -> return $ Right (size, body) _ -> fail "failed to read entry from tar file" serveTarEntry :: FilePath -> TarIndex.TarEntryOffset -> FilePath -> IO Response -serveTarEntry tarfile off fname = do - Right (size, body) <- loadTarEntry tarfile off - return . setHeader "Content-Length" (show size) - . setHeader "Content-Type" mimeType - $ resultBS 200 body +serveTarEntry = serveTarEntry_ Nothing + +serveTarEntry_ :: Maybe (BS.ByteString -> BS.ByteString) -> FilePath -> TarIndex.TarEntryOffset -> FilePath -> IO Response +serveTarEntry_ transform tarfile off fname = do + Right (size, body) <- loadTarEntry_ transform tarfile off + return . ((setHeader "Content-Length" (show size)) . + (setHeader "Content-Type" mimeType)) $ + resultBS 200 body where mimeType = mime fname constructTarIndexFromFile :: FilePath -> IO TarIndex From c84f467c2453e59f6bfd24f06ee3615befddcb18 Mon Sep 17 00:00:00 2001 From: Andreas Abel Date: Sun, 24 Jul 2022 16:57:54 +0200 Subject: [PATCH 13/43] Fix #1105: change order of markdown parsers to allow pipes in lists --- src/Distribution/Server/Util/Markdown.hs | 56 +++++++++++++++++++++--- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/src/Distribution/Server/Util/Markdown.hs b/src/Distribution/Server/Util/Markdown.hs index bc8fb84a6..033ab4f00 100644 --- a/src/Distribution/Server/Util/Markdown.hs +++ b/src/Distribution/Server/Util/Markdown.hs @@ -120,6 +120,30 @@ adjustRelativeLink url --

Published to http://hackage.haskell.org/foo3/bar.

-- -- +-- >>> renderMarkdown "test" "Issue #1105:\n- pipes\n- like `a|b`\n- should be allowed in lists" +--

Issue #1105:

+--
    +--
  • pipes +--
  • +--
  • like a|b +--
  • +--
  • should be allowed in lists +--
  • +--
+-- +-- +-- >>> renderMarkdown "test" "Tables should be supported:\n\nfoo|bar\n---|---\n" +--

Tables should be supported:

+-- +-- +-- +-- +-- +-- +-- +--
foobar
+-- +-- renderMarkdown :: String -- ^ Name or path of input. -> BS.ByteString -- ^ Commonmark text input. @@ -160,11 +184,33 @@ renderMarkdown' -> BS.ByteString -- ^ Commonmark text input. -> XHtml.Html -- ^ Rendered HTML. renderMarkdown' render name md = - either (const $ XHtml.pre XHtml.<< T.unpack txt) (XHtml.primHtml . T.unpack . sanitizeBalance . TL.toStrict . render) $ - runIdentity (commonmarkWith (mathSpec <> gfmExtensions <> defaultSyntaxSpec) - name - txt) - where txt = T.decodeUtf8With T.lenientDecode . BS.toStrict $ md + either (const $ fallback) mdToHTML $ + runIdentity $ commonmarkWith spec name txt + where + -- Input + txt = T.decodeUtf8With T.lenientDecode . BS.toStrict $ md + -- Fall back to HTML if there is a parse error for markdown + fallback = XHtml.pre XHtml.<< T.unpack txt + -- Conversion of parsed md to HTML + mdToHTML = XHtml.primHtml . T.unpack . sanitizeBalance . TL.toStrict . render + -- Specification of the markdown parser. + -- Andreas Abel, 2022-07-21, issue #1105. + -- Workaround for https://github.com/jgm/commonmark-hs/issues/95: + -- Put the table parser last. + spec = mconcat $ + mathSpec : + -- all the gfm extensions except for tables + emojiSpec : + strikethroughSpec : + autolinkSpec : + autoIdentifiersSpec : + taskListSpec : + footnoteSpec : + -- the default syntax + defaultSyntaxSpec : + -- the problematic table parser + pipeTableSpec : + [] -- | Does the file extension suggest that the file is in markdown syntax? supposedToBeMarkdown :: FilePath -> Bool From 33d7807f14c6a4d529d53a54f28dd6f7db63ad98 Mon Sep 17 00:00:00 2001 From: Andreas Abel Date: Tue, 23 Aug 2022 15:08:39 +0200 Subject: [PATCH 14/43] Fix #1128, fix #1130 by adding bounds to Cabal-syntax and haddock-library --- hackage-server.cabal | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/hackage-server.cabal b/hackage-server.cabal index ba6b86341..934ac195c 100644 --- a/hackage-server.cabal +++ b/hackage-server.cabal @@ -118,6 +118,9 @@ common defaults build-depends: , aeson ^>= 2.0.3.0 , Cabal ^>= 3.6.3.0 + , Cabal-syntax ^>= 3.6.0.0 + -- Cabal-syntax needs to be bound to constrain hackage-security + -- see https://github.com/haskell/hackage-server/issues/1130 , fail ^>= 4.9.0 -- we use Control.Monad.Except, introduced in mtl-2.2.1 , network >= 3 && < 3.2 @@ -390,9 +393,14 @@ library lib-server , cryptohash-sha256 ^>= 0.11.100 , csv ^>= 0.1 , ed25519 ^>= 0.0.5 - , hackage-security ^>= 0.6 + , hackage-security >= 0.6 && < 0.7 + -- N.B: hackage-security-0.6.2 uses Cabal-syntax-3.8.1.0 + -- see https://github.com/haskell/hackage-server/issues/1130 + -- Thus, we need to include Cabal-syntax as dependency explicitly , hackage-security-HTTP ^>= 0.1.1 - , haddock-library > 1.7 && < 2 + , haddock-library >= 1.7.0 && < 1.11 + -- haddock-library-1.11.0 changed type of markupOrderedList + -- see https://github.com/haskell/hackage-server/issues/1128 , happstack-server ^>= 7.7.1 , hashable ^>= 1.3 || ^>= 1.4 , hslogger ^>= 1.3.1 From 2377900828ba1d3086cbb01f15f45573ae05edfa Mon Sep 17 00:00:00 2001 From: Andreas Abel Date: Tue, 23 Aug 2022 13:35:33 +0200 Subject: [PATCH 15/43] Bump CI to 9.2.4 and some deps --- .github/workflows/haskell-ci.yml | 12 ++++++------ cabal.haskell-ci | 7 ++++++- hackage-server.cabal | 18 +++++++++--------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/.github/workflows/haskell-ci.yml b/.github/workflows/haskell-ci.yml index 84b405fcf..4f2631d62 100644 --- a/.github/workflows/haskell-ci.yml +++ b/.github/workflows/haskell-ci.yml @@ -8,9 +8,9 @@ # # For more information, see https://github.com/haskell-CI/haskell-ci # -# version: 0.15.20220710 +# version: 0.15.20220822 # -# REGENDATA ("0.15.20220710",["github","hackage-server.cabal"]) +# REGENDATA ("0.15.20220822",["github","hackage-server.cabal"]) # name: Haskell-CI on: @@ -34,9 +34,9 @@ jobs: strategy: matrix: include: - - compiler: ghc-9.2.3 + - compiler: ghc-9.2.4 compilerKind: ghc - compilerVersion: 9.2.3 + compilerVersion: 9.2.4 setup-method: ghcup allow-failure: false - compiler: ghc-9.0.2 @@ -62,7 +62,7 @@ jobs: apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common libtinfo5 if [ "${{ matrix.setup-method }}" = ghcup ]; then mkdir -p "$HOME/.ghcup/bin" - curl -sL https://downloads.haskell.org/ghcup/0.1.17.8/x86_64-linux-ghcup-0.1.17.8 > "$HOME/.ghcup/bin/ghcup" + curl -sL https://downloads.haskell.org/ghcup/0.1.18.0/x86_64-linux-ghcup-0.1.18.0 > "$HOME/.ghcup/bin/ghcup" chmod a+x "$HOME/.ghcup/bin/ghcup" "$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" || (cat "$HOME"/.ghcup/logs/*.* && false) "$HOME/.ghcup/bin/ghcup" install cabal 3.6.2.0 || (cat "$HOME"/.ghcup/logs/*.* && false) @@ -73,7 +73,7 @@ jobs: apt-get update apt-get install -y "$HCNAME" libbrotli-dev mkdir -p "$HOME/.ghcup/bin" - curl -sL https://downloads.haskell.org/ghcup/0.1.17.8/x86_64-linux-ghcup-0.1.17.8 > "$HOME/.ghcup/bin/ghcup" + curl -sL https://downloads.haskell.org/ghcup/0.1.18.0/x86_64-linux-ghcup-0.1.18.0 > "$HOME/.ghcup/bin/ghcup" chmod a+x "$HOME/.ghcup/bin/ghcup" "$HOME/.ghcup/bin/ghcup" install cabal 3.6.2.0 || (cat "$HOME"/.ghcup/logs/*.* && false) fi diff --git a/cabal.haskell-ci b/cabal.haskell-ci index 8b3444e8f..e8ddbd02d 100644 --- a/cabal.haskell-ci +++ b/cabal.haskell-ci @@ -13,4 +13,9 @@ installed: +all -Cabal -text -parsec -- Use Ubuntu 20.04 distribution: focal -apt: libbrotli-dev \ No newline at end of file +apt: libbrotli-dev + +-- Make sure the haddock step is included, +-- even though we don't define any library. +haddock-components: all + -- since haskell-ci 0.15.20220822 diff --git a/hackage-server.cabal b/hackage-server.cabal index 934ac195c..329faf15e 100644 --- a/hackage-server.cabal +++ b/hackage-server.cabal @@ -27,7 +27,7 @@ copyright: 2008-2015 Duncan Coutts, license: BSD-3-Clause license-file: LICENSE -tested-with: GHC == { 9.2.3, 9.0.2, 8.10.7, 8.8.4 } +tested-with: GHC == { 9.2.4, 9.0.2, 8.10.7, 8.8.4 } data-dir: datafiles data-files: @@ -111,12 +111,12 @@ common defaults , process >= 1.6 && < 1.7 , text ^>= 1.2.5.0 || ^>= 2.0 , time >= 1.9 && < 1.13 - , transformers >= 0.5 && < 0.6 - , unix >= 2.7 && < 2.8 + , transformers >= 0.5 && < 0.7 + , unix >= 2.7 && < 2.9 , scientific -- other dependencies shared by most components build-depends: - , aeson ^>= 2.0.3.0 + , aeson ^>= 2.0.3.0 || ^>= 2.1.0.0 , Cabal ^>= 3.6.3.0 , Cabal-syntax ^>= 3.6.0.0 -- Cabal-syntax needs to be bound to constrain hackage-security @@ -129,7 +129,7 @@ common defaults , parsec ^>= 3.1.13 , tar ^>= 0.5 , unordered-containers ^>= 0.2.10 - , vector ^>= 0.12 + , vector ^>= 0.12 || ^>= 0.13.0.0 , zlib ^>= 0.6.2 ghc-options: -Wall -fwarn-tabs -fno-warn-unused-do-bind -fno-warn-deprecated-flags -funbox-strict-fields @@ -377,7 +377,7 @@ library lib-server , async ^>= 2.2.1 -- requires bumping http-io-streams , attoparsec ^>= 0.14.4 - , attoparsec-iso8601 ^>= 1.0 + , attoparsec-iso8601 ^>= 1.0 || ^>= 1.1.0.0 , base16-bytestring ^>= 1.0 -- requires bumping http-io-streams , base64-bytestring ^>= 1.2.1.0 @@ -401,15 +401,15 @@ library lib-server , haddock-library >= 1.7.0 && < 1.11 -- haddock-library-1.11.0 changed type of markupOrderedList -- see https://github.com/haskell/hackage-server/issues/1128 - , happstack-server ^>= 7.7.1 - , hashable ^>= 1.3 || ^>= 1.4 + , happstack-server ^>= 7.7.1 || ^>= 7.8.0 + , hashable ^>= 1.3 || ^>= 1.4 , hslogger ^>= 1.3.1 , lifted-base ^>= 0.2.1 , mime-mail ^>= 0.5 , random ^>= 1.2 , rss ^>= 3000.2.0.7 , safecopy ^>= 0.10 - , semigroups ^>= 0.19 + , semigroups ^>= 0.20 , split ^>= 0.2 , stm ^>= 2.5.0 , tagged ^>= 0.8.5 From db0f10ad45b674f16c92b0e611ff1ad5b717ba90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9cate=20Moonlight?= Date: Fri, 26 Aug 2022 00:37:31 +0200 Subject: [PATCH 16/43] Force .txt and .text to have UTF-8 MIME charset (#1133) --- src/Distribution/Server/Framework/HappstackUtils.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Distribution/Server/Framework/HappstackUtils.hs b/src/Distribution/Server/Framework/HappstackUtils.hs index 41bce1b74..f10491f08 100644 --- a/src/Distribution/Server/Framework/HappstackUtils.hs +++ b/src/Distribution/Server/Framework/HappstackUtils.hs @@ -81,6 +81,8 @@ mime x = , ("chs", "text/plain; charset=utf-8") , ("c", " text/plain; charset=utf-8") , ("h", " text/plain; charset=utf-8") + , ("text", "text/plain; charset=utf-8") + , ("txt", "text/plain; charset=utf-8") ] From f6c1e48f8da9293055ff9e86aabd8a33f6594893 Mon Sep 17 00:00:00 2001 From: Alias Qli <2576814881@qq.com> Date: Fri, 26 Aug 2022 23:38:32 +0800 Subject: [PATCH 17/43] Upgrade to haddock-library-1.11.0 (#1126) --- hackage-server.cabal | 6 +++--- .../Server/Features/Search/ExtractDescriptionTerms.hs | 2 +- src/Distribution/Server/Pages/Package/HaddockHtml.hs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/hackage-server.cabal b/hackage-server.cabal index 329faf15e..74e511955 100644 --- a/hackage-server.cabal +++ b/hackage-server.cabal @@ -111,8 +111,8 @@ common defaults , process >= 1.6 && < 1.7 , text ^>= 1.2.5.0 || ^>= 2.0 , time >= 1.9 && < 1.13 - , transformers >= 0.5 && < 0.7 - , unix >= 2.7 && < 2.9 + , transformers >= 0.5 && < 0.6 + , unix >= 2.7 && < 2.8 , scientific -- other dependencies shared by most components build-depends: @@ -398,7 +398,7 @@ library lib-server -- see https://github.com/haskell/hackage-server/issues/1130 -- Thus, we need to include Cabal-syntax as dependency explicitly , hackage-security-HTTP ^>= 0.1.1 - , haddock-library >= 1.7.0 && < 1.11 + , haddock-library ^>= 1.11.0 -- haddock-library-1.11.0 changed type of markupOrderedList -- see https://github.com/haskell/hackage-server/issues/1128 , happstack-server ^>= 7.7.1 || ^>= 7.8.0 diff --git a/src/Distribution/Server/Features/Search/ExtractDescriptionTerms.hs b/src/Distribution/Server/Features/Search/ExtractDescriptionTerms.hs index 24a8334df..d07ed63e3 100644 --- a/src/Distribution/Server/Features/Search/ExtractDescriptionTerms.hs +++ b/src/Distribution/Server/Features/Search/ExtractDescriptionTerms.hs @@ -78,7 +78,7 @@ termsMarkup = Markup { markupBold = id, markupMonospaced = \s -> if length s > 1 then [] else s, markupUnorderedList = concat, - markupOrderedList = concat, + markupOrderedList = concat . map snd, markupDefList = concatMap (\(d,t) -> d ++ t), markupCodeBlock = const [], markupTable = concat . F.toList, diff --git a/src/Distribution/Server/Pages/Package/HaddockHtml.hs b/src/Distribution/Server/Pages/Package/HaddockHtml.hs index dba250b9f..8b5b3f0d5 100644 --- a/src/Distribution/Server/Pages/Package/HaddockHtml.hs +++ b/src/Distribution/Server/Pages/Package/HaddockHtml.hs @@ -24,7 +24,7 @@ htmlMarkup modResolv = Markup { markupBold = strong, markupMonospaced = thecode, markupUnorderedList = unordList, - markupOrderedList = ordList, + markupOrderedList = ordList . map snd, markupDefList = defList, markupCodeBlock = pre, markupHyperlink = \(Hyperlink url mLabel) -> anchor ! [href url] << maybe url showHtmlFragment mLabel, From 02cd189117c87ea73429ff21f51b437ba6d93053 Mon Sep 17 00:00:00 2001 From: Peter Becich Date: Sat, 25 Jun 2022 22:10:36 -0700 Subject: [PATCH 18/43] attempt to speed up GitHub Action for Nix Shell --- .github/workflows/nix-shell.yml | 8 ++++---- .github/workflows/test-nix-shell.sh | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) create mode 100755 .github/workflows/test-nix-shell.sh diff --git a/.github/workflows/nix-shell.yml b/.github/workflows/nix-shell.yml index 80cb6f1ac..c6ae48ce3 100644 --- a/.github/workflows/nix-shell.yml +++ b/.github/workflows/nix-shell.yml @@ -13,15 +13,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2.4.0 - - uses: cachix/install-nix-action@v16 + - uses: cachix/install-nix-action@v17 with: nix_path: nixpkgs=channel:nixos-21.11 extra_nix_config: | - trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= - substituters = https://hydra.iohk.io https://cache.nixos.org/ + trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= hackage-server.cachix.org-1:iw0iRh6+gsFIrxROFaAt5gKNgIHejKjIfyRdbpPYevY= + substituters = https://hydra.iohk.io https://cache.nixos.org/ https://hackage-server.cachix.org/ - uses: cachix/cachix-action@v10 with: # https://nix.dev/tutorials/continuous-integration-github-actions#setting-up-github-actions name: hackage-server authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - - run: nix-shell --pure --run "cabal update && cabal build all --enable-tests" + - run: nix-shell --pure --run ./.github/workflows/test-nix-shell.sh diff --git a/.github/workflows/test-nix-shell.sh b/.github/workflows/test-nix-shell.sh new file mode 100755 index 000000000..b48799885 --- /dev/null +++ b/.github/workflows/test-nix-shell.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +cabal update hackage.haskell.org,2022-08-27T00:00:00Z +cabal build all --enable-tests From e72ee4fc7044b6a2c4627262540f8a6942343a31 Mon Sep 17 00:00:00 2001 From: Gershom Bazerman Date: Mon, 26 Sep 2022 19:30:07 -0400 Subject: [PATCH 19/43] work with cabal 3.8 --- exes/BuildClient.hs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/exes/BuildClient.hs b/exes/BuildClient.hs index f72d6e251..9507d6f95 100644 --- a/exes/BuildClient.hs +++ b/exes/BuildClient.hs @@ -474,6 +474,9 @@ buildOnce opts pkgs = keepGoing $ do config <- readConfig opts notice verbosity "Initialising" + handleDoesNotExist () $ + removeDirectoryRecursive $ installDirectory opts + updatePackageIndex -- Due to caching sometimes the package repository state may lag behind the -- documentation index. Consequently, we make sure that the packages we are @@ -591,7 +594,8 @@ processPkg verbosity opts config docInfo = do createDirectoryIfMissing True $ resultsDirectory opts notice verbosity $ "Writing cabal.project for " ++ display (docInfoPackage docInfo) let projectFile = installDirectory opts "cabal.project" - writeFile projectFile $ "packages: " ++ show (docInfoTarGzURI config docInfo) + cabal opts "unpack" [show (docInfoTarGzURI config docInfo)] Nothing + writeFile projectFile $ "packages: */*.cabal" -- ++ show (docInfoTarGzURI config docInfo) setTestOutcome :: String -> [String] -> [String] setTestOutcome _ [] = [] From e907996430c6720f61ad6313d75f6f3a97cfb334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kub=C3=A1nek?= <71923533+kubaneko@users.noreply.github.com> Date: Mon, 3 Oct 2022 19:08:05 +0200 Subject: [PATCH 20/43] Updated accepted licenses (#1092) --- src/Distribution/Server/Packages/Unpack.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Distribution/Server/Packages/Unpack.hs b/src/Distribution/Server/Packages/Unpack.hs index 6836ade11..430a87426 100644 --- a/src/Distribution/Server/Packages/Unpack.hs +++ b/src/Distribution/Server/Packages/Unpack.hs @@ -42,6 +42,7 @@ import Distribution.Text -- import qualified Distribution.Compat.CharParsing as P import Distribution.Server.Util.ParseSpecVer import qualified Distribution.SPDX as SPDX +import qualified Distribution.SPDX.LicenseId as SPDX.LId import qualified Distribution.License as License import Control.Monad.Except @@ -492,7 +493,7 @@ isAcceptableLicense = either goSpdx goLegacy . licenseRaw goSimple (SPDX.ELicenseRef _) = False -- don't allow referenced licenses goSimple (SPDX.ELicenseIdPlus _) = False -- don't allow + licenses (use GPL-3.0-or-later e.g.) goSimple (SPDX.ELicenseId SPDX.CC0_1_0) = True -- CC0 isn't OSI approved, but we allow it as "PublicDomain", this is eg. PublicDomain in http://hackage.haskell.org/package/string-qq-0.0.2/src/LICENSE - goSimple (SPDX.ELicenseId lid) = SPDX.licenseIsOsiApproved lid -- allow only OSI approved licenses. + goSimple (SPDX.ELicenseId lid) = SPDX.licenseIsOsiApproved lid || SPDX.LId.licenseIsFsfLibre lid -- allow only OSI or FSF approved licenses. -- pre `cabal-version: 2.2` goLegacy License.AllRightsReserved = False From 2dadc2ac60a3d259e26282492ddd6ac29917afef Mon Sep 17 00:00:00 2001 From: Andreas Abel Date: Fri, 28 Oct 2022 07:59:30 +0200 Subject: [PATCH 21/43] Add dependabot for github workflows This will alert of outdated actions used in our workflows. Does not make much sense for the generated `haskell-ci.yml` workflow, as accepted action version bumps do not survive regeneration. Makes sense for the other workflow(s) though. --- .github/dependabot.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..c95d9e06d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +# From: +# - https://github.com/rhysd/actionlint/issues/228#issuecomment-1272493095 +# - https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot + +# Set update schedule for GitHub Actions + +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" From 4fdadd9e2bab253ffce4b359bc59c841a2f87a3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Oct 2022 06:01:50 +0000 Subject: [PATCH 22/43] Bump cachix/cachix-action from 10 to 12 Bumps [cachix/cachix-action](https://github.com/cachix/cachix-action) from 10 to 12. - [Release notes](https://github.com/cachix/cachix-action/releases) - [Commits](https://github.com/cachix/cachix-action/compare/v10...v12) --- updated-dependencies: - dependency-name: cachix/cachix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/nix-shell.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nix-shell.yml b/.github/workflows/nix-shell.yml index c6ae48ce3..057910717 100644 --- a/.github/workflows/nix-shell.yml +++ b/.github/workflows/nix-shell.yml @@ -19,7 +19,7 @@ jobs: extra_nix_config: | trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= hackage-server.cachix.org-1:iw0iRh6+gsFIrxROFaAt5gKNgIHejKjIfyRdbpPYevY= substituters = https://hydra.iohk.io https://cache.nixos.org/ https://hackage-server.cachix.org/ - - uses: cachix/cachix-action@v10 + - uses: cachix/cachix-action@v12 with: # https://nix.dev/tutorials/continuous-integration-github-actions#setting-up-github-actions name: hackage-server From 6533596b0ab6d577ac19f9dd6d5ac7bb278633ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Oct 2022 06:01:47 +0000 Subject: [PATCH 23/43] Bump actions/checkout from 2.4.0 to 3.1.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 2.4.0 to 3.1.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2.4.0...v3.1.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/haskell-ci.yml | 2 +- .github/workflows/nix-shell.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/haskell-ci.yml b/.github/workflows/haskell-ci.yml index 4f2631d62..83536c13c 100644 --- a/.github/workflows/haskell-ci.yml +++ b/.github/workflows/haskell-ci.yml @@ -158,7 +158,7 @@ jobs: chmod a+x $HOME/.cabal/bin/cabal-plan cabal-plan --version - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3.1.0 with: path: source - name: initial cabal.project for sdist diff --git a/.github/workflows/nix-shell.yml b/.github/workflows/nix-shell.yml index 057910717..7381ce232 100644 --- a/.github/workflows/nix-shell.yml +++ b/.github/workflows/nix-shell.yml @@ -12,7 +12,7 @@ jobs: nix-shell: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.4.0 + - uses: actions/checkout@v3.1.0 - uses: cachix/install-nix-action@v17 with: nix_path: nixpkgs=channel:nixos-21.11 From 2a3ad24f77a87e3e18e34cb3776b63353c25f526 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Oct 2022 08:40:23 +0000 Subject: [PATCH 24/43] Bump cachix/install-nix-action from 17 to 18 Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 17 to 18. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Commits](https://github.com/cachix/install-nix-action/compare/v17...v18) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/nix-shell.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nix-shell.yml b/.github/workflows/nix-shell.yml index 7381ce232..0e8376f9b 100644 --- a/.github/workflows/nix-shell.yml +++ b/.github/workflows/nix-shell.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3.1.0 - - uses: cachix/install-nix-action@v17 + - uses: cachix/install-nix-action@v18 with: nix_path: nixpkgs=channel:nixos-21.11 extra_nix_config: | From 06320860781a2dadb1ad6989eaac62af6ea60ec5 Mon Sep 17 00:00:00 2001 From: Andreas Abel Date: Sat, 29 Oct 2022 11:37:36 +0200 Subject: [PATCH 25/43] Build with Cabal-3.8 and GHC 9.4 (#1141) This commit makes `hackage-server` compile with Cabal-3.8, but does not add any Cabal-3.8 specific features. Adds `allow-older: Cabal:process` to solve a conflict arising from these two dependency chains: - Cabal-3.8.1.0 -> process >= 1.6.14 - Cabal-3.8.1.0 -> doctest-parallel -> ghc -> process For `ghc < 9.4`, this means `process < 1.6.14`, and the `ghc` package is not upgradeable, so the only solution is to override `Cabal-3.8.1.0`s request for this very recent version of `process`. The conflict is discussed in https://github.com/haskell/cabal/issues/8554. Likely, the next release of `Cabal-3.8` will drop the request for a specific `process` library and we can drop the `allow-older` workaround, and also reenable the tests on Haskell-CI for GHC < 9.4 (see `cabal.haskell-ci`). --- .github/workflows/haskell-ci.yml | 15 +++++++---- cabal.haskell-ci | 13 +++++++--- cabal.project | 11 ++++++++ hackage-server.cabal | 10 ++++---- .../Features/BuildReports/BuildReport.hs | 14 ++++++++++- .../Server/Framework/Instances.hs | 6 +++++ .../Server/Util/CabalRevisions.hs | 25 +++++++++++++++++-- 7 files changed, 77 insertions(+), 17 deletions(-) diff --git a/.github/workflows/haskell-ci.yml b/.github/workflows/haskell-ci.yml index 83536c13c..c9fb6ac69 100644 --- a/.github/workflows/haskell-ci.yml +++ b/.github/workflows/haskell-ci.yml @@ -8,9 +8,9 @@ # # For more information, see https://github.com/haskell-CI/haskell-ci # -# version: 0.15.20220822 +# version: 0.15.20221009 # -# REGENDATA ("0.15.20220822",["github","hackage-server.cabal"]) +# REGENDATA ("0.15.20221009",["github","hackage-server.cabal"]) # name: Haskell-CI on: @@ -34,6 +34,11 @@ jobs: strategy: matrix: include: + - compiler: ghc-9.4.2 + compilerKind: ghc + compilerVersion: 9.4.2 + setup-method: ghcup + allow-failure: false - compiler: ghc-9.2.4 compilerKind: ghc compilerVersion: 9.2.4 @@ -104,7 +109,7 @@ jobs: HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))') echo "HCNUMVER=$HCNUMVER" >> "$GITHUB_ENV" - echo "ARG_TESTS=--enable-tests" >> "$GITHUB_ENV" + if [ $((HCNUMVER >= 90400)) -ne 0 ] ; then echo "ARG_TESTS=--enable-tests" >> "$GITHUB_ENV" ; else echo "ARG_TESTS=--disable-tests" >> "$GITHUB_ENV" ; fi echo "ARG_BENCH=--enable-benchmarks" >> "$GITHUB_ENV" echo "HEADHACKAGE=false" >> "$GITHUB_ENV" echo "ARG_COMPILER=--$HCKIND --with-compiler=$HC" >> "$GITHUB_ENV" @@ -186,7 +191,7 @@ jobs: echo " ghc-options: -Werror=missing-methods" >> cabal.project cat >> cabal.project <> cabal.project.local + $HCPKG list --simple-output --names-only | perl -ne 'for (split /\s+/) { print "constraints: $_ installed\n" unless /^(Cabal|hackage-server|parsec|process|text)$/; }' >> cabal.project.local cat cabal.project cat cabal.project.local - name: dump install plan @@ -211,7 +216,7 @@ jobs: $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --write-ghc-environment-files=always - name: tests run: | - $CABAL v2-test $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --test-show-details=direct + if [ $((HCNUMVER >= 90400)) -ne 0 ] ; then $CABAL v2-test $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --test-show-details=direct ; fi - name: cabal check run: | cd ${PKGDIR_hackage_server} || false diff --git a/cabal.haskell-ci b/cabal.haskell-ci index e8ddbd02d..47b30a66e 100644 --- a/cabal.haskell-ci +++ b/cabal.haskell-ci @@ -1,10 +1,10 @@ branches: master ci* -installed: +all -Cabal -text -parsec +installed: +all -Cabal -text -parsec -process + -- Cabal-3.8.1.0 wants process-1.6.14 or newer --- -- irc-channels works with GHA, but why send to a channel --- -- when one can subscribe to github notifications? --- irc-channels: irc.libera.chat#hackage +-- Did not help to salvage ghc-9.2 and below: +-- installed: -all +ghc -- Does not work with GHA: -- -- allow failures with ghc-7.6 and ghc-7.8 @@ -19,3 +19,8 @@ apt: libbrotli-dev -- even though we don't define any library. haddock-components: all -- since haskell-ci 0.15.20220822 + +tests: >= 9.4 + -- parallel-doctest uses the ghc package + -- and thus does not build with Cabal-3.8.1.0 below GHC 9.4 + -- See: https://github.com/haskell/cabal/issues/8554 diff --git a/cabal.project b/cabal.project index c8dcbd200..82a1e8f21 100644 --- a/cabal.project +++ b/cabal.project @@ -11,6 +11,17 @@ packages: . allow-newer: rss:time, rss:base +-- Andreas, 2022-10-28: `Cabal-3.8.1.0` wants `process >= 1.6.14` +-- which is too new for the `ghc < 9.4` package that is pulled in +-- by `doctest-parallel`. +-- Since, Cabal-3.8.1.0 has no reason to want such a new version +-- of process, we can solve the conflict here by allowing +-- `Cabal` to use the shipped version of `process`. +-- This workaround can be removed once `Cabal-3.8` drops +-- its (unreasonable) constraint on `process`. +-- See: https://github.com/haskell/cabal/issues/8554 +allow-older: Cabal:process + ----------------------------------------------------------------------------- -- Anti-constraints diff --git a/hackage-server.cabal b/hackage-server.cabal index 74e511955..9120afead 100644 --- a/hackage-server.cabal +++ b/hackage-server.cabal @@ -27,7 +27,7 @@ copyright: 2008-2015 Duncan Coutts, license: BSD-3-Clause license-file: LICENSE -tested-with: GHC == { 9.2.4, 9.0.2, 8.10.7, 8.8.4 } +tested-with: GHC == { 9.4.2, 9.2.4, 9.0.2, 8.10.7, 8.8.4 } data-dir: datafiles data-files: @@ -99,7 +99,7 @@ common defaults -- see `cabal.project.local-ghc-${VERSION}` files build-depends: , array >= 0.5 && < 0.6 - , base >= 4.13 && < 4.17 + , base >= 4.13 && < 4.18 , binary >= 0.8 && < 0.9 , bytestring >= 0.10 && < 0.12 , containers ^>= 0.6.0 @@ -117,8 +117,8 @@ common defaults -- other dependencies shared by most components build-depends: , aeson ^>= 2.0.3.0 || ^>= 2.1.0.0 - , Cabal ^>= 3.6.3.0 - , Cabal-syntax ^>= 3.6.0.0 + , Cabal ^>= 3.8.1.0 + , Cabal-syntax ^>= 3.8.1.0 -- Cabal-syntax needs to be bound to constrain hackage-security -- see https://github.com/haskell/hackage-server/issues/1130 , fail ^>= 4.9.0 @@ -371,7 +371,7 @@ library lib-server -- NB: see also build-depends in `common defaults`! build-depends: , HStringTemplate ^>= 0.8 - , HTTP ^>= 4000.3.16 + , HTTP ^>= 4000.3.16 || ^>= 4000.4.1 , QuickCheck ^>= 2.14 , acid-state ^>= 0.16 , async ^>= 2.2.1 diff --git a/src/Distribution/Server/Features/BuildReports/BuildReport.hs b/src/Distribution/Server/Features/BuildReports/BuildReport.hs index 1d85cce5f..73d22b57d 100644 --- a/src/Distribution/Server/Features/BuildReports/BuildReport.hs +++ b/src/Distribution/Server/Features/BuildReports/BuildReport.hs @@ -1,9 +1,11 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} @@ -57,6 +59,10 @@ import Distribution.CabalSpecVersion ( CabalSpecVersion(CabalSpecV2_4) ) import Distribution.Pretty ( Pretty(..), pretty, prettyShow ) +#if MIN_VERSION_Cabal(3,7,0) +import Distribution.Fields.Pretty + ( pattern NoComment ) +#endif import qualified Text.PrettyPrint as Disp import Distribution.Parsec @@ -311,7 +317,13 @@ intPair = do -- Pretty-printing show :: BuildReport -> String -show = showFields (const []) . prettyFieldGrammar CabalSpecV2_4 fieldDescrs +show = showFields noComment . prettyFieldGrammar CabalSpecV2_4 fieldDescrs + where +#if MIN_VERSION_Cabal(3,7,0) + noComment _ = NoComment +#else + noComment _ = [] +#endif -- ----------------------------------------------------------------------------- -- Description of the fields, for parsing/printing diff --git a/src/Distribution/Server/Framework/Instances.hs b/src/Distribution/Server/Framework/Instances.hs index 442e9c15f..d2a68cf57 100644 --- a/src/Distribution/Server/Framework/Instances.hs +++ b/src/Distribution/Server/Framework/Instances.hs @@ -136,6 +136,7 @@ instance SafeCopy OS where putCopy Ghcjs = contain $ putWord8 14 putCopy Hurd = contain $ putWord8 15 putCopy Android = contain $ putWord8 16 + putCopy Wasi = contain $ putWord8 17 getCopy = contain $ do tag <- getWord8 @@ -157,6 +158,7 @@ instance SafeCopy OS where 14 -> return Ghcjs 15 -> return Hurd 16 -> return Android + 17 -> return Wasi _ -> fail "SafeCopy OS getCopy: unexpected tag" instance SafeCopy Arch where @@ -180,6 +182,8 @@ instance SafeCopy Arch where putCopy Vax = contain $ putWord8 15 putCopy JavaScript = contain $ putWord8 16 putCopy AArch64 = contain $ putWord8 17 + putCopy S390X = contain $ putWord8 18 + putCopy Wasm32 = contain $ putWord8 19 getCopy = contain $ do tag <- getWord8 @@ -202,6 +206,8 @@ instance SafeCopy Arch where 15 -> return Vax 16 -> return JavaScript 17 -> return AArch64 + 18 -> return S390X + 19 -> return Wasm32 _ -> fail "SafeCopy Arch getCopy: unexpected tag" instance SafeCopy CompilerFlavor where diff --git a/src/Distribution/Server/Util/CabalRevisions.hs b/src/Distribution/Server/Util/CabalRevisions.hs index f417dee1e..d984c4cc3 100644 --- a/src/Distribution/Server/Util/CabalRevisions.hs +++ b/src/Distribution/Server/Util/CabalRevisions.hs @@ -2,6 +2,7 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} @@ -39,6 +40,9 @@ import Distribution.Version import Distribution.Compiler (CompilerFlavor) import Distribution.FieldGrammar (prettyFieldGrammar) import Distribution.Fields.Pretty (PrettyField (..), showFields) +#if MIN_VERSION_Cabal(3,7,0) +import Distribution.Fields.Pretty (pattern NoComment) +#endif import Distribution.PackageDescription import Distribution.PackageDescription.Parsec (parseGenericPackageDescription, runParseResult) import Distribution.PackageDescription.FieldGrammar (sourceRepoFieldGrammar) @@ -340,7 +344,7 @@ checkPackageDescriptions checkXRevision checkSame "The package-url field is unused, don't bother changing it." pkgUrlA pkgUrlB changesOk "bug-reports" fromShortText bugReportsA bugReportsB - changesOkList changesOk "source-repository" (showFields (const []) . (:[]) . ppSourceRepo) + changesOkList changesOk "source-repository" (showFields noComment . (:[]) . ppSourceRepo) sourceReposA sourceReposB changesOk "synopsis" fromShortText synopsisA synopsisB changesOk "description" fromShortText descriptionA descriptionB @@ -365,6 +369,12 @@ checkPackageDescriptions checkXRevision when checkXRevision $ checkRevision customFieldsPDA customFieldsPDB checkCuration customFieldsPDA customFieldsPDB + where +#if MIN_VERSION_Cabal(3,7,0) + noComment _ = NoComment +#else + noComment _ = [] +#endif checkSpecVersionRaw :: Check PackageDescription checkSpecVersionRaw pdA pdB @@ -625,10 +635,20 @@ checkExecutable componentName checkTestSuite :: ComponentName -> Check TestSuite checkTestSuite componentName +#if MIN_VERSION_Cabal(3,7,0) + (TestSuite _nameA interfaceA buildInfoA testGeneratorsA) + (TestSuite _nameB interfaceB buildInfoB testGeneratorsB) +#else (TestSuite _nameA interfaceA buildInfoA) - (TestSuite _nameB interfaceB buildInfoB) = do + (TestSuite _nameB interfaceB buildInfoB) +#endif + = do checkSame "Cannot change test-suite type" interfaceA interfaceB checkBuildInfo componentName buildInfoA buildInfoB +#if MIN_VERSION_Cabal(3,7,0) + -- @test-generators@ + checkSame "Cannot change test-generators" testGeneratorsA testGeneratorsB +#endif checkBenchmark :: ComponentName -> Check Benchmark checkBenchmark componentName @@ -690,6 +710,7 @@ changesOkSet what render old new = do logChange (Change Normal ("removed " ++ what) (renderSet removed) "") unless (Set.null added) $ logChange (Change Normal ("added " ++ what) "" (renderSet added)) + return () where added = new Set.\\ old removed = old Set.\\ new From 5e893c911c60730c3be1d39c11997a1864f37030 Mon Sep 17 00:00:00 2001 From: Andreas Abel Date: Tue, 27 Dec 2022 13:52:49 +0100 Subject: [PATCH 26/43] Haskell CI: bump to Ubuntu-22.04, GHC 9.2.5 and 9.4.4 --- .github/workflows/haskell-ci.yml | 63 +++++++++++--------------------- cabal.haskell-ci | 4 +- hackage-server.cabal | 4 +- 3 files changed, 26 insertions(+), 45 deletions(-) diff --git a/.github/workflows/haskell-ci.yml b/.github/workflows/haskell-ci.yml index c9fb6ac69..00605139b 100644 --- a/.github/workflows/haskell-ci.yml +++ b/.github/workflows/haskell-ci.yml @@ -8,9 +8,9 @@ # # For more information, see https://github.com/haskell-CI/haskell-ci # -# version: 0.15.20221009 +# version: 0.15.20221225 # -# REGENDATA ("0.15.20221009",["github","hackage-server.cabal"]) +# REGENDATA ("0.15.20221225",["github","hackage-server.cabal"]) # name: Haskell-CI on: @@ -29,19 +29,19 @@ jobs: timeout-minutes: 60 container: - image: buildpack-deps:focal + image: buildpack-deps:jammy continue-on-error: ${{ matrix.allow-failure }} strategy: matrix: include: - - compiler: ghc-9.4.2 + - compiler: ghc-9.4.4 compilerKind: ghc - compilerVersion: 9.4.2 + compilerVersion: 9.4.4 setup-method: ghcup allow-failure: false - - compiler: ghc-9.2.4 + - compiler: ghc-9.2.5 compilerKind: ghc - compilerVersion: 9.2.4 + compilerVersion: 9.2.5 setup-method: ghcup allow-failure: false - compiler: ghc-9.0.2 @@ -57,7 +57,7 @@ jobs: - compiler: ghc-8.8.4 compilerKind: ghc compilerVersion: 8.8.4 - setup-method: hvr-ppa + setup-method: ghcup allow-failure: false fail-fast: false steps: @@ -65,23 +65,13 @@ jobs: run: | apt-get update apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common libtinfo5 - if [ "${{ matrix.setup-method }}" = ghcup ]; then - mkdir -p "$HOME/.ghcup/bin" - curl -sL https://downloads.haskell.org/ghcup/0.1.18.0/x86_64-linux-ghcup-0.1.18.0 > "$HOME/.ghcup/bin/ghcup" - chmod a+x "$HOME/.ghcup/bin/ghcup" - "$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" || (cat "$HOME"/.ghcup/logs/*.* && false) - "$HOME/.ghcup/bin/ghcup" install cabal 3.6.2.0 || (cat "$HOME"/.ghcup/logs/*.* && false) - apt-get update - apt-get install -y libbrotli-dev - else - apt-add-repository -y 'ppa:hvr/ghc' - apt-get update - apt-get install -y "$HCNAME" libbrotli-dev - mkdir -p "$HOME/.ghcup/bin" - curl -sL https://downloads.haskell.org/ghcup/0.1.18.0/x86_64-linux-ghcup-0.1.18.0 > "$HOME/.ghcup/bin/ghcup" - chmod a+x "$HOME/.ghcup/bin/ghcup" - "$HOME/.ghcup/bin/ghcup" install cabal 3.6.2.0 || (cat "$HOME"/.ghcup/logs/*.* && false) - fi + mkdir -p "$HOME/.ghcup/bin" + curl -sL https://downloads.haskell.org/ghcup/0.1.18.0/x86_64-linux-ghcup-0.1.18.0 > "$HOME/.ghcup/bin/ghcup" + chmod a+x "$HOME/.ghcup/bin/ghcup" + "$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" || (cat "$HOME"/.ghcup/logs/*.* && false) + "$HOME/.ghcup/bin/ghcup" install cabal 3.6.2.0 || (cat "$HOME"/.ghcup/logs/*.* && false) + apt-get update + apt-get install -y libbrotli-dev env: HCKIND: ${{ matrix.compilerKind }} HCNAME: ${{ matrix.compiler }} @@ -93,20 +83,11 @@ jobs: echo "CABAL_DIR=$HOME/.cabal" >> "$GITHUB_ENV" echo "CABAL_CONFIG=$HOME/.cabal/config" >> "$GITHUB_ENV" HCDIR=/opt/$HCKIND/$HCVER - if [ "${{ matrix.setup-method }}" = ghcup ]; then - HC=$HOME/.ghcup/bin/$HCKIND-$HCVER - echo "HC=$HC" >> "$GITHUB_ENV" - echo "HCPKG=$HOME/.ghcup/bin/$HCKIND-pkg-$HCVER" >> "$GITHUB_ENV" - echo "HADDOCK=$HOME/.ghcup/bin/haddock-$HCVER" >> "$GITHUB_ENV" - echo "CABAL=$HOME/.ghcup/bin/cabal-3.6.2.0 -vnormal+nowrap" >> "$GITHUB_ENV" - else - HC=$HCDIR/bin/$HCKIND - echo "HC=$HC" >> "$GITHUB_ENV" - echo "HCPKG=$HCDIR/bin/$HCKIND-pkg" >> "$GITHUB_ENV" - echo "HADDOCK=$HCDIR/bin/haddock" >> "$GITHUB_ENV" - echo "CABAL=$HOME/.ghcup/bin/cabal-3.6.2.0 -vnormal+nowrap" >> "$GITHUB_ENV" - fi - + HC=$HOME/.ghcup/bin/$HCKIND-$HCVER + echo "HC=$HC" >> "$GITHUB_ENV" + echo "HCPKG=$HOME/.ghcup/bin/$HCKIND-pkg-$HCVER" >> "$GITHUB_ENV" + echo "HADDOCK=$HOME/.ghcup/bin/haddock-$HCVER" >> "$GITHUB_ENV" + echo "CABAL=$HOME/.ghcup/bin/cabal-3.6.2.0 -vnormal+nowrap" >> "$GITHUB_ENV" HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))') echo "HCNUMVER=$HCNUMVER" >> "$GITHUB_ENV" if [ $((HCNUMVER >= 90400)) -ne 0 ] ; then echo "ARG_TESTS=--enable-tests" >> "$GITHUB_ENV" ; else echo "ARG_TESTS=--disable-tests" >> "$GITHUB_ENV" ; fi @@ -163,7 +144,7 @@ jobs: chmod a+x $HOME/.cabal/bin/cabal-plan cabal-plan --version - name: checkout - uses: actions/checkout@v3.1.0 + uses: actions/checkout@v3 with: path: source - name: initial cabal.project for sdist @@ -199,7 +180,7 @@ jobs: $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dry-run all cabal-plan - name: cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }} path: ~/.cabal/store diff --git a/cabal.haskell-ci b/cabal.haskell-ci index 47b30a66e..5e5272c5d 100644 --- a/cabal.haskell-ci +++ b/cabal.haskell-ci @@ -10,8 +10,8 @@ installed: +all -Cabal -text -parsec -process -- -- allow failures with ghc-7.6 and ghc-7.8 -- allow-failures: <7.9 --- Use Ubuntu 20.04 -distribution: focal +-- Use Ubuntu 22.04 +distribution: jammy apt: libbrotli-dev diff --git a/hackage-server.cabal b/hackage-server.cabal index 9120afead..63b33415c 100644 --- a/hackage-server.cabal +++ b/hackage-server.cabal @@ -27,7 +27,7 @@ copyright: 2008-2015 Duncan Coutts, license: BSD-3-Clause license-file: LICENSE -tested-with: GHC == { 9.4.2, 9.2.4, 9.0.2, 8.10.7, 8.8.4 } +tested-with: GHC == { 9.4.4, 9.2.5, 9.0.2, 8.10.7, 8.8.4 } data-dir: datafiles data-files: @@ -123,7 +123,7 @@ common defaults -- see https://github.com/haskell/hackage-server/issues/1130 , fail ^>= 4.9.0 -- we use Control.Monad.Except, introduced in mtl-2.2.1 - , network >= 3 && < 3.2 + , network >= 3 && < 3.2 , network-bsd ^>= 2.8 , network-uri ^>= 2.6 , parsec ^>= 3.1.13 From c2dd35c9c33ef50ecd213f3a6ffd3d9beddcc88f Mon Sep 17 00:00:00 2001 From: Andreas Abel Date: Fri, 30 Dec 2022 15:38:19 +0100 Subject: [PATCH 27/43] Allow mtl-2.3 and transformers-0.6 (#1150) Allow `mtl-2.3` and `transformers-0.6`. Some import statements have to be changed to accommodate the breaking changes of `mtl >= 2.3`. In case of `liftM`, I opted for the more modern `<$>`. We also contribute a new CI workflow that tests building with `mtl >= 2.3.1` so that `mtl-2.3` compatibility does not bit-rot. --- .github/workflows/cabal-mtl-2.3.yml | 63 +++++++++++++++++++ hackage-server.cabal | 4 +- src/Distribution/Server/Features/Browse.hs | 3 +- .../Server/Features/Core/Backup.hs | 2 +- .../Server/Features/Security/Backup.hs | 4 +- .../Server/Features/Security/State.hs | 3 +- .../Server/Framework/BackupRestore.hs | 4 +- src/Distribution/Server/Util/Markdown.hs | 2 +- 8 files changed, 75 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/cabal-mtl-2.3.yml diff --git a/.github/workflows/cabal-mtl-2.3.yml b/.github/workflows/cabal-mtl-2.3.yml new file mode 100644 index 000000000..d212ed339 --- /dev/null +++ b/.github/workflows/cabal-mtl-2.3.yml @@ -0,0 +1,63 @@ +name: Cabal build with mtl-2.3 +on: + push: + branches: + - master + - ci* + pull_request: + branches: + - master + - ci* + +defaults: + run: + shell: bash + +jobs: + build: + name: Build with mtl-2.3 + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + + - name: Environment settings based on the Haskell setup + run: | + GHC_VER=$(ghc --numeric-version) + CABAL_VER=$(cabal --numeric-version) + echo "GHC_VER = ${GHC_VER}" + echo "CABAL_VER = ${CABAL_VER}" + echo "GHC_VER=${GHC_VER}" >> "${GITHUB_ENV}" + echo "CABAL_VER=${CABAL_VER}" >> "${GITHUB_ENV}" + + # Brotli is already installed on ubuntu-22.04 + # - name: Install the brotli library + # run: | + # sudo apt-get update + # sudo apt-get install -y libbrotli-dev + + - uses: actions/checkout@v3 + + - name: Cache build + uses: actions/cache@v3 + with: + path: | + ~/.cabal + dist-newstyle + key: cabal-${{ env.CABAL_VER }}-ghc-${{ env.GHC_VER }}-commit-${{ github.sha }} + restore-keys: | + cabal-${{ env.CABAL_VER }}-ghc-${{ env.GHC_VER }}-commit- + + - name: Prepare cabal + run: | + cabal update + + - name: Build dependencies w/o tests with mtl-2.3 + # 2022-12-30: 'transformers >= 0.6' is needed because of happstack-server + run: | + cabal build --dependencies-only -O0 --disable-tests --constraint 'mtl >= 2.3.1' --constraint 'transformers >= 0.6' --allow-newer='Cabal:mtl' --allow-newer='Cabal:transformers' + + - name: Build w/o tests with mtl-2.3 + # 2022-12-30: 'transformers >= 0.6' is needed because of happstack-server + run: | + cabal build -O0 --disable-tests --constraint 'mtl >= 2.3.1' --constraint 'transformers >= 0.6' --allow-newer='Cabal:mtl' --allow-newer='Cabal:transformers' diff --git a/hackage-server.cabal b/hackage-server.cabal index 63b33415c..4836c0978 100644 --- a/hackage-server.cabal +++ b/hackage-server.cabal @@ -106,12 +106,12 @@ common defaults , deepseq >= 1.4 && < 1.5 , directory >= 1.3 && < 1.4 , filepath >= 1.4 && < 1.5 - , mtl ^>= 2.2.1 + , mtl >= 2.2.1 && < 2.4 , pretty >= 1.1 && < 1.2 , process >= 1.6 && < 1.7 , text ^>= 1.2.5.0 || ^>= 2.0 , time >= 1.9 && < 1.13 - , transformers >= 0.5 && < 0.6 + , transformers >= 0.5 && < 0.7 , unix >= 2.7 && < 2.8 , scientific -- other dependencies shared by most components diff --git a/src/Distribution/Server/Features/Browse.hs b/src/Distribution/Server/Features/Browse.hs index ada4b622c..7cb32013c 100644 --- a/src/Distribution/Server/Features/Browse.hs +++ b/src/Distribution/Server/Features/Browse.hs @@ -1,7 +1,8 @@ {-# LANGUAGE BlockArguments, NamedFieldPuns #-} module Distribution.Server.Features.Browse (initBrowseFeature, PaginationConfig(..), StartIndex(..), NumElems(..), paginate) where -import Control.Monad.Except (ExceptT, liftIO, throwError) +import Control.Monad.Except (ExceptT, throwError) +import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Class (lift) import qualified Data.Map as Map import Data.Maybe (isJust) diff --git a/src/Distribution/Server/Features/Core/Backup.hs b/src/Distribution/Server/Features/Core/Backup.hs index c46f56da7..bc620a3d9 100644 --- a/src/Distribution/Server/Features/Core/Backup.hs +++ b/src/Distribution/Server/Features/Core/Backup.hs @@ -40,7 +40,7 @@ import qualified Data.Foldable as Foldable import Data.List import Data.List.NonEmpty (toList) import Data.Ord (comparing) -import Control.Monad.State +import Control.Monad import qualified Distribution.Server.Util.GZip as GZip import qualified Data.ByteString.Lazy as BS import qualified Data.ByteString.Lazy.Char8 as BSC diff --git a/src/Distribution/Server/Features/Security/Backup.hs b/src/Distribution/Server/Features/Security/Backup.hs index 5911d488f..a0afe21b1 100644 --- a/src/Distribution/Server/Features/Security/Backup.hs +++ b/src/Distribution/Server/Features/Security/Backup.hs @@ -6,7 +6,7 @@ module Distribution.Server.Features.Security.Backup ( ) where -- stdlib -import Control.Monad.State +import Control.Monad.State (StateT, execStateT, modify) import Data.Time import Data.Version (Version(..), showVersion) import Text.CSV hiding (csv) @@ -224,7 +224,7 @@ import_v1 = mapM_ fromRecord fromInfoRecord [strFileLength, strSHA256, strMD5] = do fileInfoLength <- parseRead "file length" strFileLength fileInfoSHA256 <- parseSHA "file SHA256" strSHA256 - fileInfoMD5 <- Just `liftM` parseMD5 "file MD5" strMD5 + fileInfoMD5 <- Just <$> parseMD5 "file MD5" strMD5 return FileInfo{..} fromInfoRecord otherRecord = fail $ "Unexpected info record: " ++ show otherRecord diff --git a/src/Distribution/Server/Features/Security/State.hs b/src/Distribution/Server/Features/Security/State.hs index a56b417b5..dad42fcd8 100644 --- a/src/Distribution/Server/Features/Security/State.hs +++ b/src/Distribution/Server/Features/Security/State.hs @@ -7,7 +7,8 @@ module Distribution.Server.Features.Security.State where -- stdlib -import Control.Monad.Reader +import Control.Monad +import Control.Monad.Reader (ask, asks) import Data.Acid import Data.Maybe import Data.SafeCopy diff --git a/src/Distribution/Server/Framework/BackupRestore.hs b/src/Distribution/Server/Framework/BackupRestore.hs index d2158f74f..48edeab17 100644 --- a/src/Distribution/Server/Framework/BackupRestore.hs +++ b/src/Distribution/Server/Framework/BackupRestore.hs @@ -51,8 +51,8 @@ import Distribution.Server.Features.Security.SHA256 import qualified Codec.Archive.Tar as Tar import qualified Codec.Archive.Tar.Entry as Tar import Distribution.Server.Util.GZip (decompressNamed) -import Control.Monad.State -import Control.Monad.Except +import Control.Monad.State (StateT, evalStateT, MonadState, get, gets, put) +import Control.Monad.Except (ExceptT, runExceptT, MonadError(..)) import Data.Time (UTCTime) import qualified Data.Time as Time import Data.Time.Format (defaultTimeLocale) diff --git a/src/Distribution/Server/Util/Markdown.hs b/src/Distribution/Server/Util/Markdown.hs index 033ab4f00..881298f48 100644 --- a/src/Distribution/Server/Util/Markdown.hs +++ b/src/Distribution/Server/Util/Markdown.hs @@ -22,7 +22,7 @@ import qualified Data.Text.Encoding.Error as T (lenientDecode) import qualified Data.Text.Lazy as TL import Data.Typeable (Typeable) import Network.URI (isRelativeReference) -import Control.Monad.Identity +import Control.Monad.Identity (runIdentity) import Text.HTML.SanitizeXSS as XSS import System.FilePath.Posix (takeExtension) import qualified Data.ByteString.Lazy as BS (ByteString, toStrict) From 4af279eb01166de7d9a282913f90524b23f3c2a7 Mon Sep 17 00:00:00 2001 From: Alias Qli <2576814881@qq.com> Date: Sat, 31 Dec 2022 05:35:49 +0800 Subject: [PATCH 28/43] Disable test (#1124) * allow disable tests on client side --- datafiles/templates/Html/maintain.html.st | 5 + datafiles/templates/Html/reports-test.html.st | 25 +++++ exes/BuildClient.hs | 31 +++--- .../Server/Features/BuildReports.hs | 35 ++++++- .../Features/BuildReports/BuildReport.hs | 7 +- .../Features/BuildReports/BuildReports.hs | 96 +++++++++++++++---- .../Server/Features/BuildReports/State.hs | 13 ++- src/Distribution/Server/Features/Html.hs | 25 ++++- 8 files changed, 195 insertions(+), 42 deletions(-) create mode 100644 datafiles/templates/Html/reports-test.html.st diff --git a/datafiles/templates/Html/maintain.html.st b/datafiles/templates/Html/maintain.html.st index 5b7500b00..2a39e0cdc 100644 --- a/datafiles/templates/Html/maintain.html.st +++ b/datafiles/templates/Html/maintain.html.st @@ -46,6 +46,11 @@ package after its been released.

$versions:{pkgid|$pkgid$}; separator=", "$

+
Test settings
+
If your package contains tests that can't run on hackage, you can disable them here. +

$versions:{pkgid|$pkgid$}; separator=", "$

+
+
Trigger rebuild
Reset the fail count and trigger rebuild. Choose this option only if you believe our build process didn't go right for some reason. Reseting fail count won't trigger rebuild if your package has documentation.

$versions:{pkgid|$pkgid$}; separator=", "$

diff --git a/datafiles/templates/Html/reports-test.html.st b/datafiles/templates/Html/reports-test.html.st new file mode 100644 index 000000000..72a4eaee1 --- /dev/null +++ b/datafiles/templates/Html/reports-test.html.st @@ -0,0 +1,25 @@ + + + +$hackageCssTheme()$ +Test settings + + +$hackagePageHeader()$ + +
+

Test settings for $pkgid$

+ +
+ +
+
Run tests
+
+ Whether hackage should run the tests. +
+ +

+

+ +
+ diff --git a/exes/BuildClient.hs b/exes/BuildClient.hs index 9507d6f95..bef32a18e 100644 --- a/exes/BuildClient.hs +++ b/exes/BuildClient.hs @@ -362,6 +362,7 @@ data DocInfo = DocInfo { docInfoPackage :: PackageIdentifier , docInfoHasDocs :: HasDocs , docInfoIsCandidate :: Bool + , docInfoRunTests :: Bool } docInfoPackageName :: DocInfo -> PackageName @@ -410,8 +411,8 @@ getDocumentationStats verbosity opts config pkgs = do (Just (perrs, packages), Just (cerrs, candidates)) -> do liftIO . when (not . null $ perrs) . putStrLn $ "failed package json parses: " ++ show perrs liftIO . when (not . null $ cerrs) . putStrLn $ "failed candidate json parses: " ++ show cerrs - packages' <- liftIO $ mapM checkFailed packages - candidates' <- liftIO $ mapM checkFailed candidates + let packages' = map checkFailed packages + candidates' = map checkFailed candidates return $ map (setIsCandidate False) packages' ++ map (setIsCandidate True) candidates' where @@ -447,21 +448,23 @@ getDocumentationStats verbosity opts config pkgs = do addEnd (Just pkgs') Nothing uri = uri "docs.json" ++ "?pkgs=" ++ (getQry pkgs') addEnd Nothing Nothing uri = uri "docs.json" - checkFailed :: BR.PkgDetails -> IO (PackageIdentifier, HasDocs) - checkFailed pkgDetails = do + checkFailed :: BR.PkgDetails -> (PackageIdentifier, HasDocs, Bool) + checkFailed pkgDetails = let pkgId = BR.pkid pkgDetails - case (BR.docs pkgDetails, BR.failCnt pkgDetails) of - (True , _) -> return (pkgId, HasDocs) - (False, Just BR.BuildOK) -> return (pkgId, DocsFailed) - (False, Just (BR.BuildFailCnt a)) - | a >= bo_buildAttempts opts -> return (pkgId, DocsFailed) - (False, _) -> return (pkgId, DocsNotBuilt) - - setIsCandidate :: Bool -> (PackageIdentifier, HasDocs) -> DocInfo - setIsCandidate isCandidate (pId, hasDocs) = DocInfo { + hasDocs = case (BR.docs pkgDetails, BR.failCnt pkgDetails) of + (True , _) -> HasDocs + (False, Just BR.BuildOK) -> DocsFailed + (False, Just (BR.BuildFailCnt a)) + | a >= bo_buildAttempts opts -> DocsFailed + (False, _) -> DocsNotBuilt + in (pkgId, hasDocs, fromMaybe True $ BR.runTests pkgDetails) + + setIsCandidate :: Bool -> (PackageIdentifier, HasDocs, Bool) -> DocInfo + setIsCandidate isCandidate (pId, hasDocs, runTests) = DocInfo { docInfoPackage = pId , docInfoHasDocs = hasDocs , docInfoIsCandidate = isCandidate + , docInfoRunTests = runTests } @@ -573,7 +576,7 @@ processPkg verbosity opts config docInfo = do let installOk = fmap ("install-outcome: InstallOk" `isInfixOf`) buildReport == Just True -- Run Tests if installOk, Run coverage is Tests runs - (testOutcome, hpcLoc) <- case installOk of + (testOutcome, hpcLoc) <- case installOk && docInfoRunTests docInfo of True -> testPackage verbosity opts docInfo False -> return (Nothing, Nothing) coverageFile <- mapM (coveragePackage verbosity opts docInfo) hpcLoc diff --git a/src/Distribution/Server/Features/BuildReports.hs b/src/Distribution/Server/Features/BuildReports.hs index 73985286c..c443bbab2 100644 --- a/src/Distribution/Server/Features/BuildReports.hs +++ b/src/Distribution/Server/Features/BuildReports.hs @@ -32,6 +32,7 @@ import Data.ByteString.Lazy (toStrict) import Data.String (fromString) import Data.Maybe import Distribution.Compiler ( CompilerId(..) ) +import Data.Aeson (toJSON) -- TODO: @@ -47,6 +48,7 @@ data ReportsFeature = ReportsFeature { queryBuildLog :: forall m. MonadIO m => BuildLog -> m Resource.BuildLog, pkgReportDetails :: forall m. MonadIO m => (PackageIdentifier, Bool) -> m BuildReport.PkgDetails, queryLastReportStats:: forall m. MonadIO m => PackageIdentifier -> m (Maybe (BuildReportId, BuildReport, Maybe BuildCovg)), + queryRunTests :: forall m. MonadIO m => PackageId -> m Bool, reportsResource :: ReportsResource } @@ -59,6 +61,7 @@ data ReportsResource = ReportsResource { reportsPage :: Resource, reportsLog :: Resource, reportsReset:: Resource, + reportsTest :: Resource, reportsListUri :: String -> PackageId -> String, reportsPageUri :: String -> PackageId -> BuildReportId -> String, reportsLogUri :: PackageId -> BuildReportId -> String @@ -119,6 +122,7 @@ buildReportsFeature name , reportsPage , reportsLog , reportsReset + , reportsTest ] , featureState = [abstractAcidStateComponent reportsState] } @@ -140,6 +144,13 @@ buildReportsFeature name ] , resourceGet = [ ("", resetBuildFails) ] } + , reportsTest = (extendResourcePath "/reports/test/" corePackagePage) { + resourceDesc = [ (GET, "Get reports test settings") + , (POST, "Set reports test settings") + ] + , resourceGet = [ ("json", getReportsTest) ] + , resourcePost = [ ("", postReportsTest) ] + } , reportsPage = (extendResourcePath "/reports/:id.:format" corePackagePage) { resourceDesc = [ (GET, "Get a specific build report") , (DELETE, "Delete a specific build report") @@ -201,12 +212,13 @@ buildReportsFeature name pkgReportDetails (pkgid, docs) = do failCnt <- queryState reportsState $ LookupFailCount pkgid latestRpt <- queryState reportsState $ LookupLatestReport pkgid + runTests <- fmap Just . queryState reportsState $ LookupRunTests pkgid (time, ghcId) <- case latestRpt of Nothing -> return (Nothing,Nothing) Just (_, brp, _, _) -> do let (CompilerId _ vrsn) = compiler brp return (time brp, Just vrsn) - return (BuildReport.PkgDetails pkgid docs failCnt time ghcId) + return (BuildReport.PkgDetails pkgid docs failCnt time ghcId runTests) queryLastReportStats :: MonadIO m => PackageIdentifier -> m (Maybe (BuildReportId, BuildReport, Maybe BuildCovg)) queryLastReportStats pkgid = do @@ -215,6 +227,8 @@ buildReportsFeature name Nothing -> return Nothing Just (rptId, rpt, _, covg) -> return (Just (rptId, rpt, covg)) + queryRunTests :: MonadIO m => PackageId -> m Bool + queryRunTests pkgid = queryState reportsState $ LookupRunTests pkgid --------------------------------------------------------------------------- @@ -318,6 +332,25 @@ buildReportsFeature name then seeOther (reportsListUri reportsResource "" pkgid) $ toResponse () else errNotFound "Report not found" [MText "Build report does not exist"] + getReportsTest :: DynamicPath -> ServerPartE Response + getReportsTest dpath = do + pkgid <- packageInPath dpath + guardValidPackageId pkgid + guardAuthorisedAsMaintainerOrTrustee (packageName pkgid) + runTest <- queryRunTests pkgid + pure $ toResponse $ toJSON runTest + + postReportsTest :: DynamicPath -> ServerPartE Response + postReportsTest dpath = do + pkgid <- packageInPath dpath + runTests <- body $ looks "runTests" + guardValidPackageId pkgid + guardAuthorisedAsMaintainerOrTrustee (packageName pkgid) + success <- updateState reportsState $ SetRunTests pkgid ("on" `elem` runTests) + if success + then seeOther (reportsListUri reportsResource "" pkgid) $ toResponse () + else errNotFound "Package not found" [MText "Package does not exist"] + putAllReports :: DynamicPath -> ServerPartE Response putAllReports dpath = do diff --git a/src/Distribution/Server/Features/BuildReports/BuildReport.hs b/src/Distribution/Server/Features/BuildReports/BuildReport.hs index 73d22b57d..b6959eff2 100644 --- a/src/Distribution/Server/Features/BuildReports/BuildReport.hs +++ b/src/Distribution/Server/Features/BuildReports/BuildReport.hs @@ -635,7 +635,8 @@ data PkgDetails = PkgDetails { docs :: Bool, failCnt :: Maybe BuildStatus, buildTime :: Maybe UTCTime, - ghcId :: Maybe Version + ghcId :: Maybe Version, + runTests :: Maybe Bool } deriving(Show) instance Data.Aeson.ToJSON PkgDetails where @@ -644,7 +645,8 @@ instance Data.Aeson.ToJSON PkgDetails where "docs" .= docs p, "failCnt" .= failCnt p, "buildTime" .= buildTime p, - "ghcId" .= k (ghcId p) ] + "ghcId" .= k (ghcId p), + "runTests" .= runTests p ] where k (Just a) = Just $ DT.display a k Nothing = Nothing @@ -657,6 +659,7 @@ instance Data.Aeson.FromJSON PkgDetails where <*> o .:? "failCnt" <*> o .:? "buildTime" <*> fmap parseVersion (o .:? "ghcId") + <*> o .: "runTests" where parseVersion :: Maybe String -> Maybe Version parseVersion Nothing = Nothing diff --git a/src/Distribution/Server/Features/BuildReports/BuildReports.hs b/src/Distribution/Server/Features/BuildReports/BuildReports.hs index ae6d724ab..611f7d23e 100644 --- a/src/Distribution/Server/Features/BuildReports/BuildReports.hs +++ b/src/Distribution/Server/Features/BuildReports/BuildReports.hs @@ -22,7 +22,9 @@ module Distribution.Server.Features.BuildReports.BuildReports ( setFailStatus, resetFailCount, lookupLatestReport, - lookupFailCount + lookupFailCount, + lookupRunTests, + setRunTests ) where import qualified Distribution.Server.Framework.BlobStorage as BlobStorage @@ -48,6 +50,7 @@ import Data.SafeCopy import Data.Typeable (Typeable) import qualified Data.List as L import qualified Data.Char as Char +import Data.Maybe (fromMaybe) import Text.StringTemplate (ToSElem(..)) @@ -86,18 +89,21 @@ data PkgBuildReports = PkgBuildReports { reports :: !(Map BuildReportId (BuildReport, Maybe BuildLog, Maybe BuildCovg )), -- one more than the maximum report id used nextReportId :: !BuildReportId, - buildStatus :: !BuildStatus + buildStatus :: !BuildStatus, + runTests :: !Bool } deriving (Eq, Typeable, Show) data BuildReports = BuildReports { reportsIndex :: !(Map.Map PackageId PkgBuildReports) + } deriving (Eq, Typeable, Show) emptyPkgReports :: PkgBuildReports emptyPkgReports = PkgBuildReports { reports = Map.empty, nextReportId = BuildReportId 1, - buildStatus = BuildFailCnt 0 + buildStatus = BuildFailCnt 0, + runTests = True } emptyReports :: BuildReports @@ -126,7 +132,8 @@ addReport pkgid (brpt,blog) buildReports = reportId = nextReportId pkgReports pkgReports' = PkgBuildReports { reports = Map.insert reportId (brpt,blog,Nothing) (reports pkgReports) , nextReportId = incrementReportId reportId - , buildStatus = buildStatus pkgReports } + , buildStatus = buildStatus pkgReports + , runTests = runTests pkgReports } in (buildReports { reportsIndex = Map.insert pkgid pkgReports' (reportsIndex buildReports) }, reportId) unsafeSetReport :: PackageId -> BuildReportId -> (BuildReport, Maybe BuildLog) -> BuildReports -> BuildReports @@ -134,7 +141,8 @@ unsafeSetReport pkgid reportId (brpt,blog) buildReports = let pkgReports = Map.findWithDefault emptyPkgReports pkgid (reportsIndex buildReports) pkgReports' = PkgBuildReports { reports = Map.insert reportId (brpt,blog,Nothing) (reports pkgReports) , nextReportId = max (incrementReportId reportId) (nextReportId pkgReports) - , buildStatus = buildStatus pkgReports } + , buildStatus = buildStatus pkgReports + , runTests = runTests pkgReports } in buildReports { reportsIndex = Map.insert pkgid pkgReports' (reportsIndex buildReports) } deleteReport :: PackageId -> BuildReportId -> BuildReports -> Maybe BuildReports @@ -159,7 +167,8 @@ addRptLogCovg pkgid report buildReports = reportId = nextReportId pkgReports pkgReports' = PkgBuildReports { reports = Map.insert reportId report (reports pkgReports) , nextReportId = incrementReportId reportId - , buildStatus = buildStatus pkgReports } + , buildStatus = buildStatus pkgReports + , runTests = runTests pkgReports } in (buildReports { reportsIndex = Map.insert pkgid pkgReports' (reportsIndex buildReports) }, reportId) lookupReportCovg :: PackageId -> BuildReportId -> BuildReports -> Maybe (BuildReport, Maybe BuildLog, Maybe BuildCovg ) @@ -170,7 +179,8 @@ setFailStatus pkgid fStatus buildReports = let pkgReports = Map.findWithDefault emptyPkgReports pkgid (reportsIndex buildReports) pkgReports' = PkgBuildReports { reports = (reports pkgReports) , nextReportId = (nextReportId pkgReports) - , buildStatus = (getfst fStatus (buildStatus pkgReports)) } + , buildStatus = (getfst fStatus (buildStatus pkgReports)) + , runTests = runTests pkgReports } in buildReports { reportsIndex = Map.insert pkgid pkgReports' (reportsIndex buildReports) } where getfst nfst cfst = do @@ -185,7 +195,8 @@ resetFailCount pkgid buildReports = case Map.lookup pkgid (reportsIndex buildRep Just pkgReports -> do let pkgReports' = PkgBuildReports { reports = (reports pkgReports) , nextReportId = (nextReportId pkgReports) - , buildStatus = BuildFailCnt 0 } + , buildStatus = BuildFailCnt 0 + , runTests = runTests pkgReports } return buildReports { reportsIndex = Map.insert pkgid pkgReports' (reportsIndex buildReports) } lookupFailCount :: PackageId -> BuildReports -> Maybe BuildStatus @@ -203,6 +214,14 @@ lookupLatestReport pkgid buildReports = do else Just $ Map.findMax rs Just (maxKey, rep, buildLog, covg) +lookupRunTests :: PackageId -> BuildReports -> Bool +lookupRunTests pkgid buildReports = maybe True runTests $ Map.lookup pkgid (reportsIndex buildReports) + +setRunTests :: PackageId -> Bool -> BuildReports -> Maybe BuildReports +setRunTests pkgid b buildReports = + let rp = fromMaybe emptyPkgReports $ Map.lookup pkgid (reportsIndex buildReports) + in Just $ BuildReports (Map.insert pkgid rp{runTests = b} (reportsIndex buildReports)) + -- addPkg::` ------------------- -- HStringTemplate instances @@ -247,20 +266,41 @@ deriveSafeCopy 2 'extension ''BuildLog -- however, upon importing, nextReportId will = 3, one more than the maximum present -- this is also a problem in ReportsBackup.hs. but it's not a major issue I think. instance SafeCopy PkgBuildReports where - version = 3 + version = 4 kind = extension - putCopy (PkgBuildReports x _ y) = contain $ safePut (x,y) + putCopy (PkgBuildReports x _ y z) = contain $ safePut (x,y,z) getCopy = contain $ mkReports <$> safeGet where - mkReports (rs,f) = PkgBuildReports rs + mkReports (rs,f,b) = PkgBuildReports rs (if Map.null rs then BuildReportId 1 else incrementReportId (fst $ Map.findMax rs)) - f + f b instance MemSize PkgBuildReports where - memSize (PkgBuildReports a b c) = memSize3 a b c + memSize (PkgBuildReports a b c d) = memSize4 a b c d + + +data PkgBuildReports_v3 = PkgBuildReports_v3 { + reports_v3 :: !(Map BuildReportId (BuildReport, Maybe BuildLog, Maybe BuildCovg )), + nextReportId_v3 :: !BuildReportId, + buildStatus_v3 :: !BuildStatus +} deriving (Eq, Typeable, Show) + +instance SafeCopy PkgBuildReports_v3 where + version = 3 + kind = extension + putCopy (PkgBuildReports_v3 x _ y) = contain $ safePut (x,y) + getCopy = contain $ mkReports <$> safeGet + where + mkReports (rs,f) = PkgBuildReports_v3 rs + (if Map.null rs + then BuildReportId 1 + else incrementReportId (fst $ Map.findMax rs)) + f +instance MemSize PkgBuildReports_v3 where + memSize (PkgBuildReports_v3 a b c) = memSize3 a b c data PkgBuildReports_v2 = PkgBuildReports_v2 { reports_v2 :: !(Map BuildReportId (BuildReport, Maybe BuildLog)), @@ -309,16 +349,20 @@ instance Migrate PkgBuildReports_v2 where . Map.map (\(br, l) -> (migrate (migrate br), fmap migrate l)) -instance Migrate PkgBuildReports where - type MigrateFrom PkgBuildReports = PkgBuildReports_v2 +instance Migrate PkgBuildReports_v3 where + type MigrateFrom PkgBuildReports_v3 = PkgBuildReports_v2 migrate (PkgBuildReports_v2 m n) = - PkgBuildReports (migrateMap m) n BuildOK + PkgBuildReports_v3 (migrateMap m) n BuildOK where migrateMap :: Map BuildReportId (BuildReport, Maybe BuildLog) -> Map BuildReportId (BuildReport, Maybe BuildLog, Maybe BuildCovg) migrateMap = Map.mapKeys (\x->x) . Map.map (\(br, l) -> (br, l, Nothing)) +instance Migrate PkgBuildReports where + type MigrateFrom PkgBuildReports = PkgBuildReports_v3 + migrate (PkgBuildReports_v3 m n c) = + PkgBuildReports m n c True data BuildReports_v0 = BuildReports_v0 !(Map.Map PackageIdentifier_v0 PkgBuildReports_v0) @@ -345,12 +389,26 @@ instance MemSize BuildReports_v2 where deriveSafeCopy 2 'extension ''BuildReports_v2 -instance Migrate BuildReports where - type MigrateFrom BuildReports = BuildReports_v2 +data BuildReports_v3 = BuildReports_v3 + { reportsIndex_v3 :: !(Map.Map PackageId PkgBuildReports_v3) + } deriving (Eq, Typeable, Show) + +instance Migrate BuildReports_v3 where + type MigrateFrom BuildReports_v3 = BuildReports_v2 migrate (BuildReports_v2 m) = + BuildReports_v3 (Map.mapKeys id $ Map.map migrate m) + +instance MemSize BuildReports_v3 where + memSize (BuildReports_v3 a) = memSize1 a + +deriveSafeCopy 3 'extension ''BuildReports_v3 + +instance Migrate BuildReports where + type MigrateFrom BuildReports = BuildReports_v3 + migrate (BuildReports_v3 m) = BuildReports (Map.mapKeys id $ Map.map migrate m) instance MemSize BuildReports where memSize (BuildReports a) = memSize1 a -deriveSafeCopy 3 'extension ''BuildReports +deriveSafeCopy 4 'extension ''BuildReports \ No newline at end of file diff --git a/src/Distribution/Server/Features/BuildReports/State.hs b/src/Distribution/Server/Features/BuildReports/State.hs index 0895a95a7..ce6ed6d7c 100644 --- a/src/Distribution/Server/Features/BuildReports/State.hs +++ b/src/Distribution/Server/Features/BuildReports/State.hs @@ -80,6 +80,16 @@ lookupFailCount pkgid = asks (BuildReports.lookupFailCount pkgid) lookupLatestReport :: PackageId -> Query BuildReports (Maybe (BuildReportId, BuildReport, Maybe BuildLog, Maybe BuildCovg)) lookupLatestReport pkgid = asks (BuildReports.lookupLatestReport pkgid) +lookupRunTests :: PackageId -> Query BuildReports (Bool) +lookupRunTests pkgid = asks (BuildReports.lookupRunTests pkgid) + +setRunTests :: PackageId -> Bool -> Update BuildReports Bool +setRunTests pkgid b = do + buildReports <- State.get + case BuildReports.setRunTests pkgid b buildReports of + Nothing -> pure False + Just reports -> State.put reports >> pure True + makeAcidic ''BuildReports ['addReport ,'setBuildLog ,'deleteReport @@ -93,5 +103,6 @@ makeAcidic ''BuildReports ['addReport ,'resetFailCount ,'lookupFailCount ,'lookupLatestReport + ,'lookupRunTests + ,'setRunTests ] - diff --git a/src/Distribution/Server/Features/Html.hs b/src/Distribution/Server/Features/Html.hs index 7155f62e4..8e279497b 100644 --- a/src/Distribution/Server/Features/Html.hs +++ b/src/Distribution/Server/Features/Html.hs @@ -123,7 +123,7 @@ initHtmlFeature env@ServerEnv{serverTemplatesDir, serverTemplatesMode, templates <- loadTemplates serverTemplatesMode [serverTemplatesDir, serverTemplatesDir "Html"] [ "maintain.html", "maintain-candidate.html" - , "reports.html", "report.html" + , "reports.html", "report.html", "reports-test.html" , "maintain-docs.html" , "distro-monitor.html" , "revisions.html" @@ -283,7 +283,7 @@ htmlFeature env@ServerEnv{..} htmlUploads = mkHtmlUploads utilities upload htmlDocUploads = mkHtmlDocUploads utilities core docsCore templates htmlDownloads = mkHtmlDownloads utilities download - htmlReports = mkHtmlReports utilities core reportsCore templates + htmlReports = mkHtmlReports utilities core upload user reportsCore templates htmlCandidates = mkHtmlCandidates utilities core versions upload docsCandidates tarIndexCache candidates user templates @@ -1014,10 +1014,10 @@ data HtmlReports = HtmlReports { htmlReportsResources :: [Resource] } -mkHtmlReports :: HtmlUtilities -> CoreFeature -> ReportsFeature -> Templates -> HtmlReports -mkHtmlReports HtmlUtilities{..} CoreFeature{..} ReportsFeature{..} templates = HtmlReports{..} +mkHtmlReports :: HtmlUtilities -> CoreFeature -> UploadFeature -> UserFeature -> ReportsFeature -> Templates -> HtmlReports +mkHtmlReports HtmlUtilities{..} CoreFeature{..} UploadFeature{..} UserFeature{..} ReportsFeature{..} templates = HtmlReports{..} where - CoreResource{packageInPath} = coreResource + CoreResource{packageInPath, guardValidPackageId} = coreResource ReportsResource{..} = reportsResource htmlReportsResources = [ @@ -1027,6 +1027,9 @@ mkHtmlReports HtmlUtilities{..} CoreFeature{..} ReportsFeature{..} templates = H , (extendResource reportsPage) { resourceGet = [ ("html", servePackageReport) ] } + , (extendResource reportsTest) { + resourceGet = [ ("html", servePackageReportTests) ] + } ] servePackageReports :: DynamicPath -> ServerPartE Response @@ -1074,6 +1077,18 @@ mkHtmlReports HtmlUtilities{..} CoreFeature{..} ReportsFeature{..} templates = H det::(Int,Int)->(Int,Int,Int) det (_,0) = (100,0,0) det (a,b) = ((a * 100) `div` b ,a,b) + + servePackageReportTests :: DynamicPath -> ServerPartE Response + servePackageReportTests dpath = do + pkgid <- packageInPath dpath + guardValidPackageId pkgid + guardAuthorised_ [InGroup (maintainersGroup (packageName pkgid)), InGroup trusteesGroup] + template <- getTemplate templates "reports-test.html" + runTests <- queryRunTests pkgid + return $ toResponse $ template + [ "pkgid" $= pkgid + , "runTests" $= runTests + ] {------------------------------------------------------------------------------- Candidates From 8b2be1824bbf1cbc6cbc1a66be7e9ee85a501d62 Mon Sep 17 00:00:00 2001 From: Alias Qli <2576814881@qq.com> Date: Sat, 31 Dec 2022 05:40:14 +0800 Subject: [PATCH 29/43] add deprecated version warning (#1123) --- datafiles/templates/Html/package-page.html.st | 8 +++++++- src/Distribution/Server/Features/Html.hs | 1 + src/Distribution/Server/Features/PreferredVersions.hs | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/datafiles/templates/Html/package-page.html.st b/datafiles/templates/Html/package-page.html.st index bacafe9fc..e24b2d7cf 100644 --- a/datafiles/templates/Html/package-page.html.st +++ b/datafiles/templates/Html/package-page.html.st @@ -36,9 +36,15 @@ $if(isDeprecated)$
- Deprecated. + Deprecated. $deprecatedMsg$
+ $else$ + $if(isDeprecatedVersion)$ +
+ This version is deprecated. +
+ $endif$ $endif$
diff --git a/src/Distribution/Server/Features/Html.hs b/src/Distribution/Server/Features/Html.hs index 8e279497b..c9822095f 100644 --- a/src/Distribution/Server/Features/Html.hs +++ b/src/Distribution/Server/Features/Html.hs @@ -644,6 +644,7 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} , "analyticsPixels" $= map analyticsPixelUrl (Set.toList analyticsPixels) , "versions" $= (PagesNew.renderVersion realpkg (classifyVersions prefInfo $ map packageVersion pkgs) infoUrl) + , "isDeprecatedVersion" $= getVersionStatus prefInfo (packageVersion realpkg) == DeprecatedVersion , "totalDownloads" $= totalDown , "hasexecs" $= not (null execs) , "recentDownloads" $= recentDown diff --git a/src/Distribution/Server/Features/PreferredVersions.hs b/src/Distribution/Server/Features/PreferredVersions.hs index 860c9b7c7..4cd1b4ac9 100644 --- a/src/Distribution/Server/Features/PreferredVersions.hs +++ b/src/Distribution/Server/Features/PreferredVersions.hs @@ -7,6 +7,7 @@ module Distribution.Server.Features.PreferredVersions ( PreferredInfo(..), VersionStatus(..), + getVersionStatus, classifyVersions, PreferredRender(..), From a5bf92c52206d31535213a78df59ce2442629786 Mon Sep 17 00:00:00 2001 From: Alias Qli <2576814881@qq.com> Date: Sat, 31 Dec 2022 06:14:25 +0800 Subject: [PATCH 30/43] List maintainers on package page (#1098) * List maintainers on package page --- datafiles/templates/Html/candidate-page.html.st | 6 ++++++ datafiles/templates/Html/package-page.html.st | 8 +++++++- src/Distribution/Server/Features/Html.hs | 12 +++++++++++- src/Distribution/Server/Pages/Group.hs | 15 +++++++++++---- .../Server/Pages/PackageFromTemplate.hs | 3 +-- 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/datafiles/templates/Html/candidate-page.html.st b/datafiles/templates/Html/candidate-page.html.st index 196b8dd03..940a8efbc 100644 --- a/datafiles/templates/Html/candidate-page.html.st +++ b/datafiles/templates/Html/candidate-page.html.st @@ -130,6 +130,12 @@ $downloadSection$

Maintainer's Corner

+

Package maintainers

+
    +
  • + $maintainers$ +
  • +

For package maintainers and hackage trustees

diff --git a/exes/BuildClient.hs b/exes/BuildClient.hs index bef32a18e..cc9f593fb 100644 --- a/exes/BuildClient.hs +++ b/exes/BuildClient.hs @@ -38,7 +38,7 @@ import System.Exit(exitFailure, ExitCode(..)) import System.FilePath import System.Directory (canonicalizePath, createDirectoryIfMissing, doesFileExist, doesDirectoryExist, getDirectoryContents, - renameFile, removeFile, getAppUserDataDirectory, + renameFile, removeFile, createDirectory, removeDirectoryRecursive, createDirectoryIfMissing, makeAbsolute) import System.Console.GetOpt @@ -156,13 +156,24 @@ initialise opts uri auxUris readMissingOpt prompt = maybe (putStrLn prompt >> getLine) return -- | Parse the @00-index.cache@ file of the available package repositories. -parseRepositoryIndices :: Verbosity -> IO (M.Map PackageIdentifier Tar.EpochTime) -parseRepositoryIndices verbosity = do - cabalDir <- getAppUserDataDirectory "cabal/packages" +parseRepositoryIndices :: BuildOpts -> Verbosity -> IO (M.Map PackageIdentifier Tar.EpochTime) +parseRepositoryIndices opts verbosity = do cacheDirs <- listDirectory cabalDir - indexFiles <- filterM doesFileExist $ map (\dir -> cabalDir dir "01-index.tar") cacheDirs + indexFiles <- catMaybes <$> mapM findIdx cacheDirs M.unions <$> mapM readIndex indexFiles where + cabalDir = bo_stateDir opts "cached-tarballs" + findIdx dir = do + let index01 = cabalDir dir "01-index.tar" + index00 = cabalDir dir "00-index.tar" + b <- doesFileExist index01 + if b + then return (Just index01) + else do + b2 <- doesFileExist index00 + if b2 + then return (Just index00) + else return Nothing readIndex fname = do bs <- BS.readFile fname let mkPkg pkg entry = (pkg, Tar.entryTime entry) @@ -364,6 +375,7 @@ data DocInfo = DocInfo { , docInfoIsCandidate :: Bool , docInfoRunTests :: Bool } + deriving Show docInfoPackageName :: DocInfo -> PackageName docInfoPackageName = pkgName . docInfoPackage @@ -485,7 +497,7 @@ buildOnce opts pkgs = keepGoing $ do -- documentation index. Consequently, we make sure that the packages we are -- going to build actually appear in the repository before building. See -- #543. - repoIndex <- parseRepositoryIndices verbosity + repoIndex <- parseRepositoryIndices opts verbosity pkgIdsHaveDocs <- getDocumentationStats verbosity opts config (Just pkgs) infoStats verbosity Nothing pkgIdsHaveDocs @@ -576,9 +588,9 @@ processPkg verbosity opts config docInfo = do let installOk = fmap ("install-outcome: InstallOk" `isInfixOf`) buildReport == Just True -- Run Tests if installOk, Run coverage is Tests runs - (testOutcome, hpcLoc) <- case installOk && docInfoRunTests docInfo of + (testOutcome, hpcLoc, testfile) <- case installOk && docInfoRunTests docInfo of True -> testPackage verbosity opts docInfo - False -> return (Nothing, Nothing) + False -> return (Nothing, Nothing, Nothing) coverageFile <- mapM (coveragePackage verbosity opts docInfo) hpcLoc -- Modify test-outcome and rewrite report file. @@ -587,7 +599,7 @@ processPkg verbosity opts config docInfo = do case bo_dryRun opts of True -> return () False -> uploadResults verbosity config docInfo - mTgz mRpt logfile coverageFile installOk + mTgz mRpt logfile testfile coverageFile installOk where prepareTempBuildDir :: IO () prepareTempBuildDir = do @@ -637,7 +649,7 @@ coveragePackage verbosity opts docInfo loc = do return coverageFile -testPackage :: Verbosity -> BuildOpts -> DocInfo -> IO (Maybe String, Maybe FilePath) +testPackage :: Verbosity -> BuildOpts -> DocInfo -> IO (Maybe String, Maybe FilePath, Maybe FilePath) testPackage verbosity opts docInfo = do let pkgid = docInfoPackage docInfo testLogFile = (installDirectory opts) display pkgid <.> "test" @@ -670,7 +682,7 @@ testPackage verbosity opts docInfo = do [ "Test results for " ++ display pkgid ++ ":" , testResultFile ] - return (testOutcome, hpcLoc) + return (testOutcome, hpcLoc, Just testResultFile) -- | Build documentation and return @(Just tgz)@ for the built tgz file @@ -862,9 +874,9 @@ tarGzDirectory dir = do where (containing_dir, nested_dir) = splitFileName dir uploadResults :: Verbosity -> BuildConfig -> DocInfo -> Maybe FilePath - -> Maybe FilePath -> FilePath -> Maybe FilePath -> Bool -> IO () + -> Maybe FilePath -> FilePath -> Maybe FilePath -> Maybe FilePath -> Bool -> IO () uploadResults verbosity config docInfo - mdocsTarballFile buildReportFile buildLogFile coverageFile installOk = + mdocsTarballFile buildReportFile buildLogFile testLogFile coverageFile installOk = httpSession verbosity "hackage-build" version $ do -- Make sure we authenticate to Hackage setAuthorityGen (provideAuthInfo (bc_srcURI config) @@ -874,7 +886,7 @@ uploadResults verbosity config docInfo Just docsTarballFile -> putDocsTarball config docInfo docsTarballFile - putBuildFiles config docInfo buildReportFile buildLogFile coverageFile installOk + putBuildFiles config docInfo buildReportFile buildLogFile testLogFile coverageFile installOk putDocsTarball :: BuildConfig -> DocInfo -> FilePath -> HttpSession () putDocsTarball config docInfo docsTarballFile = @@ -882,13 +894,14 @@ putDocsTarball config docInfo docsTarballFile = "application/x-tar" (Just "gzip") docsTarballFile putBuildFiles :: BuildConfig -> DocInfo -> Maybe FilePath - -> FilePath -> Maybe FilePath -> Bool -> HttpSession () -putBuildFiles config docInfo reportFile buildLogFile coverageFile installOk = do + -> FilePath -> Maybe FilePath -> Maybe FilePath -> Bool -> HttpSession () +putBuildFiles config docInfo reportFile buildLogFile testLogFile coverageFile installOk = do reportContent <- liftIO $ traverse readFile reportFile logContent <- liftIO $ readFile buildLogFile + testContent <- liftIO $ traverse readFile testLogFile coverageContent <- liftIO $ traverse readFile coverageFile let uri = docInfoReports config docInfo - body = encode $ BR.BuildFiles reportContent (Just logContent) coverageContent (not installOk) + body = encode $ BR.BuildFiles reportContent (Just logContent) testContent coverageContent (not installOk) setAllowRedirects False (_, response) <- request Request { rqURI = uri, diff --git a/src/Distribution/Server/Features/BuildReports.hs b/src/Distribution/Server/Features/BuildReports.hs index c443bbab2..300872e11 100644 --- a/src/Distribution/Server/Features/BuildReports.hs +++ b/src/Distribution/Server/Features/BuildReports.hs @@ -6,7 +6,7 @@ module Distribution.Server.Features.BuildReports ( initBuildReportsFeature ) where -import Distribution.Server.Framework hiding (BuildLog, BuildCovg) +import Distribution.Server.Framework hiding (BuildLog, TestLog, BuildCovg) import Distribution.Server.Features.Users import Distribution.Server.Features.Upload @@ -16,7 +16,7 @@ import Distribution.Server.Features.BuildReports.Backup import Distribution.Server.Features.BuildReports.State import qualified Distribution.Server.Features.BuildReports.BuildReport as BuildReport import Distribution.Server.Features.BuildReports.BuildReport (BuildReport(..)) -import Distribution.Server.Features.BuildReports.BuildReports (BuildReports, BuildReportId(..), BuildCovg(..), BuildLog(..)) +import Distribution.Server.Features.BuildReports.BuildReports (BuildReports, BuildReportId(..), BuildCovg(..), BuildLog(..), TestLog(..)) import qualified Distribution.Server.Framework.ResponseContentTypes as Resource import Distribution.Server.Packages.Types @@ -42,10 +42,11 @@ data ReportsFeature = ReportsFeature { reportsFeatureInterface :: HackageFeature, packageReports :: DynamicPath -> ([(BuildReportId, BuildReport)] -> ServerPartE Response) -> ServerPartE Response, - packageReport :: DynamicPath -> ServerPartE (BuildReportId, BuildReport, Maybe BuildLog, Maybe BuildCovg), + packageReport :: DynamicPath -> ServerPartE (BuildReportId, BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg), queryPackageReports :: forall m. MonadIO m => PackageId -> m [(BuildReportId, BuildReport)], queryBuildLog :: forall m. MonadIO m => BuildLog -> m Resource.BuildLog, + queryTestLog :: forall m. MonadIO m => TestLog -> m Resource.TestLog, pkgReportDetails :: forall m. MonadIO m => (PackageIdentifier, Bool) -> m BuildReport.PkgDetails, queryLastReportStats:: forall m. MonadIO m => PackageIdentifier -> m (Maybe (BuildReportId, BuildReport, Maybe BuildCovg)), queryRunTests :: forall m. MonadIO m => PackageId -> m Bool, @@ -60,8 +61,9 @@ data ReportsResource = ReportsResource { reportsList :: Resource, reportsPage :: Resource, reportsLog :: Resource, - reportsReset:: Resource, reportsTest :: Resource, + reportsReset:: Resource, + reportsTestsEnabled :: Resource, reportsListUri :: String -> PackageId -> String, reportsPageUri :: String -> PackageId -> BuildReportId -> String, reportsLogUri :: PackageId -> BuildReportId -> String @@ -121,8 +123,9 @@ buildReportsFeature name reportsList , reportsPage , reportsLog - , reportsReset , reportsTest + , reportsReset + , reportsTestsEnabled ] , featureState = [abstractAcidStateComponent reportsState] } @@ -144,12 +147,12 @@ buildReportsFeature name ] , resourceGet = [ ("", resetBuildFails) ] } - , reportsTest = (extendResourcePath "/reports/test/" corePackagePage) { + , reportsTestsEnabled = (extendResourcePath "/reports/testsEnabled/" corePackagePage) { resourceDesc = [ (GET, "Get reports test settings") , (POST, "Set reports test settings") ] - , resourceGet = [ ("json", getReportsTest) ] - , resourcePost = [ ("", postReportsTest) ] + , resourceGet = [ ("json", getReportsTestsEnabled) ] + , resourcePost = [ ("", postReportsTestsEnabled) ] } , reportsPage = (extendResourcePath "/reports/:id.:format" corePackagePage) { resourceDesc = [ (GET, "Get a specific build report") @@ -167,6 +170,15 @@ buildReportsFeature name , resourceDelete = [ ("", deleteBuildLog )] , resourcePut = [ ("", putBuildLog) ] } + , reportsTest = (extendResourcePath "/reports/:id/test" corePackagePage) { + resourceDesc = [ (GET, "Get the test log associated with a build report") + , (DELETE, "Delete a test log") + , (PUT, "Upload a test log for a build report") + ] + , resourceGet = [ ("txt", serveTestLog) ] + , resourceDelete = [ ("", deleteTestLog )] + , resourcePut = [ ("", putTestLog) ] + } , reportsListUri = \format pkgid -> renderResource (reportsList reportsResource) [display pkgid, format] , reportsPageUri = \format pkgid repid -> renderResource (reportsPage reportsResource) [display pkgid, display repid, format] , reportsLogUri = \pkgid repid -> renderResource (reportsLog reportsResource) [display pkgid, display repid] @@ -187,7 +199,7 @@ buildReportsFeature name guardValidPackageId pkgid queryPackageReports pkgid >>= continue - packageReport :: DynamicPath -> ServerPartE (BuildReportId, BuildReport, Maybe BuildLog, Maybe BuildCovg) + packageReport :: DynamicPath -> ServerPartE (BuildReportId, BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg) packageReport dpath = do pkgid <- packageInPath dpath guardValidPackageId pkgid @@ -195,18 +207,22 @@ buildReportsFeature name mreport <- queryState reportsState $ LookupReportCovg pkgid reportId case mreport of Nothing -> errNotFound "Report not found" [MText "Build report does not exist"] - Just (report, mlog, covg) -> return (reportId, report, mlog, covg) + Just (report, mlog, mtest, covg) -> return (reportId, report, mlog, mtest, covg) queryPackageReports :: MonadIO m => PackageId -> m [(BuildReportId, BuildReport)] queryPackageReports pkgid = do reports <- queryState reportsState $ LookupPackageReports pkgid - return $ map (second fst) reports + return $ map (second (\(a, _, _) -> a)) reports queryBuildLog :: MonadIO m => BuildLog -> m Resource.BuildLog queryBuildLog (BuildLog blobId) = do file <- liftIO $ BlobStorage.fetch store blobId return $ Resource.BuildLog file + queryTestLog :: MonadIO m => TestLog -> m Resource.TestLog + queryTestLog (TestLog blobId) = do + file <- liftIO $ BlobStorage.fetch store blobId + return $ Resource.TestLog file pkgReportDetails :: MonadIO m => (PackageIdentifier, Bool) -> m BuildReport.PkgDetails--(PackageIdentifier, Bool, Maybe (BuildStatus, Maybe UTCTime, Maybe Version)) pkgReportDetails (pkgid, docs) = do @@ -215,7 +231,7 @@ buildReportsFeature name runTests <- fmap Just . queryState reportsState $ LookupRunTests pkgid (time, ghcId) <- case latestRpt of Nothing -> return (Nothing,Nothing) - Just (_, brp, _, _) -> do + Just (_, brp, _, _, _) -> do let (CompilerId _ vrsn) = compiler brp return (time brp, Just vrsn) return (BuildReport.PkgDetails pkgid docs failCnt time ghcId runTests) @@ -225,7 +241,7 @@ buildReportsFeature name lookupRes <- queryState reportsState $ LookupLatestReport pkgid case lookupRes of Nothing -> return Nothing - Just (rptId, rpt, _, covg) -> return (Just (rptId, rpt, covg)) + Just (rptId, rpt, _, _, covg) -> return (Just (rptId, rpt, covg)) queryRunTests :: MonadIO m => PackageId -> m Bool queryRunTests pkgid = queryState reportsState $ LookupRunTests pkgid @@ -235,19 +251,30 @@ buildReportsFeature name textPackageReports dpath = packageReports dpath $ return . toResponse . show textPackageReport dpath = do - (_, report, _, _) <- packageReport dpath + (_, report, _, _, _) <- packageReport dpath return . toResponse $ BuildReport.show report -- result: not-found error or text file serveBuildLog :: DynamicPath -> ServerPartE Response serveBuildLog dpath = do - (repid, _, mlog, _) <- packageReport dpath + (repid, _, mlog, _, _) <- packageReport dpath case mlog of Nothing -> errNotFound "Log not found" [MText $ "Build log for report " ++ display repid ++ " not found"] Just logId -> do cacheControlWithoutETag [Public, maxAgeDays 30] toResponse <$> queryBuildLog logId + -- result: not-found error or text file + serveTestLog :: DynamicPath -> ServerPartE Response + serveTestLog dpath = do + (repid, _, _, mtest, _) <- packageReport dpath + case mtest of + Nothing -> errNotFound "Test log not found" [MText $ "Test log for report " ++ display repid ++ " not found"] + Just logId -> do + cacheControlWithoutETag [Public, maxAgeDays 30] + toResponse <$> queryTestLog logId + + -- result: auth error, not-found error, parse error, or redirect submitBuildReport :: DynamicPath -> ServerPartE Response submitBuildReport dpath = do @@ -300,6 +327,18 @@ buildReportsFeature name void $ updateState reportsState $ SetBuildLog pkgid reportId (Just $ BuildLog buildLog) noContent (toResponse ()) + putTestLog :: DynamicPath -> ServerPartE Response + putTestLog dpath = do + pkgid <- packageInPath dpath + guardValidPackageId pkgid + reportId <- reportIdInPath dpath + -- logged in users + guardAuthorised_ [AnyKnownUser] + blogbody <- expectTextPlain + testLog <- liftIO $ BlobStorage.add store blogbody + void $ updateState reportsState $ SetTestLog pkgid reportId (Just $ TestLog testLog) + noContent (toResponse ()) + {- Example using curl: (TODO: why is this PUT, while logs are POST?) @@ -319,6 +358,15 @@ buildReportsFeature name void $ updateState reportsState $ SetBuildLog pkgid reportId Nothing noContent (toResponse ()) + deleteTestLog :: DynamicPath -> ServerPartE Response + deleteTestLog dpath = do + pkgid <- packageInPath dpath + guardValidPackageId pkgid + reportId <- reportIdInPath dpath + guardAuthorised_ [InGroup trusteesGroup] + void $ updateState reportsState $ SetTestLog pkgid reportId Nothing + noContent (toResponse ()) + guardAuthorisedAsMaintainerOrTrustee pkgname = guardAuthorised_ [InGroup (maintainersGroup pkgname), InGroup trusteesGroup] @@ -332,16 +380,16 @@ buildReportsFeature name then seeOther (reportsListUri reportsResource "" pkgid) $ toResponse () else errNotFound "Report not found" [MText "Build report does not exist"] - getReportsTest :: DynamicPath -> ServerPartE Response - getReportsTest dpath = do + getReportsTestsEnabled :: DynamicPath -> ServerPartE Response + getReportsTestsEnabled dpath = do pkgid <- packageInPath dpath guardValidPackageId pkgid guardAuthorisedAsMaintainerOrTrustee (packageName pkgid) runTest <- queryRunTests pkgid pure $ toResponse $ toJSON runTest - postReportsTest :: DynamicPath -> ServerPartE Response - postReportsTest dpath = do + postReportsTestsEnabled :: DynamicPath -> ServerPartE Response + postReportsTestsEnabled dpath = do pkgid <- packageInPath dpath runTests <- body $ looks "runTests" guardValidPackageId pkgid @@ -360,6 +408,7 @@ buildReportsFeature name buildFiles <- expectAesonContent::ServerPartE BuildReport.BuildFiles let reportBody = BuildReport.reportContent buildFiles logBody = BuildReport.logContent buildFiles + testBody = BuildReport.testContent buildFiles covgBody = BuildReport.coverageContent buildFiles failStatus = BuildReport.buildFail buildFiles @@ -374,8 +423,9 @@ buildReportsFeature name guardAuthorisedAsMaintainerOrTrustee (packageName pkgid) report' <- liftIO $ BuildReport.affixTimestamp report logBlob <- liftIO $ traverse (\x -> BlobStorage.add store $ fromString x) logBody + testBlob <- liftIO $ traverse (\x -> BlobStorage.add store $ fromString x) testBody reportId <- updateState reportsState $ - AddRptLogCovg pkgid (report', (fmap BuildLog logBlob), (fmap BuildReport.parseCovg covgBody)) + AddRptLogTestCovg pkgid (report', (fmap BuildLog logBlob), (fmap TestLog testBlob), (fmap BuildReport.parseCovg covgBody)) -- redirect to new reports page seeOther (reportsPageUri reportsResource "" pkgid reportId) $ toResponse () diff --git a/src/Distribution/Server/Features/BuildReports/Backup.hs b/src/Distribution/Server/Features/BuildReports/Backup.hs index 5ba7f8081..09e9fe4c5 100644 --- a/src/Distribution/Server/Features/BuildReports/Backup.hs +++ b/src/Distribution/Server/Features/BuildReports/Backup.hs @@ -8,7 +8,7 @@ module Distribution.Server.Features.BuildReports.Backup ( import Distribution.Server.Features.BuildReports.BuildReport (BuildReport) import qualified Distribution.Server.Features.BuildReports.BuildReport as Report -import Distribution.Server.Features.BuildReports.BuildReports (BuildReports(..), BuildCovg(..), PkgBuildReports(..), BuildReportId(..), BuildLog(..)) +import Distribution.Server.Features.BuildReports.BuildReports (BuildReports(..), BuildCovg(..), PkgBuildReports(..), BuildReportId(..), BuildLog(..), TestLog(..)) import qualified Distribution.Server.Features.BuildReports.BuildReports as Reports import qualified Distribution.Server.Framework.BlobStorage as BlobStorage @@ -94,8 +94,8 @@ packageReportsToExport :: PackageId -> PkgBuildReports -> [BackupEntry] packageReportsToExport pkgId pkgReports = concatMap (uncurry $ reportToExport prefix) (Map.toList $ Reports.reports pkgReports) where prefix = ["package", display pkgId] -reportToExport :: [FilePath] -> BuildReportId -> (BuildReport, Maybe BuildLog, Maybe BuildCovg ) -> [BackupEntry] -reportToExport prefix reportId (report, mlog, _) = BackupByteString (getPath ".txt") (packUTF8 $ Report.show report) : +reportToExport :: [FilePath] -> BuildReportId -> (BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg ) -> [BackupEntry] +reportToExport prefix reportId (report, mlog, _, _) = BackupByteString (getPath ".txt") (packUTF8 $ Report.show report) : case mlog of Nothing -> []; Just (BuildLog blobId) -> [blobToBackup (getPath ".log") blobId] where getPath ext = prefix ++ [display reportId ++ ext] diff --git a/src/Distribution/Server/Features/BuildReports/BuildReport.hs b/src/Distribution/Server/Features/BuildReports/BuildReport.hs index b6959eff2..ef1231cca 100644 --- a/src/Distribution/Server/Features/BuildReports/BuildReport.hs +++ b/src/Distribution/Server/Features/BuildReports/BuildReport.hs @@ -611,6 +611,7 @@ instance Migrate InstallOutcome where data BuildFiles = BuildFiles { reportContent :: Maybe String, logContent :: Maybe String, + testContent :: Maybe String, coverageContent :: Maybe String, buildFail :: Bool } deriving Show @@ -620,6 +621,7 @@ instance Data.Aeson.FromJSON BuildFiles where BuildFiles <$> o .:? "report" <*> o .:? "log" + <*> o .:? "test" <*> o .:? "coverage" <*> o .: "buildFail" @@ -627,6 +629,7 @@ instance Data.Aeson.ToJSON BuildFiles where toJSON p = object [ "report" .= reportContent p, "log" .= logContent p, + "test" .= testContent p, "coverage" .= coverageContent p, "buildFail" .= buildFail p ] diff --git a/src/Distribution/Server/Features/BuildReports/BuildReports.hs b/src/Distribution/Server/Features/BuildReports/BuildReports.hs index 611f7d23e..fa650eda5 100644 --- a/src/Distribution/Server/Features/BuildReports/BuildReports.hs +++ b/src/Distribution/Server/Features/BuildReports/BuildReports.hs @@ -4,18 +4,21 @@ module Distribution.Server.Features.BuildReports.BuildReports ( BuildReport(..), BuildReports(..), + BuildReports_v3, BuildReportId(..), PkgBuildReports(..), BuildLog(..), + TestLog(..), BuildCovg(..), BuildStatus(..), - addRptLogCovg, + addRptLogTestCovg, lookupReportCovg, emptyReports, emptyPkgReports, addReport, deleteReport, setBuildLog, + setTestLog, lookupReport, lookupPackageReports, unsafeSetReport, @@ -80,13 +83,15 @@ instance Parsec BuildReportId where newtype BuildLog = BuildLog BlobStorage.BlobId deriving (Eq, Typeable, Show, MemSize) +newtype TestLog = TestLog BlobStorage.BlobId + deriving (Eq, Typeable, Show, MemSize) data PkgBuildReports = PkgBuildReports { -- for each report, other useful information: Maybe UserId, UTCTime -- perhaps deserving its own data structure (SubmittedReport?) -- When a report was submitted is very useful information. -- also, use IntMap instead of Map BuildReportId? - reports :: !(Map BuildReportId (BuildReport, Maybe BuildLog, Maybe BuildCovg )), + reports :: !(Map BuildReportId (BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg )), -- one more than the maximum report id used nextReportId :: !BuildReportId, buildStatus :: !BuildStatus, @@ -111,26 +116,26 @@ emptyReports = BuildReports { reportsIndex = Map.empty } -lookupReport :: PackageId -> BuildReportId -> BuildReports -> Maybe (BuildReport, Maybe BuildLog) +lookupReport :: PackageId -> BuildReportId -> BuildReports -> Maybe (BuildReport, Maybe BuildLog, Maybe TestLog) lookupReport pkgid reportId buildReports = remCvg.Map.lookup reportId . reports =<< Map.lookup pkgid (reportsIndex buildReports) where remCvg Nothing = Nothing - remCvg (Just (brpt,blog,_)) = Just (brpt,blog) + remCvg (Just (brpt,blog,btest,_)) = Just (brpt,blog,btest) -lookupPackageReports :: PackageId -> BuildReports -> [(BuildReportId, (BuildReport, Maybe BuildLog))] +lookupPackageReports :: PackageId -> BuildReports -> [(BuildReportId, (BuildReport, Maybe BuildLog, Maybe TestLog))] lookupPackageReports pkgid buildReports = case Map.lookup pkgid (reportsIndex buildReports) of Nothing -> [] Just rs -> map removeCovg $ Map.toList (reports rs) where - removeCovg (brid,(brpt,blog,_)) = (brid,(brpt,blog)) + removeCovg (brid,(brpt,blog,btest,_)) = (brid,(brpt,blog,btest)) ------------------------- -- PackageIds should /not/ have empty Versions. Caller should ensure this. -addReport :: PackageId -> (BuildReport, Maybe BuildLog) -> BuildReports -> (BuildReports, BuildReportId) -addReport pkgid (brpt,blog) buildReports = +addReport :: PackageId -> (BuildReport, Maybe BuildLog, Maybe TestLog) -> BuildReports -> (BuildReports, BuildReportId) +addReport pkgid (brpt,blog,btest) buildReports = let pkgReports = Map.findWithDefault emptyPkgReports pkgid (reportsIndex buildReports) reportId = nextReportId pkgReports - pkgReports' = PkgBuildReports { reports = Map.insert reportId (brpt,blog,Nothing) (reports pkgReports) + pkgReports' = PkgBuildReports { reports = Map.insert reportId (brpt,blog,btest,Nothing) (reports pkgReports) , nextReportId = incrementReportId reportId , buildStatus = buildStatus pkgReports , runTests = runTests pkgReports } @@ -139,7 +144,7 @@ addReport pkgid (brpt,blog) buildReports = unsafeSetReport :: PackageId -> BuildReportId -> (BuildReport, Maybe BuildLog) -> BuildReports -> BuildReports unsafeSetReport pkgid reportId (brpt,blog) buildReports = let pkgReports = Map.findWithDefault emptyPkgReports pkgid (reportsIndex buildReports) - pkgReports' = PkgBuildReports { reports = Map.insert reportId (brpt,blog,Nothing) (reports pkgReports) + pkgReports' = PkgBuildReports { reports = Map.insert reportId (brpt,blog,Nothing,Nothing) (reports pkgReports) , nextReportId = max (incrementReportId reportId) (nextReportId pkgReports) , buildStatus = buildStatus pkgReports , runTests = runTests pkgReports } @@ -158,11 +163,19 @@ setBuildLog pkgid reportId buildLog buildReports = case Map.lookup pkgid (report Nothing -> Nothing Just pkgReports -> case Map.lookup reportId (reports pkgReports) of Nothing -> Nothing - Just (rlog, _, covg) -> let pkgReports' = pkgReports { reports = Map.insert reportId (rlog, buildLog, covg) (reports pkgReports) } + Just (rlog, _, btest, covg) -> let pkgReports' = pkgReports { reports = Map.insert reportId (rlog, buildLog, btest, covg) (reports pkgReports) } + in Just $ buildReports { reportsIndex = Map.insert pkgid pkgReports' (reportsIndex buildReports) } + +setTestLog :: PackageId -> BuildReportId -> Maybe TestLog -> BuildReports -> Maybe BuildReports +setTestLog pkgid reportId testLog buildReports = case Map.lookup pkgid (reportsIndex buildReports) of + Nothing -> Nothing + Just pkgReports -> case Map.lookup reportId (reports pkgReports) of + Nothing -> Nothing + Just (rlog, blog, _, covg) -> let pkgReports' = pkgReports { reports = Map.insert reportId (rlog, blog, testLog, covg) (reports pkgReports) } in Just $ buildReports { reportsIndex = Map.insert pkgid pkgReports' (reportsIndex buildReports) } -addRptLogCovg :: PackageId -> (BuildReport, Maybe BuildLog, Maybe BuildCovg ) -> BuildReports -> (BuildReports, BuildReportId) -addRptLogCovg pkgid report buildReports = +addRptLogTestCovg :: PackageId -> (BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg ) -> BuildReports -> (BuildReports, BuildReportId) +addRptLogTestCovg pkgid report buildReports = let pkgReports = Map.findWithDefault emptyPkgReports pkgid (reportsIndex buildReports) reportId = nextReportId pkgReports pkgReports' = PkgBuildReports { reports = Map.insert reportId report (reports pkgReports) @@ -171,7 +184,7 @@ addRptLogCovg pkgid report buildReports = , runTests = runTests pkgReports } in (buildReports { reportsIndex = Map.insert pkgid pkgReports' (reportsIndex buildReports) }, reportId) -lookupReportCovg :: PackageId -> BuildReportId -> BuildReports -> Maybe (BuildReport, Maybe BuildLog, Maybe BuildCovg ) +lookupReportCovg :: PackageId -> BuildReportId -> BuildReports -> Maybe (BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg ) lookupReportCovg pkgid reportId buildReports = Map.lookup reportId . reports =<< Map.lookup pkgid (reportsIndex buildReports) setFailStatus :: PackageId -> Bool -> BuildReports -> BuildReports @@ -204,15 +217,15 @@ lookupFailCount pkgid buildReports = do rp <- Map.lookup pkgid (reportsIndex buildReports) return $ buildStatus rp -lookupLatestReport :: PackageId -> BuildReports -> Maybe (BuildReportId, BuildReport, Maybe BuildLog, Maybe BuildCovg) +lookupLatestReport :: PackageId -> BuildReports -> Maybe (BuildReportId, BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg) lookupLatestReport pkgid buildReports = do rp <- Map.lookup pkgid (reportsIndex buildReports) let rs = reports rp - (maxKey, (rep, buildLog, covg)) <- + (maxKey, (rep, buildLog, testLog, covg)) <- if Map.null rs then Nothing else Just $ Map.findMax rs - Just (maxKey, rep, buildLog, covg) + Just (maxKey, rep, buildLog, testLog, covg) lookupRunTests :: PackageId -> BuildReports -> Bool lookupRunTests pkgid buildReports = maybe True runTests $ Map.lookup pkgid (reportsIndex buildReports) @@ -261,6 +274,8 @@ instance Migrate BuildLog where deriveSafeCopy 2 'extension ''BuildLog +deriveSafeCopy 0 'base ''TestLog + -- note: if the set of report ids is [1, 2, 3], then nextReportId = 4 -- after calling deleteReport for 3, the set is [1, 2] and nextReportId is still 4. -- however, upon importing, nextReportId will = 3, one more than the maximum present @@ -361,8 +376,13 @@ instance Migrate PkgBuildReports_v3 where instance Migrate PkgBuildReports where type MigrateFrom PkgBuildReports = PkgBuildReports_v3 - migrate (PkgBuildReports_v3 m n c) = - PkgBuildReports m n c True + migrate (PkgBuildReports_v3 m n o) = + PkgBuildReports (migrateMap m) n o True + where + migrateMap :: Map BuildReportId (BuildReport, Maybe BuildLog, Maybe BuildCovg) + -> Map BuildReportId (BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg) + migrateMap = Map.mapKeys id + . Map.map (\(br, l, c) -> (br, l, Nothing, c)) data BuildReports_v0 = BuildReports_v0 !(Map.Map PackageIdentifier_v0 PkgBuildReports_v0) @@ -411,4 +431,4 @@ instance Migrate BuildReports where instance MemSize BuildReports where memSize (BuildReports a) = memSize1 a -deriveSafeCopy 4 'extension ''BuildReports \ No newline at end of file +deriveSafeCopy 4 'extension ''BuildReports diff --git a/src/Distribution/Server/Features/BuildReports/State.hs b/src/Distribution/Server/Features/BuildReports/State.hs index ce6ed6d7c..0dec1518c 100644 --- a/src/Distribution/Server/Features/BuildReports/State.hs +++ b/src/Distribution/Server/Features/BuildReports/State.hs @@ -5,7 +5,7 @@ module Distribution.Server.Features.BuildReports.State where import Distribution.Server.Features.BuildReports.BuildReports - (BuildReportId, BuildLog, BuildReport, BuildReports,BuildCovg, BuildStatus) + (BuildReportId, BuildLog, TestLog, BuildReport, BuildReports,BuildCovg, BuildStatus) import qualified Distribution.Server.Features.BuildReports.BuildReports as BuildReports import Distribution.Package @@ -19,9 +19,9 @@ initialBuildReports = BuildReports.emptyReports -- and defined methods addReport :: PackageId -> (BuildReport, Maybe BuildLog) -> Update BuildReports BuildReportId -addReport pkgid report = do +addReport pkgid (bRpt, blog) = do buildReports <- State.get - let (reports, reportId) = BuildReports.addReport pkgid report buildReports + let (reports, reportId) = BuildReports.addReport pkgid (bRpt, blog, Nothing) buildReports State.put reports return reportId @@ -39,10 +39,10 @@ deleteReport pkgid reportId = do Nothing -> return False Just reports -> State.put reports >> return True -lookupReport :: PackageId -> BuildReportId -> Query BuildReports (Maybe (BuildReport, Maybe BuildLog)) +lookupReport :: PackageId -> BuildReportId -> Query BuildReports (Maybe (BuildReport, Maybe BuildLog, Maybe TestLog)) lookupReport pkgid reportId = asks (BuildReports.lookupReport pkgid reportId) -lookupPackageReports :: PackageId -> Query BuildReports [(BuildReportId, (BuildReport, Maybe BuildLog))] +lookupPackageReports :: PackageId -> Query BuildReports [(BuildReportId, (BuildReport, Maybe BuildLog, Maybe TestLog))] lookupPackageReports pkgid = asks (BuildReports.lookupPackageReports pkgid) getBuildReports :: Query BuildReports BuildReports @@ -52,13 +52,13 @@ replaceBuildReports :: BuildReports -> Update BuildReports () replaceBuildReports = State.put addRptLogCovg :: PackageId -> (BuildReport, Maybe BuildLog, Maybe BuildCovg ) -> Update BuildReports BuildReportId -addRptLogCovg pkgid report = do +addRptLogCovg pkgid (bRpt, blog, bcovg) = do buildReports <- State.get - let (reports, reportId) = BuildReports.addRptLogCovg pkgid report buildReports + let (reports, reportId) = BuildReports.addRptLogTestCovg pkgid (bRpt, blog, Nothing, bcovg) buildReports State.put reports return reportId -lookupReportCovg :: PackageId -> BuildReportId -> Query BuildReports (Maybe (BuildReport, Maybe BuildLog, Maybe BuildCovg)) +lookupReportCovg :: PackageId -> BuildReportId -> Query BuildReports (Maybe (BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg)) lookupReportCovg pkgid reportId = asks (BuildReports.lookupReportCovg pkgid reportId) setFailStatus :: PackageId -> Bool -> Update BuildReports () @@ -77,9 +77,23 @@ resetFailCount pkgid = do lookupFailCount :: PackageId -> Query BuildReports (Maybe BuildStatus) lookupFailCount pkgid = asks (BuildReports.lookupFailCount pkgid) -lookupLatestReport :: PackageId -> Query BuildReports (Maybe (BuildReportId, BuildReport, Maybe BuildLog, Maybe BuildCovg)) +lookupLatestReport :: PackageId -> Query BuildReports (Maybe (BuildReportId, BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg)) lookupLatestReport pkgid = asks (BuildReports.lookupLatestReport pkgid) +addRptLogTestCovg :: PackageId -> (BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg ) -> Update BuildReports BuildReportId +addRptLogTestCovg pkgid (bRpt, blog, btest, bcovg) = do + buildReports <- State.get + let (reports, reportId) = BuildReports.addRptLogTestCovg pkgid (bRpt, blog, btest, bcovg) buildReports + State.put reports + return reportId + +setTestLog :: PackageId -> BuildReportId -> Maybe TestLog -> Update BuildReports Bool +setTestLog pkgid reportId testLog = do + buildReports <- State.get + case BuildReports.setTestLog pkgid reportId testLog buildReports of + Nothing -> return False + Just reports -> State.put reports >> return True + lookupRunTests :: PackageId -> Query BuildReports (Bool) lookupRunTests pkgid = asks (BuildReports.lookupRunTests pkgid) @@ -103,6 +117,8 @@ makeAcidic ''BuildReports ['addReport ,'resetFailCount ,'lookupFailCount ,'lookupLatestReport + ,'addRptLogTestCovg + ,'setTestLog ,'lookupRunTests ,'setRunTests ] diff --git a/src/Distribution/Server/Features/Html.hs b/src/Distribution/Server/Features/Html.hs index 48dc07d39..b332205a2 100644 --- a/src/Distribution/Server/Features/Html.hs +++ b/src/Distribution/Server/Features/Html.hs @@ -1035,7 +1035,7 @@ mkHtmlReports HtmlUtilities{..} CoreFeature{..} UploadFeature{..} UserFeature{.. , (extendResource reportsPage) { resourceGet = [ ("html", servePackageReport) ] } - , (extendResource reportsTest) { + , (extendResource reportsTestsEnabled) { resourceGet = [ ("html", servePackageReportTests) ] } ] @@ -1059,8 +1059,9 @@ mkHtmlReports HtmlUtilities{..} CoreFeature{..} UploadFeature{..} UserFeature{.. servePackageReport :: DynamicPath -> ServerPartE Response servePackageReport dpath = do - (repid, report, mlog, covg) <- packageReport dpath + (repid, report, mlog, mtest, covg) <- packageReport dpath mlog' <- traverse queryBuildLog mlog + mtest' <- traverse queryTestLog mtest let covg' = fmap getCvgDet covg pkgid <- packageInPath dpath cacheControlWithoutETag [Public, maxAgeDays 30] @@ -1069,6 +1070,7 @@ mkHtmlReports HtmlUtilities{..} CoreFeature{..} UploadFeature{..} UserFeature{.. [ "pkgid" $= (pkgid :: PackageIdentifier) , "report" $= (repid, report) , "log" $= toMessage <$> mlog' + , "test" $= toMessage <$> mtest' , "covg" $= covg' ] where @@ -1085,7 +1087,7 @@ mkHtmlReports HtmlUtilities{..} CoreFeature{..} UploadFeature{..} UserFeature{.. det::(Int,Int)->(Int,Int,Int) det (_,0) = (100,0,0) det (a,b) = ((a * 100) `div` b ,a,b) - + servePackageReportTests :: DynamicPath -> ServerPartE Response servePackageReportTests dpath = do pkgid <- packageInPath dpath diff --git a/src/Distribution/Server/Framework/ResponseContentTypes.hs b/src/Distribution/Server/Framework/ResponseContentTypes.hs index f42fee51c..fd20d7fe2 100644 --- a/src/Distribution/Server/Framework/ResponseContentTypes.hs +++ b/src/Distribution/Server/Framework/ResponseContentTypes.hs @@ -180,6 +180,12 @@ instance ToMessage BuildLog where toContentType _ = "text/plain" toMessage (BuildLog bs) = bs +newtype TestLog = TestLog BS.Lazy.ByteString + +instance ToMessage TestLog where + toContentType _ = "text/plain" + toMessage (TestLog bs) = bs + newtype BuildCovg = BuildCovg BS.Lazy.ByteString instance ToMessage BuildCovg where From 4d0bd17b1b90f495b59d3e3a6e88e04411b78024 Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Sun, 1 Jan 2023 12:34:00 -0600 Subject: [PATCH 38/43] Reverse Dependencies indexed on PackageName (#1082) * Rebased Reverse Dependencies --- benchmarks/RevDeps.hs | 76 ++ datafiles/static/graph/graph.css | 1 + datafiles/static/graph/tmpl.js | 86 +++ datafiles/static/graph/vivagraph.js | 114 +++ datafiles/static/hackage.css | 4 + datafiles/templates/Html/graph.html.st | 299 ++++++++ datafiles/templates/Html/package-page.html.st | 14 + .../templates/Html/table-interface.html.st | 1 + hackage-server.cabal | 34 +- src/Distribution/Server/Features.hs | 14 +- src/Distribution/Server/Features/Core.hs | 2 + src/Distribution/Server/Features/Html.hs | 213 ++++-- .../Server/Features/Html/HtmlUtilities.hs | 22 + .../Server/Features/PackageList.hs | 62 +- .../Server/Features/PreferredVersions.hs | 17 + .../Server/Features/ReverseDependencies.hs | 420 ++++++----- .../Features/ReverseDependencies/State.hs | 662 ++++++------------ src/Distribution/Server/Framework/MemSize.hs | 10 + .../Server/Packages/PackageIndex.hs | 7 +- src/Distribution/Server/Pages/Reverse.hs | 282 ++++---- tests/RevDepCommon.hs | 61 ++ tests/ReverseDependenciesTest.hs | 225 ++++++ 22 files changed, 1735 insertions(+), 891 deletions(-) create mode 100644 benchmarks/RevDeps.hs create mode 100644 datafiles/static/graph/graph.css create mode 100644 datafiles/static/graph/tmpl.js create mode 100644 datafiles/static/graph/vivagraph.js create mode 100644 datafiles/templates/Html/graph.html.st create mode 100644 tests/RevDepCommon.hs create mode 100644 tests/ReverseDependenciesTest.hs diff --git a/benchmarks/RevDeps.hs b/benchmarks/RevDeps.hs new file mode 100644 index 000000000..25e3222c3 --- /dev/null +++ b/benchmarks/RevDeps.hs @@ -0,0 +1,76 @@ +{-# LANGUAGE ScopedTypeVariables , TypeApplications #-} +module Main where + +import Control.Monad (replicateM) +import Data.Containers.ListUtils (nubOrd) +import qualified Data.Vector as Vector +import Distribution.Package (packageName) +import Distribution.Server.Features.ReverseDependencies.State (constructReverseIndex, getDependenciesFlat) +import Distribution.Server.Packages.PackageIndex as PackageIndex + +import Gauge.Benchmark (nfAppIO, bench) +import Gauge.Main (defaultMain) +import System.Random.Stateful + +import RevDepCommon (Package(..), packToPkgInfo, TestPackage(..)) + +randomPacks + :: forall m g. StatefulGen g m + => g + -> Int + -> Vector.Vector (Package TestPackage) + -> m (Vector.Vector (Package TestPackage)) +randomPacks gen limit generated | length generated < limit = do + makeNewPack <- uniformM gen -- if not new pack, just make a new version of an existing + toInsert <- + if makeNewPack || generated == mempty + then + Package + <$> pure (TestPackage (fromIntegral @Int @Word $ Vector.length generated)) + <*> uniformRM (0, 10) gen + <*> pure mempty + else do + prevIdx <- uniformRM (0, length generated - 1) gen + let Package { pName = prevName } = generated Vector.! prevIdx + (prevNamePacks, nonPrevName) = Vector.partition ((== prevName) . pName) generated + depPacks <- + if mempty /= nonPrevName + then do + -- TODO this should have an expected amount of deps equal to what is actually on hackage. what is it? + numOfDeps <- uniformRM (1, min (length nonPrevName - 1) 7) gen + indicesMayDuplicate <- replicateM numOfDeps (uniformRM (0, length nonPrevName - 1) gen) + let indices = nubOrd indicesMayDuplicate + pure $ map (nonPrevName Vector.!) indices + else + pure [] + let + newVersion = + if mempty /= prevNamePacks + then 1 + maximum (fmap pVersion prevNamePacks) + else 0 + pure $ + Package + { pName = prevName + , pVersion = newVersion + , pDeps = map pName depPacks + } + let added = generated <> pure toInsert + randomPacks gen limit added +randomPacks _ _ generated = pure generated + +main :: IO () +main = do + packs :: Vector.Vector (Package TestPackage) <- randomPacks globalStdGen 20000 mempty + let idx = PackageIndex.fromList $ map packToPkgInfo (Vector.toList packs) + Right revs <- pure $ constructReverseIndex idx + let numPacks = length packs + defaultMain $ + (:[]) $ + bench "get transitive dependencies for one randomly selected package" $ + flip nfAppIO revs $ \revs' -> do + select <- uniformRM (0, numPacks - 1) globalStdGen + -- TODO why are there so many transitive deps? + length <$> + getDependenciesFlat + (packageName $ packToPkgInfo (packs Vector.! select)) + revs' diff --git a/datafiles/static/graph/graph.css b/datafiles/static/graph/graph.css new file mode 100644 index 000000000..4ad5cafc2 --- /dev/null +++ b/datafiles/static/graph/graph.css @@ -0,0 +1 @@ + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */.label,sub,sup{vertical-align:baseline}.search ul,hr{box-sizing:content-box}hr,img{border:0}body,figure{margin:0}.btn-group>.btn-group,.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.dropdown-menu{float:left}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.pre-scrollable{max-height:340px}.form-control-feedback,.navigation-help,.node-hover-list,.node-hover-tooltip,.steering,a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0}mark{background:#ff0;color:#000}sub,sup{font-size:75%;line-height:0;position:relative}sup{top:-.5em}sub{bottom:-.25em}img{vertical-align:middle}svg:not(:root){overflow:hidden}hr{height:0}pre,textarea{overflow:auto}code,kbd,pre,samp{font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}.glyphicon,address{font-style:normal}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{blockquote,img,pre,tr{page-break-inside:avoid}*,:after,:before{background:0 0!important;color:#000!important;box-shadow:none!important;text-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999}thead{display:table-header-group}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}.btn,.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-warning.active,.btn-warning:active,.btn.active,.btn:active,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover,.form-control,.navbar-toggle,.open>.dropdown-toggle.btn-danger,.open>.dropdown-toggle.btn-default,.open>.dropdown-toggle.btn-info,.open>.dropdown-toggle.btn-primary,.open>.dropdown-toggle.btn-warning{background-image:none}@font-face{font-family:'Glyphicons Halflings';src:url(glyphicons-halflings-regular.eot);src:url(glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(glyphicons-halflings-regular.woff2) format('woff2'),url(glyphicons-halflings-regular.woff) format('woff'),url(glyphicons-halflings-regular.ttf) format('truetype'),url(glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before,.glyphicon-btc:before,.glyphicon-xbt:before{content:"\e227"}.glyphicon-jpy:before,.glyphicon-yen:before{content:"\00a5"}.glyphicon-rub:before,.glyphicon-ruble:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*,:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}body{font-size:14px;line-height:1.42857143;color:#333}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}dt,kbd kbd,label{font-weight:700}address,blockquote .small,blockquote footer,blockquote small,dd,dt,pre{line-height:1.42857143}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{background-color:#fcf8e3;padding:.2em}.list-inline,.list-unstyled{padding-left:0;list-style:none}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}pre code,table{background-color:transparent}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}dl,ol,ul{margin-top:0}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child,ol ol,ol ul,ul ol,ul ul{margin-bottom:0}address,dl{margin-bottom:20px}ol,ul{margin-bottom:10px}.list-inline{margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.container{width:750px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;color:#777}legend,pre{display:block;color:#333}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}code,kbd{padding:2px 4px;font-size:90%}caption,th{text-align:left}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;box-shadow:none}pre{padding:9.5px;margin:0 0 10px;font-size:13px;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}.container,.container-fluid{margin-right:auto;margin-left:auto}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;border-radius:0}.container,.container-fluid{padding-left:15px;padding-right:15px}.pre-scrollable{overflow-y:scroll}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.row{margin-left:-15px;margin-right:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}caption{padding-top:8px;padding-bottom:8px;color:#777}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered,.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover,.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{overflow-x:auto;min-height:.01%}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset,legend{padding:0;border:0}fieldset{margin:0;min-width:0}legend{width:100%;margin-bottom:20px;font-size:21px;line-height:inherit;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}.form-control,output{font-size:14px;line-height:1.42857143;color:#555;display:block}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}output{padding-top:7px}.form-control{width:100%;height:34px;padding:6px 12px;background-color:#fff;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .form-control-feedback,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.form-control::-ms-expand{border:0;background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-left:-20px;margin-top:4px\9}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.checkbox-inline.disabled,.checkbox.disabled label,.radio-inline.disabled,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio label,fieldset[disabled] .radio-inline,fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0;min-height:34px}.form-control-static.input-lg,.form-control-static.input-sm{padding-left:0;padding-right:0}.form-group-sm .form-control,.input-sm{padding:5px 10px;border-radius:3px;font-size:12px}.input-sm{height:30px;line-height:1.5}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;line-height:1.5}.form-group-lg .form-control,.input-lg{border-radius:6px;padding:10px 16px;font-size:18px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;line-height:1.3333333}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;line-height:1.3333333}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center}.collapsing,.dropdown,.dropup{position:relative}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .form-control-feedback,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .form-control-feedback,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-control-static,.form-inline .form-group{display:inline-block}.form-inline .control-label,.form-inline .form-group{margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;vertical-align:middle;touch-action:manipulation;cursor:pointer;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.btn-default:hover,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary.active,.btn-primary:active,.btn-primary:hover,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success.active,.btn-success:active,.btn-success:hover,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info.active,.btn-info:active,.btn-info:hover,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.btn-warning:hover,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger.active,.btn-danger:active,.btn-danger:hover,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#337ab7;font-weight:400;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{height:0;overflow:hidden;-webkit-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;text-align:left;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu-right,.dropdown-menu.pull-right{left:auto;right:0}.dropdown-header,.dropdown-menu>li>a{display:block;padding:3px 20px;line-height:1.42857143;white-space:nowrap}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle,.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child,.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child),.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn,.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{clear:both;font-weight:400;color:#333}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;outline:0;background-color:#337ab7}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;background-color:transparent;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{font-size:12px;color:#777}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.nav-justified>.dropdown .dropdown-menu,.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn .caret,.btn-group>.btn:first-child{margin-left:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn-lg .caret{border-width:5px 5px 0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child:not(:first-child){border-radius:0 0 4px 4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.nav>li,.nav>li>a{display:block;position:relative}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li>a{padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px;margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0;border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-justified>li,.nav-stacked>li{float:none}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}.navbar-collapse{overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar{border-radius:4px}.navbar-header{float:left}.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-left:0;padding-right:0}}.embed-responsive,.modal,.modal-open,.progress{overflow:hidden}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}.navbar-static-top{z-index:1000;border-width:0 0 1px}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px;font-size:18px;line-height:20px;height:50px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}.progress-bar-striped,.progress-striped .progress-bar,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}@media (min-width:768px){.navbar-toggle{display:none}.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin:8px -15px}@media (min-width:768px){.navbar-form .form-control-static,.navbar-form .form-group{display:inline-block}.navbar-form .control-label,.navbar-form .form-group{margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}}.breadcrumb>li,.pagination{display:inline-block}.btn .badge,.btn .label{top:-1px;position:relative}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-radius:4px 4px 0 0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-nav>li>a,.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{background-color:#e7e7e7;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>li>a,.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{background-color:#080808;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#777}.pagination{padding-left:0;margin:20px 0;border-radius:4px}.pager li,.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#337ab7;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.close,.list-group-item>.badge,.pager .next>a,.pager .next>span{float:right}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;background-color:#337ab7;border-color:#337ab7;cursor:default}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.badge,.label{font-weight:700;line-height:1;white-space:nowrap;text-align:center}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;background-color:#fff;cursor:not-allowed}a.badge:focus,a.badge:hover,a.label:focus,a.label:hover{color:#fff;cursor:pointer;text-decoration:none}.label{display:inline;padding:.2em .6em .3em;font-size:75%;border-radius:.25em}.label:empty{display:none}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;color:#fff;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.media-object,.thumbnail{display:block}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.jumbotron,.jumbotron .h1,.jumbotron h1{color:inherit}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;background-color:#eee}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.alert,.thumbnail{margin-bottom:20px}.alert .alert-link,.close{font-weight:700}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px;padding-left:15px;padding-right:15px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-left:60px;padding-right:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-left:auto;margin-right:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.modal,.modal-backdrop{top:0;right:0;bottom:0;left:0}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-striped .progress-bar-info,.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{zoom:1;overflow:hidden}.media-body{width:10000px}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{text-decoration:none;color:#555;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{background-color:#eee;color:#777;cursor:not-allowed}.carousel-indicators li,.in-degree,.node-focus,.out-degree,button.close{cursor:pointer}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.panel-heading>.dropdown .dropdown-toggle,.panel-title,.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-title,.panel>.list-group,.panel>.panel-collapse>.list-group,.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-title{margin-top:0;font-size:16px}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel-group .panel-heading,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-responsive:last-child>.table:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.table-responsive:first-child>.table:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.list-group+.panel-footer,.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-left:15px;padding-right:15px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{font-size:21px;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.popover,.tooltip{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.42857143;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;text-decoration:none}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;background:0 0;border:0;-webkit-appearance:none}.modal-content,.popover{background-clip:padding-box}.modal{display:none;position:fixed;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}.tooltip.top-left .tooltip-arrow,.tooltip.top-right .tooltip-arrow{bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;text-align:left;text-align:start;font-size:12px;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px}.tooltip.top-right .tooltip-arrow{left:5px}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow,.tooltip.bottom-left .tooltip-arrow,.tooltip.bottom-right .tooltip-arrow{border-width:0 5px 5px;border-bottom-color:#000;top:0}.tooltip.bottom .tooltip-arrow{left:50%;margin-left:-5px}.tooltip.bottom-left .tooltip-arrow{right:5px;margin-top:-5px}.tooltip.bottom-right .tooltip-arrow{left:5px;margin-top:-5px}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;text-align:left;text-align:start;font-size:14px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.carousel-caption,.carousel-control{color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.6);text-align:center}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.carousel,.carousel-inner{position:relative}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.left>.arrow:after,.popover.right>.arrow:after{content:" ";bottom:-10px}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,.25)}.popover.right>.arrow:after{left:1px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;border-right-width:0;border-left-color:#fff}.carousel-inner{overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-moz-transition:-moz-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;-moz-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);left:0}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);left:0}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:.5;filter:alpha(opacity=50);font-size:20px;background-color:rgba(0,0,0,0)}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:focus,.carousel-control:hover{outline:0;color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;margin-top:-10px;z-index:5;display:inline-block}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;line-height:1;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #fff;border-radius:10px;background-color:#000\9;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#fff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px}.carousel-caption .btn,.text-hide{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{content:" ";display:table}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.hidden,.visible-lg,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;background-color:transparent;border:0}.affix{position:fixed}.steering,.steering .inner{position:absolute;top:50%;left:50%}@-ms-viewport{width:device-width}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}.visible-xs-block{display:block!important}.visible-xs-inline{display:inline!important}.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}.visible-sm-block{display:block!important}.visible-sm-inline{display:inline!important}.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}.visible-md-block{display:block!important}.visible-md-inline{display:inline!important}.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}.visible-lg-block{display:block!important}.visible-lg-inline{display:inline!important}.visible-lg-inline-block{display:inline-block!important}.hidden-lg{display:none!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}.hidden-print{display:none!important}}.steering{width:200px;height:200px;z-index:1;margin-left:-100px;margin-top:-100px;border-radius:200px;border:2px solid rgba(255,255,255,.6)}.steering .inner{width:100px;height:100px;margin-left:-50px;margin-top:-50px;border-radius:100px;border:2px solid rgba(255,255,255,.2)}.steering .steering-help{position:fixed;color:rgba(255,255,255,.9);bottom:12px;font-family:'PT Sans',sans-serif;font-size:large;left:50%;margin-left:-187px}#app,.graph-full-size,.label,.navigation-help,.node-details,.node-hover-list,.search,.window-container{position:absolute}.node-details h2,.node-details h4{margin:0;font-family:'PT Sans',sans-serif}.node-details{width:395px;left:16px;bottom:12px;padding:12px;z-index:1;font-family:'PT Sans',sans-serif;background:rgba(0,0,0,.85);border:1px solid grey;color:#fff}.node-details h4{display:block;display:-webkit-box;max-width:400px;height:50.4px;font-size:18px;line-height:1.4;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis}.node-details h2{text-align:center}.node-details .small{text-align:center;color:#999}@media (max-width:768px){.node-details{right:12px;width:inherit}.node-details .info-block{padding-left:0}.node-details .info-block a{display:inline-block;margin:0 7px}}.node-details .info-block a{font-size:18px}.github-avatar-detail{width:50px;margin-left:-16px;margin-right:8px;float:left}.search{top:12px;z-index:1}.search .search-results{padding-top:12px;background:rgba(0,0,0,.85)}.search .search-results h4{margin:0 0 5px;padding:0 0 5px 20px}.search .scroll-wrapper{border-top:1px solid grey;height:250px;overflow:hidden}.search ul{overflow-y:scroll;overflow-x:hidden;width:100%;height:100%;color:#fff;list-style:none;margin:0;padding:0 20px 0 0}.search ul li{height:25px}#app,.graph-full-size,.search ul li a{width:100%;height:100%}.search ul li a{opacity:.5;color:#fff;text-decoration:none;display:block;margin:0;padding:0 0 0 20px;line-height:25px;white-space:nowrap}.search ul li a:focus,.search ul li a:hover{text-decoration:none;opacity:1;background:rgba(26,26,26,.95)}.search .search-form input{width:100%;background:rgba(26,26,26,.5);color:#fff;border-radius:0;border:0}.search .search-form .input-group{border:1px solid grey}.search .search-form .input-group.focused,.search .search-form .input-group:hover{border-color:#fff}.search .search-form .input-group.focused button.btn,.search .search-form .input-group:hover button.btn{background:#66afe9}.search .search-form button.btn{color:#fff;border-radius:0;background:grey}.search h4{color:#fff}.search h4 .small{opacity:.5}body{background-color:#000;font-family:Roboto,sans-serif}#app{overflow:auto}.graph-full-size{top:0;left:0;overflow:hidden}h1,h2{color:#efefef}h4{font-family:'PT Sans',sans-serif}.window-container{background-color:rgba(0,0,0,.8);color:#7F7F7F}.window-list-content{overflow-y:auto;overflow-x:hidden;max-height:200px}.search-results-window{left:16px;top:47px;padding:10px;width:395px}.degree-results-window{left:16px;bottom:87px;padding:10px;width:395px}.no-overflow{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.window-title strong{color:#fff;font-weight:400;font-size:1.2em}.window-title .node-name{color:#fff}.label{color:#fff;background:#000;z-index:1}.loading{padding:10px;left:10px;top:48px}.about{padding:10px;right:10px;top:0}@media (max-width:768px){.degree-results-window{display:none}.about{padding:10px;right:13px;bottom:22px;top:inherit}}.node-hover-tooltip{position:absolute;background:rgba(0,0,0,.8);color:#fff;padding:4px 10px;min-width:200px;border:1px solid grey}.in-degree,.window-indegree{color:#90EE90}.node-hover-tooltip span{margin:0 5px}.out-degree,.window-outdegree{color:#F08080}.node-hover-list{background:#000;right:-20px;bottom:10px;height:150px;color:#fff;padding:4px;width:220px;overflow-y:scroll}.node-hover-list ul{padding:0}.node-hover-list li{list-style-type:none}.vcenter{display:inline-block;vertical-align:middle;float:none}a.reset-color,a.reset-color:focus,a.reset-color:hover{text-decoration:none;color:inherit}a.media:focus,a.media:hover{text-decoration:none;color:#FF008C;border:none}a.media{color:#fff;background-color:#000;border:none}.error{color:pink}.error-details{color:wheat}.navigation-help{background-color:rgba(0,0,0,.55);padding:7px;right:20px;top:50%;transform:translateY(-50%);font-family:Roboto,sans-serif}.navigation-help h3{color:#fff;border-bottom:1px solid #fff;padding-bottom:6px;margin-top:2px}.navigation-help code{font-family:'PT Sans',sans-serif;font-size:100%;color:#fff;background-color:inherit}.navigation-help code.important-key{color:#90EE90}.navigation-help td{color:#999;text-align:right}.navigation-help td:nth-child(even){text-align:left}.navigation-help tr.spacer-row{border-bottom:10px solid transparent} diff --git a/datafiles/static/graph/tmpl.js b/datafiles/static/graph/tmpl.js new file mode 100644 index 000000000..88968fd95 --- /dev/null +++ b/datafiles/static/graph/tmpl.js @@ -0,0 +1,86 @@ +/* + * JavaScript Templates + * https://github.com/blueimp/JavaScript-Templates + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + * + * Inspired by John Resig's JavaScript Micro-Templating: + * http://ejohn.org/blog/javascript-micro-templating/ + */ + +/*global document, define, module */ + +;(function ($) { + 'use strict' + var tmpl = function (str, data) { + var f = !/[^\w\-\.:]/.test(str) + ? tmpl.cache[str] = tmpl.cache[str] || tmpl(tmpl.load(str)) + : new Function(// eslint-disable-line no-new-func + tmpl.arg + ',tmpl', + 'var _e=tmpl.encode' + tmpl.helper + ",_s='" + + str.replace(tmpl.regexp, tmpl.func) + "';return _s;" + ) + return data ? f(data, tmpl) : function (data) { + return f(data, tmpl) + } + } + tmpl.cache = {} + tmpl.load = function (id) { + return document.getElementById(id).innerHTML + } + tmpl.regexp = /([\s'\\])(?!(?:[^{]|\{(?!%))*%\})|(?:\{%(=|#)([\s\S]+?)%\})|(\{%)|(%\})/g + tmpl.func = function (s, p1, p2, p3, p4, p5) { + if (p1) { // whitespace, quote and backspace in HTML context + return { + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + ' ': ' ' + }[p1] || '\\' + p1 + } + if (p2) { // interpolation: {%=prop%}, or unescaped: {%#prop%} + if (p2 === '=') { + return "'+_e(" + p3 + ")+'" + } + return "'+(" + p3 + "==null?'':" + p3 + ")+'" + } + if (p4) { // evaluation start tag: {% + return "';" + } + if (p5) { // evaluation end tag: %} + return "_s+='" + } + } + tmpl.encReg = /[<>&"'\x00]/g + tmpl.encMap = { + '<': '<', + '>': '>', + '&': '&', + '"': '"', + "'": ''' + } + tmpl.encode = function (s) { + return (s == null ? '' : '' + s).replace( + tmpl.encReg, + function (c) { + return tmpl.encMap[c] || '' + } + ) + } + tmpl.arg = 'o' + tmpl.helper = ",print=function(s,e){_s+=e?(s==null?'':s):_e(s);}" + + ',include=function(s,d){_s+=tmpl(s,d);}' + if (typeof define === 'function' && define.amd) { + define(function () { + return tmpl + }) + } else if (typeof module === 'object' && module.exports) { + module.exports = tmpl + } else { + $.tmpl = tmpl + } +}(this)) diff --git a/datafiles/static/graph/vivagraph.js b/datafiles/static/graph/vivagraph.js new file mode 100644 index 000000000..673125611 --- /dev/null +++ b/datafiles/static/graph/vivagraph.js @@ -0,0 +1,114 @@ +!function(S){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=S();else if("function"==typeof define&&define.amd)define([],S);else{var e;"undefined"!=typeof window?e=window:"undefined"!=typeof global?e=global:"undefined"!=typeof self&&(e=self);e.Viva=S()}}(function(){return function e(g,l,d){function m(a,c){if(!l[a]){if(!g[a]){var f="function"==typeof require&&require;if(!c&&f)return f(a,!0);if(b)return b(a,!0);f=Error("Cannot find module '"+a+"'");throw f.code="MODULE_NOT_FOUND", +f;}f=l[a]={exports:{}};g[a][0].call(f.exports,function(f){var c=g[a][1][f];return m(c?c:f)},f,f.exports,e,g,l,d)}return l[a].exports}for(var b="function"==typeof require&&require,c=0;ce?e-0:e+0)/b;e=k*r-n*a;x.y=(0>e?e-0:e+0)/b;return x}},{}],3:[function(e,g,l){g.exports.degree=e("./src/degree.js");g.exports.betweenness=e("./src/betweenness.js")},{"./src/betweenness.js":4,"./src/degree.js":5}],4:[function(e,g,l){g.exports=function(d,m){function b(a){r[a]/=2}function c(a){e[a.id]=0}function a(a){function c(a){a=a.id;-1===n[a]&&(n[a]=n[b]+1,p.push(a));n[a]===n[b]+1&&(k[a]+=k[b],h[a].push(b))}d.forEachNode(function(a){a=a.id;h[a]=[];n[a]=-1;k[a]=0}); +n[a]=0;k[a]=1;for(p.push(a);p.length;){var b=p.shift();Object.create(null);f.push(b);d.forEachLinkedNode(b,c,m)}}var p=[],f=[],h=Object.create(null),n=Object.create(null),k=Object.create(null),e=Object.create(null),g,r=Object.create(null);d.forEachNode(function(a){r[a.id]=0});d.forEachNode(function(b){g=b.id;a(g);for(d.forEachNode(c);f.length;){b=f.pop();for(var p=(1+e[b])/k[b],t=h[b],n=0;na)))return k.splice(a,1),0===k.length&&r.reset(),!0},addSpring:function(a,b,c,f,h){if(!a||!b)throw Error("Cannot add null spring to force simulator");"number"!==typeof c&&(c= +-1);a=new m(a,b,c,0<=h?h:-1,f);g.push(a);return a},removeSpring:function(a){if(a&&(a=g.indexOf(a),-1h&&(h=k.pos.x); +k.pos.yn&&(n=k.pos.y)}c.x1=b;c.x2=h;c.y1=f;c.y2=n}},reset:function(){c.x1=c.y1=0;c.x2=c.y2=0},getBestNewPosition:function(a){var p=0,f=0;if(a.length){for(var h=0;h +a.length?d.springLength:a.length,n=f.pos.x-c.pos.x,k=f.pos.y-c.pos.y,m=Math.sqrt(n*n+k*k);0===m&&(n=(b.nextDouble()-.5)/50,k=(b.nextDouble()-.5)/50,m=Math.sqrt(n*n+k*k));a=(!a.coeff||0>a.coeff?d.springCoeff:a.coeff)*(m-h)/m*a.weight;c.force.x+=a*n;c.force.y+=a*k;f.force.x-=a*n;f.force.y-=a*k}};c(d,m,["springCoeff","springLength"]);return m}},{"ngraph.expose":15,"ngraph.merge":24,"ngraph.random":25}],15:[function(e,g,l){function d(d,b,c){d.hasOwnProperty(c)&&"function"!==typeof b[c]&&(b[c]=function(a){return void 0!== +a?(d[c]=a,b):d[c]})}g.exports=function(m,b,c){if("[object Array]"===Object.prototype.toString.call(c))for(var a=0;ad&&(d=e);lk&&(k=l)}p=d-c;e=k-f;p>e?k=f+p:d=c+e;g=0;r=q();r.left=c;r.right=d;r.top=f;r.bottom=k;p=n-1;0n&&(d+=1,D=k,k=n,n+=n-D);v>l&&(d+=2,v=e,e=l,l+=l-v);(v=0===d?c.quad0:1===d?c.quad1: +2===d?c.quad2:3===d?c.quad3:null)?h.push(v,f):(v=q(),v.left=k,v.top=e,v.right=n,v.bottom=l,v.body=f,f=d,d=v,0===f?c.quad0=d:1===f?c.quad1=d:2===f?c.quad2=d:3===f&&(c.quad3=d))}},updateBodyForce:function(a){var b,c,d,h,k=0,e=0,g=1,C=0,q=1;for(f[0]=r;g;){b=f[C];var l=b.body,g=g-1,C=C+1;c=l!==a;l&&c?(c=l.pos.x-a.pos.x,d=l.pos.y-a.pos.y,h=Math.sqrt(c*c+d*d),0===h&&(c=(m.nextDouble()-.5)/50,d=(m.nextDouble()-.5)/50,h=Math.sqrt(c*c+d*d)),b=p*l.mass*a.mass/(h*h*h),k+=b*c,e+=b*d):c&&(c=b.massX/b.mass-a.pos.x, +d=b.massY/b.mass-a.pos.y,h=Math.sqrt(c*c+d*d),0===h&&(c=(m.nextDouble()-.5)/50,d=(m.nextDouble()-.5)/50,h=Math.sqrt(c*c+d*d)),(b.right-b.left)/hMath.abs(d.x-e.x)&&1E-8>b}},{}],20:[function(e,g,l){g.exports=function(){this.quad3=this.quad2=this.quad1=this.quad0=this.body=null;this.right=this.bottom=this.top=this.left=this.massY=this.massX=this.mass=0}},{}],21:[function(e,g,l){function d(b){return b}g.exports=function(b,c,a){c=c||d;a=a||d;b="string"===typeof b?JSON.parse(b):b;var p=m(),f;if(void 0===b.links||void 0===b.nodes)throw Error("Cannot load graph without links and nodes");for(f=0;fb)throw Error("Invalid number of nodes");var c=m(),a;for(a=0;ab)throw Error("At least two nodes are expected for complete graph");var c=m(),a,d;for(a=0;ab||0>c)throw Error("Graph dimensions are invalid. Number of nodes in each partition should be greater than 0");var a=m(),d,f;for(d=0;d +b)throw Error("Invalid number of nodes in balanced tree");var c=m(),a=Math.pow(2,b);0===b&&c.addNode(1);for(b=1;bb)throw Error("Invalid number of nodes");var c=m(),a;c.addNode(0);for(a=1;ab)throw Error("Invalid number of nodes");var c=d(b);c.addLink(0,b-1);c.addLink(b,2*b-1);return c},grid:function(b,c){if(1>b||1>c)throw Error("Invalid number of nodes in grid graph"); +var a=m(),d,f;if(1===b&&1===c)return a.addNode(0),a;for(d=0;db||1>c||1>a)throw Error("Invalid number of nodes in grid3 graph");var d=m(),f,h,n;if(1===b&&1===c&&1===a)return d.addNode(0),d;for(n=0;n +b)throw Error("Number of nodes shoul be >= 0");var c=m(),a;for(a=0;a=b)throw Error("Choose smaller `k`. It cannot be larger than number of nodes `n`");d=e("ngraph.random").random(d||42);var f=m(),h,n;for(h=0;hb)return!1;G();B.splice(b,1);var c=u[a.fromId],f=u[a.toId];c&&(b=d(a,c.links),0<=b&&c.links.splice(b,1));f&&(b=d(a,f.links),0<=b&&f.links.splice(b,1));M(a,"remove");L();return!0}function r(a,b){var c=u[a],d;if(!c)return null;for(d=0;d>>19)&4294967295;b=b+374761393+(b<<5)&4294967295;b=(b+3550635116^b<<9)&4294967295;b=b+4251993797+(b<<3)&4294967295;b=(b^3042594569^b>>>16)&4294967295;return(b&268435455)/268435456};return{next:function(a){return Math.floor(c()*a)},nextDouble:function(){return c()}}}g.exports= +{random:d,randomIterator:function(e,b){var c=b||d();if("function"!==typeof c.next)throw Error("customRandom does not match expected API: next() function is missing");return{forEach:function(a){var b,d,h;for(b=e.length-1;0m.indexOf(".")))throw Error("simplesvg currently does not support nested bindings");(a=d[m])?a.push(e):a=d[m]=[e]}}function b(a,b){function d(b){a.nodeValue=b[n]}var e=a.nodeValue;if(e&&(e=e.match(c))){var n=e[1];n.indexOf(".");(e=b[n])?e.push(d):e=b[n]=[d]}}g.exports=function(a){var b=Object.create(null);d(a,b);return{link:function(a){function c(b){b(a)} +Object.keys(b).forEach(function(a){b[a].forEach(c)})}}};var c=/{{(.+?)}}/},{}],30:[function(e,g,l){function d(){throw Error("DOMParser is not supported by this platform. Please open issue here https://github.com/anvaka/simplesvg");}e="undefined"===typeof DOMParser?{parseFromString:d}:new DOMParser;g.exports=e},{}],31:[function(e,g,l){function d(d,e,n,k){p=p||(document.addEventListener?{add:m,rm:b}:{add:c,rm:a});return p.add(d,e,n,k)}function m(a,b,c,d){a.addEventListener(b,c,d)}function b(a,b,c,d){a.removeEventListener(b, +c,d)}function c(a,b,c,d){if(d)throw Error("cannot useCapture in oldIE");a.attachEvent("on"+b,c)}function a(a,b,c,d){a.detachEvent("on"+b,c)}d.removeEventListener=function(d,e,n,k){p=p||(document.addEventListener?{add:m,rm:b}:{add:c,rm:a});return p.rm(d,e,n,k)};d.addEventListener=d;g.exports=d;var p=null},{}],32:[function(e,g,l){function d(a){a=c.betweenness(a);return b(a)}function m(a,d){var f=c.degree(a,d);return b(f)}function b(a){return Object.keys(a).sort(function(b,c){return a[c]-a[b]}).map(function(b){return{key:b, +value:a[b]}})}var c=e("ngraph.centrality");g.exports=function(){return{betweennessCentrality:d,degreeCentrality:m}}},{"ngraph.centrality":3}],33:[function(e,g,l){g.exports=function(){return{density:function(d,e){var b=d.getNodesCount();return 0===b?NaN:e?d.getLinksCount()/(b*(b-1)):2*d.getLinksCount()/(b*(b-1))}}}},{}],34:[function(e,g,l){g.exports=function(e,b){var c={};return{bindDragNDrop:function(a,e){var f;if(e){f=b.getNodeUI(a.id);f=d(f);if("function"===typeof e.onStart)f.onStart(e.onStart); +if("function"===typeof e.onDrag)f.onDrag(e.onDrag);if("function"===typeof e.onStop)f.onStop(e.onStop);c[a.id]=f}else if(f=c[a.id])f.release(),delete c[a.id]}}};var d=e("./dragndrop.js")},{"./dragndrop.js":35}],35:[function(e,g,l){g.exports=function(c){var a,e,f,h,n,k,g=0,q=0,l,x=!1,y=0,t=function(a){a.stopPropagation?a.stopPropagation():a.cancelBubble=!0},z=function(a){t(a);return!1},A=function(a){a=a||window.event;var b=a.clientX,c=a.clientY;e&&e(a,{x:b-g,y:c-q});g=b;q=c},u=function(b){b=b||window.event; +if(x)return t(b),!1;if(1===b.button&&null!==window.event||0===b.button)return g=b.clientX,q=b.clientY,l=b.target||b.srcElement,a&&a(b,{x:g,y:q}),d.on("mousemove",A),d.on("mouseup",B),t(b),n=window.document.onselectstart,k=window.document.ondragstart,window.document.onselectstart=z,l.ondragstart=z,!1},B=function(a){a=a||window.event;d.off("mousemove",A);d.off("mouseup",B);window.document.onselectstart=n;l.ondragstart=k;l=null;f&&f(a)},F=function(a){if("function"===typeof h){a=a||window.event;a.preventDefault&& +a.preventDefault();a.returnValue=!1;var d,f=0,e=0;d=a||window.event;if(d.pageX||d.pageY)f=d.pageX,e=d.pageY;else if(d.clientX||d.clientY)f=d.clientX+window.document.body.scrollLeft+window.document.documentElement.scrollLeft,e=d.clientY+window.document.body.scrollTop+window.document.documentElement.scrollTop;d=[f,e];f=b(c);h(a,a.wheelDelta?a.wheelDelta/360:a.detail/-9,{x:d[0]-f[0],y:d[1]-f[1]})}},H=function(a){!h&&a?"webkit"===m.browser?c.addEventListener("mousewheel",F,!1):c.addEventListener("DOMMouseScroll", +F,!1):h&&!a&&("webkit"===m.browser?c.removeEventListener("mousewheel",F,!1):c.removeEventListener("DOMMouseScroll",F,!1));h=a},D=function(a,b){return(a.clientX-b.clientX)*(a.clientX-b.clientX)+(a.clientY-b.clientY)*(a.clientY-b.clientY)},v=function(a){if(1===a.touches.length){t(a);var b=a.touches[0],c=b.clientX,b=b.clientY;e&&e(a,{x:c-g,y:b-q});g=c;q=b}else 2===a.touches.length&&(c=D(a.touches[0],a.touches[1]),b=0,cy&&(b=1),h(a,b,{x:a.touches[0].clientX,y:a.touches[0].clientY}),y=c,t(a), +a.preventDefault&&a.preventDefault())},w=function(a){x=!1;d.off("touchmove",v);d.off("touchend",w);d.off("touchcancel",w);l=null;f&&f(a)},E=function(b){if(1===b.touches.length){var c=b.touches[0];t(b);b.preventDefault&&b.preventDefault();g=c.clientX;q=c.clientY;l=b.target||b.srcElement;a&&a(b,{x:g,y:q});x||(x=!0,d.on("touchmove",v),d.on("touchend",w),d.on("touchcancel",w))}else 2===b.touches.length&&(t(b),b.preventDefault&&b.preventDefault(),y=D(b.touches[0],b.touches[1]))};c.addEventListener("mousedown", +u);c.addEventListener("touchstart",E);return{onStart:function(b){a=b;return this},onDrag:function(a){e=a;return this},onStop:function(a){f=a;return this},onScroll:function(a){H(a);return this},release:function(){c.removeEventListener("mousedown",u);c.removeEventListener("touchstart",E);d.off("mousemove",A);d.off("mouseup",B);d.off("touchmove",v);d.off("touchend",w);d.off("touchcancel",w);H(null)}}};var d=e("../Utils/documentEvents.js"),m=e("../Utils/browserInfo.js"),b=e("../Utils/findElementPosition.js")}, +{"../Utils/browserInfo.js":39,"../Utils/documentEvents.js":40,"../Utils/findElementPosition.js":41}],36:[function(e,g,l){g.exports=function(e,b){var c=d(b),a=null,g={},f={x:0,y:0};c.mouseDown(function(b,d){a=b;f.x=d.clientX;f.y=d.clientY;c.mouseCapture(a);var e=g[b.id];if(e&&e.onStart)e.onStart(d,f);return!0}).mouseUp(function(b){c.releaseMouseCapture(a);a=null;if((b=g[b.id])&&b.onStop)b.onStop();return!0}).mouseMove(function(b,c){if(a){var d=g[a.id];if(d&&d.onDrag)d.onDrag(c,{x:c.clientX-f.x,y:c.clientY- +f.y});f.x=c.clientX;f.y=c.clientY;return!0}});return{bindDragNDrop:function(a,b){(g[a.id]=b)||delete g[a.id]}}};var d=e("../WebGL/webglInputEvents.js")},{"../WebGL/webglInputEvents.js":57}],37:[function(e,g,l){g.exports=function(c,a){a=d(a,{maxX:1024,maxY:1024,seed:"Deterministic randomness made me do this"});var e=m(a.seed),f=new b(Number.MAX_VALUE,Number.MAX_VALUE,Number.MIN_VALUE,Number.MIN_VALUE),h={},g=function(b){return{x:e.next(a.maxX),y:e.next(a.maxY)}},k="function"===typeof Object.create? +Object.create(null):{},l=function(a){k[a.id]=g(a);a=k[a.id];a.xf.x2&&(f.x2=a.x);a.yf.y2&&(f.y2=a.y)},q=function(){0!==c.getNodesCount()&&(f.x1=Number.MAX_VALUE,f.y1=Number.MAX_VALUE,f.x2=Number.MIN_VALUE,f.y2=Number.MIN_VALUE,c.forEachNode(l))},r=function(a){h[a.id]=a},x=function(a){for(var b=0;be.indexOf("compatible")&&m.exec(e)||[];e={browser:e[1]||"",version:e[2]||"0"}}else e={browser:"",version:"0"};g.exports=e},{}],40:[function(e,g,l){e("./nullEvents.js");g.exports={on:function(d,e){document.addEventListener(d,e)},off:function(d,e){document.removeEventListener(d, +e)}}},{"./nullEvents.js":44}],41:[function(e,g,l){g.exports=function(d){var e=0,b=0;if(d.offsetParent){do e+=d.offsetLeft,b+=d.offsetTop;while(null!==(d=d.offsetParent))}return[e,b]}},{}],42:[function(e,g,l){g.exports=function(d){if(!d)throw{message:"Cannot get dimensions of undefined container"};return{left:0,top:0,width:d.clientWidth,height:d.clientHeight}}},{}],43:[function(e,g,l){var d=e("gintersect");g.exports=function(e,b,c,a,g,f,h,n){return d(e,b,e,a,g,f,h,n)||d(e,a,c,a,g,f,h,n)||d(c,a,c,b, +g,f,h,n)||d(c,b,e,b,g,f,h,n)}},{gintersect:2}],44:[function(e,g,l){function d(){}g.exports={on:d,off:d,stop:d}},{}],45:[function(e,g,l){g.exports=function(d,e,b,c){this.x1=d||0;this.y1=e||0;this.x2=b||0;this.y2=c||0}},{}],46:[function(e,g,l){(function(d){function e(){}g.exports=function(){function b(b){var c=(new Date).getTime(),d=Math.max(0,16-(c-a)),e=h.setTimeout(function(){b(c+d)},d);a=c+d;return e}function c(a){h.clearTimeout(a)}var a=0,g=["ms","moz","webkit","o"],f,h;h="undefined"!==typeof window? +window:"undefined"!==typeof d?d:{setTimeout:e,clearTimeout:e};for(f=0;fb,c)}));e.forEachNode(A);e.off("changed",u);e.on("changed",u)}function D(){L=!1;e.off("changed",u);N&&(N.release(),N=null);c.off("resize",B);R.off();G.stop();e.forEachLink(function(a){k.renderLinks&&w.releaseLink(a)});e.forEachNode(function(a){I.bindDragNDrop(a,null);w.releaseNode(a)});v.dispose();w.release(E)}k=k||{};var v=k.layout,w=k.graphics,E=k.container,M=void 0!==k.interactive?k.interactive:!0,I,G,L= +!1,K=!0,O=!1,P=!1,Q=!1,J={offsetX:0,offsetY:0,scale:1},R=d({}),N;return{run:function(c){if(!L){E=E||window.document.body;v=v||m(e,{springLength:80,springCoeff:2E-4});w=w||b(e,{container:E});k.hasOwnProperty("renderLinks")||(k.renderLinks=!0);k.prerender=k.prerender||0;I=(w.inputManager||a)(e,w);if("number"===typeof k.prerender&&0a.id&&(a=a.id,c=B[b],B[b]=B[a],B[b].id=b,B[a]=c,B[a].id=a)},graphCenterChanged:function(a,b){t[12]=2*a/q-1;t[13]=1-2*b/r;I()},addLink:function(a,b){var c=y++,d=M(a);d.id=c;d.pos=b;v.createLink(d);B[c]=d;return D[a.id]=d},addNode:function(a,b){var c=x++,d=E(a);d.id=c;d.position=b;d.node=a;w.createNode(d);u[c]=d;return H[a.id]=d},translateRel:function(a,b){t[12]+=2*t[0]*a/q/t[0];t[13]-=2*t[5]*b/r/t[5];I()},scale:function(a,b){var c=2*b.x/q-1,d=1-2*b.y/r,c=c- +t[12],d=d-t[13];t[12]+=c*(1-a);t[13]+=d*(1-a);t[0]*=a;t[5]*=a;I();this.fire("rescaled");return t[0]},resetScale:function(){G();l&&(L(),I());return this},init:function(a){var b={};e.preserveDrawingBuffer&&(b.preserveDrawingBuffer=!0);g=a;L();G();g.appendChild(k);l=k.getContext("experimental-webgl",b);if(!l)throw window.alert("Could not initialize WebGL. Seems like the browser doesn't support it."),"Could not initialize WebGL. Seems like the browser doesn't support it.";e.enableBlending&&(l.blendFunc(l.SRC_ALPHA, +l.ONE_MINUS_SRC_ALPHA),l.enable(l.BLEND));e.clearColor&&(a=e.clearColorValue,l.clearColor(a.r,a.g,a.b,a.a),this.beginRender=function(){l.clear(l.COLOR_BUFFER_BIT)});v.load(l);v.updateSize(q/2,r/2);w.load(l);w.updateSize(q/2,r/2);I();"function"===typeof F&&F(k)},release:function(a){k&&a&&a.removeChild(k)},isSupported:function(){var a=window.document.createElement("canvas");return a&&a.getContext&&a.getContext("experimental-webgl")},releaseLink:function(a){0a.length?(b=new Float32Array(a.length*c*2),b.set(a),b):a},copyArrayPart:d,swapArrayPart:m,getLocations:function(a,c){for(var d= +{},e=0;e= +q.length){var l=new d(g*f);q.push(l)}l=q[c.textureNumber];l.ctx.drawImage(k,c.col*f,c.row*f,f,f);r[m]=k.src;n[k.src]=h;l.isDirty=!0;e(h)};k.src=c}}};return x}},{"./texture.js":52}],55:[function(e,g,l){g.exports=function(d,e){return{_texture:0,_offset:0,size:"number"===typeof d?d:32,src:e}}},{}],56:[function(e,g,l){var d=e("./webglAtlas.js"),m=e("./webgl.js");g.exports=function(){var b,c,a,e,f,h,g=0,k=new Float32Array(64),l,q,r,x;return{load:function(g){a=g;f=m(g);b=new d(1024);c=f.createProgram("attribute vec2 a_vertexPos;\nattribute float a_customAttributes;\nuniform vec2 u_screenSize;\nuniform mat4 u_transform;\nuniform float u_tilesPerTexture;\nvarying vec3 vTextureCoord;\nvoid main(void) {\n gl_Position = u_transform * vec4(a_vertexPos/u_screenSize, 0, 1);\nfloat corner = mod(a_customAttributes, 4.);\nfloat tileIndex = mod(floor(a_customAttributes / 4.), u_tilesPerTexture);\nfloat tilesPerRow = sqrt(u_tilesPerTexture);\nfloat tileSize = 1./tilesPerRow;\nfloat tileColumn = mod(tileIndex, tilesPerRow);\nfloat tileRow = floor(tileIndex/tilesPerRow);\nif(corner == 0.0) {\n vTextureCoord.xy = vec2(0, 1);\n} else if(corner == 1.0) {\n vTextureCoord.xy = vec2(1, 1);\n} else if(corner == 2.0) {\n vTextureCoord.xy = vec2(0, 0);\n} else {\n vTextureCoord.xy = vec2(1, 0);\n}\nvTextureCoord *= tileSize;\nvTextureCoord.x += tileColumn * tileSize;\nvTextureCoord.y += tileRow * tileSize;\nvTextureCoord.z = floor(floor(a_customAttributes / 4.)/u_tilesPerTexture);\n}", +"precision mediump float;\nvarying vec4 color;\nvarying vec3 vTextureCoord;\nuniform sampler2D u_sampler0;\nuniform sampler2D u_sampler1;\nuniform sampler2D u_sampler2;\nuniform sampler2D u_sampler3;\nvoid main(void) {\n if (vTextureCoord.z == 0.) {\n gl_FragColor = texture2D(u_sampler0, vTextureCoord.xy);\n } else if (vTextureCoord.z == 1.) {\n gl_FragColor = texture2D(u_sampler1, vTextureCoord.xy);\n } else if (vTextureCoord.z == 2.) {\n gl_FragColor = texture2D(u_sampler2, vTextureCoord.xy);\n } else if (vTextureCoord.z == 3.) {\n gl_FragColor = texture2D(u_sampler3, vTextureCoord.xy);\n } else { gl_FragColor = vec4(0, 1, 0, 1); }\n}"); +a.useProgram(c);h=f.getLocations(c,"a_vertexPos a_customAttributes u_screenSize u_transform u_sampler0 u_sampler1 u_sampler2 u_sampler3 u_tilesPerTexture".split(" "));a.uniform1f(h.tilesPerTexture,1024);a.enableVertexAttribArray(h.vertexPos);a.enableVertexAttribArray(h.customAttributes);e=a.createBuffer()},position:function(a,b){var c=18*a.id;k[c]=b.x-a.size;k[c+1]=b.y-a.size;k[c+2]=4*a._offset;k[c+3]=b.x+a.size;k[c+4]=b.y-a.size;k[c+5]=4*a._offset+1;k[c+6]=b.x-a.size;k[c+7]=b.y+a.size;k[c+8]=4*a._offset+ +2;k[c+9]=b.x-a.size;k[c+10]=b.y+a.size;k[c+11]=4*a._offset+2;k[c+12]=b.x+a.size;k[c+13]=b.y-a.size;k[c+14]=4*a._offset+1;k[c+15]=b.x+a.size;k[c+16]=b.y+a.size;k[c+17]=4*a._offset+3},createNode:function(a){k=f.extendArray(k,g,18);g+=1;var c=b.getCoordinates(a.src);c?a._offset=c.offset:(a._offset=0,b.load(a.src,function(b){a._offset=b.offset}))},removeNode:function(a){0d-H&&f[0]===z?g(x,f):g(r,f),H=d,g(C,f)&&c(a))})})(e.getGraphicsRoot());var z={mouseEnter:function(a){"function"=== +typeof a&&h.push(a);return z},mouseLeave:function(a){"function"===typeof a&&l.push(a);return z},mouseDown:function(a){"function"===typeof a&&k.push(a);return z},mouseUp:function(a){"function"===typeof a&&C.push(a);return z},mouseMove:function(a){"function"===typeof a&&q.push(a);return z},click:function(a){"function"===typeof a&&r.push(a);return z},dblClick:function(a){"function"===typeof a&&x.push(a);return z},mouseCapture:function(a){f=a},releaseMouseCapture:function(){f=null}};return e.webglInputEvents= +z}},{"../Utils/documentEvents.js":40}],58:[function(e,g,l){var d=e("./parseColor.js");g.exports=function(e){return{color:d(e)}}},{"./parseColor.js":51}],59:[function(e,g,l){var d=e("./webgl.js");g.exports=function(){var e=2*(2*Float32Array.BYTES_PER_ELEMENT+Uint32Array.BYTES_PER_ELEMENT),b,c,a,g,f,h=0,l,k=new ArrayBuffer(16*e),C=new Float32Array(k),q=new Uint32Array(k),r,x,y,t;return{load:function(e){c=e;g=d(e);b=g.createProgram("attribute vec2 a_vertexPos;\nattribute vec4 a_color;\nuniform vec2 u_screenSize;\nuniform mat4 u_transform;\nvarying vec4 color;\nvoid main(void) {\n gl_Position = u_transform * vec4(a_vertexPos/u_screenSize, 0.0, 1.0);\n color = a_color.abgr;\n}", +"precision mediump float;\nvarying vec4 color;\nvoid main(void) {\n gl_FragColor = color;\n}");c.useProgram(b);f=g.getLocations(b,["a_vertexPos","a_color","u_screenSize","u_transform"]);c.enableVertexAttribArray(f.vertexPos);c.enableVertexAttribArray(f.color);a=c.createBuffer()},position:function(a,b,c){var d=6*a.id;C[d]=b.x;C[d+1]=b.y;q[d+2]=a.color;C[d+3]=c.x;C[d+4]=c.y;q[d+5]=a.color},createLink:function(a){if((h+1)*e>k.byteLength){var b=new ArrayBuffer(2*k.byteLength),c=new Float32Array(b), +d=new Uint32Array(b);d.set(q);C=c;q=d;k=b}h+=1;l=a.id},removeLink:function(a){0a.id&&g.swapArrayPart(C,6*a.id,6*l,6);0=h.byteLength){var a=new ArrayBuffer(2*h.byteLength),b=new Float32Array(a),c=new Uint32Array(a);c.set(k);l=b;k=c;h=a}C+=1},replaceProperties:function(){},render:function(){c.useProgram(b);c.bindBuffer(c.ARRAY_BUFFER,a);c.bufferData(c.ARRAY_BUFFER,h,c.DYNAMIC_DRAW);y&&(y=!1,c.uniformMatrix4fv(g.transform,!1,x),c.uniform2f(g.screenSize,q,r));c.vertexAttribPointer(g.vertexPos,3,c.FLOAT,!1,4*Float32Array.BYTES_PER_ELEMENT,0);c.vertexAttribPointer(g.color, +4,c.UNSIGNED_BYTE,!0,4*Float32Array.BYTES_PER_ELEMENT,12);c.drawArrays(c.POINTS,0,C)}}}},{"./webgl.js":53}],61:[function(e,g,l){var d=e("./parseColor.js");g.exports=function(e,b){return{size:"number"===typeof e?e:10,color:d(b)}}},{"./parseColor.js":51}],62:[function(e,g,l){g.exports="0.8.1"},{}]},{},[1])(1)}); diff --git a/datafiles/static/hackage.css b/datafiles/static/hackage.css index 8d5f1dc41..112d57596 100644 --- a/datafiles/static/hackage.css +++ b/datafiles/static/hackage.css @@ -991,6 +991,10 @@ table.fancy th, table.properties th { padding: 0.15em 0.45em; } +table.fancy tr.even td { + background-color: #eee; +} + table.dataTable.compact.fancy tbody th, table.dataTable.compact.fancy tbody td { padding: 6px 10px; diff --git a/datafiles/templates/Html/graph.html.st b/datafiles/templates/Html/graph.html.st new file mode 100644 index 000000000..205d1680c --- /dev/null +++ b/datafiles/templates/Html/graph.html.st @@ -0,0 +1,299 @@ + + + + + Hackage Dependencies Graph + + ` + + + + + + +
+
+ +
+ +
+
+
+
+ + + + + + + + + + + diff --git a/datafiles/templates/Html/package-page.html.st b/datafiles/templates/Html/package-page.html.st index f980ad20e..a85f11ab8 100644 --- a/datafiles/templates/Html/package-page.html.st +++ b/datafiles/templates/Html/package-page.html.st @@ -225,6 +225,20 @@ $endif$ + $if(hasrdeps)$ + + Reverse Dependencies + $rdeps$ + + + $endif$ + $if(hasexecs)$ Executables diff --git a/datafiles/templates/Html/table-interface.html.st b/datafiles/templates/Html/table-interface.html.st index 8c75d3ecf..f08fb67eb 100644 --- a/datafiles/templates/Html/table-interface.html.st +++ b/datafiles/templates/Html/table-interface.html.st @@ -24,6 +24,7 @@
Name
DLs
Rating
+
Rev Deps
Description
Tags
Last U/L
diff --git a/hackage-server.cabal b/hackage-server.cabal index 4f99bd94f..73aa484b8 100644 --- a/hackage-server.cabal +++ b/hackage-server.cabal @@ -237,7 +237,7 @@ library lib-server Distribution.Server.Pages.Package.HaddockParse Distribution.Server.Pages.Recent Distribution.Server.Pages.AdminLog - -- [reverse index disabled] Distribution.Server.Pages.Reverse + Distribution.Server.Pages.Reverse Distribution.Server.Pages.Template Distribution.Server.Pages.Util @@ -353,8 +353,8 @@ library lib-server Distribution.Server.Features.PreferredVersions Distribution.Server.Features.PreferredVersions.State Distribution.Server.Features.PreferredVersions.Backup - -- [reverse index disabled] Distribution.Server.Features.ReverseDependencies - -- [reverse index disabled] Distribution.Server.Features.ReverseDependencies.State + Distribution.Server.Features.ReverseDependencies + Distribution.Server.Features.ReverseDependencies.State Distribution.Server.Features.Tags Distribution.Server.Features.Tags.Backup Distribution.Server.Features.Tags.State @@ -388,6 +388,7 @@ library lib-server , base16-bytestring ^>= 1.0 -- requires bumping http-io-streams , base64-bytestring ^>= 1.2.1.0 + , bimap ^>= 0.3 --NOTE: blaze-builder-0.4 is now a compat package that uses bytestring-0.10 builder , blaze-builder ^>= 0.4 , blaze-html ^>= 0.9 @@ -400,6 +401,7 @@ library lib-server , cryptohash-sha256 ^>= 0.11.100 , csv ^>= 0.1 , ed25519 ^>= 0.0.5 + , exceptions ^>= 0.10 , hackage-security >= 0.6 && < 0.7 -- N.B: hackage-security-0.6.2 uses Cabal-syntax-3.8.1.0 -- see https://github.com/haskell/hackage-server/issues/1130 @@ -546,6 +548,32 @@ test-suite HighLevelTest , io-streams ^>= 1.5.0.1 , http-io-streams ^>= 0.1.6.1 +test-suite ReverseDependenciesTest + import: test-defaults + type: exitcode-stdio-1.0 + main-is: ReverseDependenciesTest.hs + build-tool-depends: hackage-server:hackage-server + build-depends: + , tasty ^>= 1.4 + , tasty-hunit ^>= 0.10 + , HUnit ^>= 1.6 + , hedgehog ^>= 1.1 + , exceptions + , bimap + other-modules: RevDepCommon + +benchmark RevDeps + import: test-defaults + type: exitcode-stdio-1.0 + hs-source-dirs: tests, benchmarks + main-is: RevDeps.hs + build-tool-depends: hackage-server:hackage-server + build-depends: + , random ^>= 1.2 + , gauge + ghc-options: -with-rtsopts=-s + other-modules: RevDepCommon + test-suite PaginationTest import: test-defaults type: exitcode-stdio-1.0 diff --git a/src/Distribution/Server/Features.hs b/src/Distribution/Server/Features.hs index 6b2a6a0e4..5b1995b04 100644 --- a/src/Distribution/Server/Features.hs +++ b/src/Distribution/Server/Features.hs @@ -33,7 +33,7 @@ import Distribution.Server.Features.BuildReports (initBuildReportsFeature import Distribution.Server.Features.PackageInfoJSON (initPackageInfoJSONFeature) import Distribution.Server.Features.LegacyRedirects (legacyRedirectsFeature) import Distribution.Server.Features.PreferredVersions (initVersionsFeature) --- [reverse index disabled] import Distribution.Server.Features.ReverseDependencies (initReverseFeature) +import Distribution.Server.Features.ReverseDependencies (initReverseFeature) import Distribution.Server.Features.DownloadCount (initDownloadFeature) import Distribution.Server.Features.Tags (initTagsFeature) import Distribution.Server.Features.AnalyticsPixels (initAnalyticsPixelsFeature) @@ -132,8 +132,8 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do initAnalyticsPixelsFeature env mkVersionsFeature <- logStartup "versions" $ initVersionsFeature env - -- mkReverseFeature <- logStartup "reverse deps" $ - -- initReverseFeature env + mkReverseFeature <- logStartup "reverse deps" $ + initReverseFeature env mkListFeature <- logStartup "list" $ initListFeature env mkSearchFeature <- logStartup "search" $ @@ -272,15 +272,13 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do usersFeature uploadFeature - {- [reverse index disabled] reverseFeature <- mkReverseFeature coreFeature versionsFeature - -} listFeature <- mkListFeature coreFeature - -- [reverse index disabled] reverseFeature + reverseFeature downloadFeature votesFeature tagsFeature @@ -301,7 +299,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do uploadFeature candidatesFeature versionsFeature - -- [reverse index disabled] reverseFeature + reverseFeature tagsFeature analyticsPixelsFeature downloadFeature @@ -387,7 +385,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do , getFeatureInterface tagsFeature , getFeatureInterface analyticsPixelsFeature , getFeatureInterface versionsFeature - -- [reverse index disabled] , getFeatureInterface reverseFeature + , getFeatureInterface reverseFeature , getFeatureInterface searchFeature , getFeatureInterface listFeature , getFeatureInterface platformFeature diff --git a/src/Distribution/Server/Features/Core.hs b/src/Distribution/Server/Features/Core.hs index c6d8835d6..6aec793ab 100644 --- a/src/Distribution/Server/Features/Core.hs +++ b/src/Distribution/Server/Features/Core.hs @@ -18,6 +18,8 @@ module Distribution.Server.Features.Core ( -- * Misc other utils packageExists, packageIdExists, + + packagesStateComponent, ) where -- stdlib diff --git a/src/Distribution/Server/Features/Html.hs b/src/Distribution/Server/Features/Html.hs index b332205a2..4b118d394 100644 --- a/src/Distribution/Server/Features/Html.hs +++ b/src/Distribution/Server/Features/Html.hs @@ -23,7 +23,7 @@ import Distribution.Server.Features.DownloadCount import Distribution.Server.Features.Votes import Distribution.Server.Features.Search import Distribution.Server.Features.PreferredVersions --- [reverse index disabled] import Distribution.Server.Features.ReverseDependencies +import Distribution.Server.Features.ReverseDependencies import Distribution.Server.Features.PackageContents (PackageContentsFeature(..)) import Distribution.Server.Features.PackageList import Distribution.Server.Features.Tags @@ -51,7 +51,7 @@ import qualified Distribution.Server.Pages.PackageFromTemplate as PagesNew import Distribution.Server.Pages.Template import Distribution.Server.Pages.Util import qualified Distribution.Server.Pages.Group as Pages --- [reverse index disabled] import qualified Distribution.Server.Pages.Reverse as Pages +import Distribution.Server.Pages.Reverse (LatestOrOld(..), ReverseHtmlUtil(..), reverseHtmlUtil) import qualified Distribution.Server.Pages.Index as Pages import Distribution.Server.Util.CountingMap (cmFind, cmToList) import Distribution.Server.Util.DocMeta (loadTarDocMeta) @@ -103,7 +103,7 @@ initHtmlFeature :: ServerEnv -> PackageContentsFeature -> UploadFeature -> PackageCandidatesFeature -> VersionsFeature - -- [reverse index disabled] -> ReverseFeature + -> ReverseFeature -> TagsFeature -> AnalyticsPixelsFeature -> DownloadFeature @@ -137,13 +137,14 @@ initHtmlFeature env@ServerEnv{serverTemplatesDir, serverTemplatesMode, , "noscript-search-form.html" , "analytics-pixels-page.html" , "user-analytics-pixels-page.html" + , "graph.html" ] return $ \user core@CoreFeature{packageChangeHook} packages upload candidates versions - -- [reverse index disabled] reverse + reversef tags analyticsPixels download rank list@ListFeature{itemUpdate} @@ -158,6 +159,7 @@ initHtmlFeature env@ServerEnv{serverTemplatesDir, serverTemplatesMode, htmlFeature env user core packages upload candidates versions + reversef tags analyticsPixels download rank list names @@ -167,6 +169,7 @@ initHtmlFeature env@ServerEnv{serverTemplatesDir, serverTemplatesMode, reportsCore usersdetails (htmlUtilities core candidates tags user) + (reverseHtmlUtil reversef) mainCache namesCache templates @@ -202,6 +205,7 @@ htmlFeature :: ServerEnv -> UploadFeature -> PackageCandidatesFeature -> VersionsFeature + -> ReverseFeature -> TagsFeature -> AnalyticsPixelsFeature -> DownloadFeature @@ -216,6 +220,7 @@ htmlFeature :: ServerEnv -> ReportsFeature -> UserDetailsFeature -> HtmlUtilities + -> ReverseHtmlUtil -> AsyncCache Response -> AsyncCache Response -> Templates @@ -226,7 +231,7 @@ htmlFeature env@ServerEnv{..} core@CoreFeature{queryGetPackageIndex} packages upload candidates versions - -- [reverse index disabled] ReverseFeature{..} + revf@ReverseFeature{..} tags analyticsPixels download rank list@ListFeature{getAllLists} @@ -237,6 +242,7 @@ htmlFeature env@ServerEnv{..} reportsCore usersdetails utilities@HtmlUtilities{..} + reverseH@ReverseHtmlUtil{..} cachePackagesPage cacheNamesPage templates = (HtmlFeature{..}, packageIndex, packagesPage) @@ -274,6 +280,8 @@ htmlFeature env@ServerEnv{..} distros packages htmlTags + htmlReverse + revf htmlPreferred cachePackagesPage cacheNamesPage @@ -290,6 +298,7 @@ htmlFeature env@ServerEnv{..} candidates user templates htmlPreferred = mkHtmlPreferred utilities core versions htmlTags = mkHtmlTags utilities core upload user list tags templates + htmlReverse = mkHtmlReverse utilities core versions list revf reverseH htmlAnalyticsPixels = mkHtmlAnalyticsPixels utilities core user upload analyticsPixels templates @@ -304,6 +313,7 @@ htmlFeature env@ServerEnv{..} , htmlDownloadsResources htmlDownloads , htmlTagsResources htmlTags , htmlAnalyticsPixelsResources htmlAnalyticsPixels + , htmlReverseResource htmlReverse -- and user groups. package maintainers, trustees, admins , htmlGroupResource user (maintainersGroupResource . uploadResource $ upload) , htmlGroupResource user (trusteesGroupResource . uploadResource $ upload) @@ -339,74 +349,6 @@ htmlFeature env@ServerEnv{..} } -} - - -- reverse index (disabled) - {- - , (extendResource $ reversePackage reverses) { - resourceGet = [("html", serveReverse True)] - } - , (extendResource $ reversePackageOld reverses) { - resourceGet = [("html", serveReverse False)] - } - , (extendResource $ reversePackageAll reverses) { - resourceGet = [("html", serveReverseFlat)] - } - , (extendResource $ reversePackageStats reverses) { - resourceGet = [("html", serveReverseStats)] - } - , (extendResource $ reversePackages reverses) { - resourceGet = [("html", serveReverseList)] - } - -} - - - - -- [reverse index disabled] reverses = reverseResource - - - - - - - {- [reverse index disabled] - -------------------------------------------------------------------------------- - -- Reverse - serveReverse :: Bool -> DynamicPath -> ServerPart Response - serveReverse isRecent dpath = - htmlResponse $ - withPackageId dpath $ \pkgid -> do - let pkgname = packageName pkgid - rdisp <- case packageVersion pkgid of - Version [] [] -> withPackageAll pkgname $ \_ -> revPackageName pkgname - _ -> withPackageVersion pkgid $ \_ -> revPackageId pkgid - render <- (if isRecent then renderReverseRecent else renderReverseOld) pkgname rdisp - return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ " - Reverse dependencies ") $ - Pages.reversePackageRender pkgid (corePackageIdUri "") revr isRecent render - - serveReverseFlat :: DynamicPath -> ServerPart Response - serveReverseFlat dpath = htmlResponse $ - withPackageAllPath dpath $ \pkgname _ -> do - revCount <- query $ GetReverseCount pkgname - pairs <- revPackageFlat pkgname - return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ "Flattened reverse dependencies") $ - Pages.reverseFlatRender pkgname (corePackageNameUri "") revr revCount pairs - - serveReverseStats :: DynamicPath -> ServerPart Response - serveReverseStats dpath = htmlResponse $ - withPackageAllPath dpath $ \pkgname pkgs -> do - revCount <- query $ GetReverseCount pkgname - return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ "Reverse dependency statistics") $ - Pages.reverseStatsRender pkgname (map packageVersion pkgs) (corePackageIdUri "") revr revCount - - serveReverseList :: DynamicPath -> ServerPart Response - serveReverseList _ = do - let revr = reverseResource revs - triple <- sortedRevSummary revs - hackCount <- PackageIndex.indexSize <$> queryGetPackageIndex - return $ toResponse $ Resource.XHtml $ hackagePage "Reverse dependencies" $ - Pages.reversePackagesRender (corePackageNameUri "") revr hackCount triple - -} - -------------------------------------------------------------------------------- -- Additional package indices @@ -469,6 +411,8 @@ mkHtmlCore :: ServerEnv -> DistroFeature -> PackageContentsFeature -> HtmlTags + -> HtmlReverse + -> ReverseFeature -> HtmlPreferred -> AsyncCache Response -> AsyncCache Response @@ -496,6 +440,8 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} DistroFeature{queryPackageStatus} PackageContentsFeature{packageRender} HtmlTags{..} + HtmlReverse{..} + ReverseFeature{queryReverseDeps, revJSON} HtmlPreferred{..} cachePackagesPage cacheNamesPage @@ -537,6 +483,16 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} , (extendResource searchPackagesResource) { resourceGet = [("html", serveBrowsePage)] } + , (resourceAt "/packages/graph.json" ) { + resourceDesc = [(GET, "Show JSON of package dependency information")] + , resourceGet = [("json", + serveGraphJSON)] + } + , (resourceAt "/packages/graph" ) { + resourceDesc = [(GET, "Show graph of package dependency information")] + , resourceGet = [("html", + serveGraph)] + } , (extendResource $ corePackagesPage cores) { resourceDesc = [(GET, "Show package index")] , resourceGet = [("html", const $ readAsyncCache cachePackagesPage)] @@ -575,6 +531,19 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} <> noscriptFormRendered ] + serveGraphJSON :: DynamicPath -> ServerPartE Response + serveGraphJSON _ = do + graph <- revJSON + --TODO: use proper type for graph with ETag + cacheControl [Public, maxAgeMinutes 30] (etagFromHash graph) + ok . toResponse $ graph + + serveGraph :: DynamicPath -> ServerPartE Response + serveGraph _ = do + cacheControlWithoutETag [Public, maxAgeDays 1] -- essentially static + template <- getTemplate templates "graph.html" + return $ toResponse $ template [] + -- Currently the main package page is thrown together by querying a bunch -- of features about their attributes for the given package. It'll need -- reorganizing to look aesthetic, as opposed to the sleek and simple current @@ -603,6 +572,7 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} userRating <- case auth of Just (uid,_) -> pkgUserVote pkgname uid; _ -> return Nothing mdoctarblob <- queryDocumentation realpkg tags <- queryTagsForPackage pkgname + rdeps <- queryReverseDeps pkgname deprs <- queryGetDeprecatedFor pkgname mreadme <- makeReadme render hasDocs <- queryHasDocumentation documentationFeature realpkg @@ -659,6 +629,9 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} , "hasExecOnly" $= (not . hasLibs) pkgdesc && (not . null) execs , "userRating" $= userRating , "score" $= pkgScore + , "hasrdeps" $= not (rdeps == ([],[])) + , "rdeps" $= renderPkgPageDeps rdeps + , "rdepsummary" $= renderDeps pkgname rdeps , "buildStatus" $= buildStatus , "hasDocs" $= hasDocs , "install" $= install @@ -2028,3 +2001,97 @@ htmlGroupResource UserFeature{..} r@(GroupResource groupR userR getGroup) = groupDeleteUser group dpath goToList dpath goToList dpath = seeOther (renderResource' (groupResource r) dpath) (toResponse ()) + +{------------------------------------------------------------------------------- + Reverse +-------------------------------------------------------------------------------} +data HtmlReverse = HtmlReverse { + htmlReverseResource :: [Resource] + } + +mkHtmlReverse :: HtmlUtilities + -> CoreFeature + -> VersionsFeature + -> ListFeature + -> ReverseFeature + -> ReverseHtmlUtil + -> HtmlReverse +mkHtmlReverse HtmlUtilities{..} + CoreFeature{ coreResource = CoreResource{ + packageInPath + , lookupPackageName + , corePackageIdUri + , corePackageNameUri + }, + queryGetPackageIndex + } + VersionsFeature{withPackageVersion} + ListFeature{} + ReverseFeature{..} + ReverseHtmlUtil{..} + = HtmlReverse{..} + where + htmlReverseResource = [ + (extendResource $ reversePackage reverseResource) { + resourceGet = [("html", serveReverse OnlyLatest)] + } + , (extendResource $ reversePackageOld reverseResource) { + resourceGet = [("html", serveReverse OnlyOlder)] + } + ,(extendResource $ reversePackageFlat reverseResource) { + resourceGet = [("html", serveReverseFlat)] + } + , (extendResource $ reversePackageVerbose reverseResource) { + resourceGet = [("html", serveReverseVerbose)] + } + , (extendResource $ reversePackages reverseResource) { + resourceGet = [("html", serveReverseList)] + } + ] + + + serveReverse :: LatestOrOld -> DynamicPath -> ServerPartE Response + serveReverse isRecent dpath = do + pkgid <- packageInPath dpath + let pkgname = pkgName pkgid + rdisp <- if nullVersion == packageVersion pkgid + then lookupPackageName pkgname *> revPackageName pkgname + else withPackageVersion pkgid $ \_ -> revPackageId pkgid + render <- (if isRecent == OnlyLatest then renderReverseRecent else renderReverseOld) pkgname rdisp + return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ " - Reverse dependencies ") $ + reversePackageRender pkgid (corePackageIdUri "") isRecent render + + redirectIfVersion uriGen pkgid = + if packageVersion pkgid /= nullVersion + then do + let newUri = uriGen reverseResource "" (packageName pkgid) + seeOther newUri () + else pure () + + serveReverseFlat :: DynamicPath -> ServerPartE Response + serveReverseFlat dpath = do + pkg <- packageInPath dpath + redirectIfVersion reverseFlatUri pkg + let pkgname = pkgName pkg + revCount <- revPackageStats pkgname + pairs <- revPackageFlat pkgname + return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ " - Flattened reverse dependencies") $ + reverseFlatRender pkgname (corePackageNameUri "") revCount pairs + + serveReverseVerbose :: DynamicPath -> ServerPartE Response + serveReverseVerbose dpath = do + pkg <- packageInPath dpath + redirectIfVersion reverseVerboseUri pkg + let pkgname = pkgName pkg + pkgids <- lookupPackageName pkgname + revCount <- revPackageStats pkgname + versions <- revForEachVersion pkgname + return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ " - Reverse dependency statistics") $ + reverseVerboseRender pkgname (map packageVersion pkgids) (corePackageIdUri "") revCount versions + + serveReverseList :: DynamicPath -> ServerPartE Response + serveReverseList _ = do + namesWithCounts <- revCountForAllPackages + hackCount <- PackageIndex.indexSize <$> queryGetPackageIndex + return $ toResponse $ Resource.XHtml $ hackagePage "Reverse dependencies" $ + reversePackagesRender (corePackageNameUri "") hackCount namesWithCounts diff --git a/src/Distribution/Server/Features/Html/HtmlUtilities.hs b/src/Distribution/Server/Features/Html/HtmlUtilities.hs index c217ab9b4..d0fd2f44f 100644 --- a/src/Distribution/Server/Features/Html/HtmlUtilities.hs +++ b/src/Distribution/Server/Features/Html/HtmlUtilities.hs @@ -26,6 +26,8 @@ data HtmlUtilities = HtmlUtilities { , makeRow :: PackageItem -> Html , renderTags :: Set Tag -> [Html] , renderReviewTags :: Set Tag -> (Set Tag, Set Tag) -> PackageName -> [Html] + , renderDeps :: PackageName -> ([PackageName], [PackageName]) -> Html + , renderPkgPageDeps :: ([PackageName], [PackageName]) -> Html } htmlUtilities :: CoreFeature -> PackageCandidatesFeature -> TagsFeature -> UserFeature -> HtmlUtilities @@ -46,6 +48,7 @@ htmlUtilities CoreFeature{coreResource} makeRow item = tr << [ td $ itemNameHtml , td $ toHtml $ show $ itemDownloads item , td $ toHtml $ show $ itemVotes item + , td $ toHtml $ show $ itemRevDepsCount item , td $ toHtml $ itemDesc item , td $ " (" +++ renderTags (itemTags item) +++ ")" , td $ toHtml $ formatTime defaultTimeLocale "%F" (itemLastUpload item) @@ -98,3 +101,22 @@ htmlUtilities CoreFeature{coreResource} cores = coreResource + + renderPkgPageDeps :: ([PackageName], [PackageName])-> Html + renderPkgPageDeps (direct, indirect) = + map toHtml [show (length direct), " direct", ", ", show (length indirect), " indirect "] +++ + thespan ! [thestyle "font-size: small", theclass "revdepdetails"] + << (" [" +++ anchor ! [href ""] << "details" +++ "]") + + renderDeps :: PackageName -> ([PackageName], [PackageName])-> Html + renderDeps pkg (direct, indirect) = + (if null direct then (toHtml "") else summary "Direct" direct) +++ + (if null indirect then (toHtml "") else summary "Indirect" indirect) +++ + detailsLink + where + summary title_ dep = thediv << [ bold (toHtml title_), br + , p << intersperse (toHtml ", ") (map packageNameLink dep) + ] + detailsLink = thespan ! [thestyle "font-size: small"] + << (" [" +++ anchor ! [href detailURL] << "details" +++ "]") + detailURL = "/package/" ++ unPackageName pkg ++ "/reverse" diff --git a/src/Distribution/Server/Features/PackageList.hs b/src/Distribution/Server/Features/PackageList.hs index cf07d90f6..51cbf5fe0 100644 --- a/src/Distribution/Server/Features/PackageList.hs +++ b/src/Distribution/Server/Features/PackageList.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE RankNTypes, RecordWildCards #-} +{-# LANGUAGE RankNTypes, RecordWildCards, NamedFieldPuns #-} module Distribution.Server.Features.PackageList ( ListFeature(..), initListFeature, @@ -9,7 +9,7 @@ module Distribution.Server.Features.PackageList ( import Distribution.Server.Framework import Distribution.Server.Features.Core --- [reverse index disabled] import Distribution.Server.Features.ReverseDependencies +import Distribution.Server.Features.ReverseDependencies import Distribution.Server.Features.Votes import Distribution.Server.Features.DownloadCount import Distribution.Server.Features.Tags @@ -23,7 +23,6 @@ import qualified Distribution.Server.Packages.PackageIndex as PackageIndex import Distribution.Server.Util.CountingMap (cmFind) import Distribution.Server.Packages.Types --- [reverse index disabled] import Distribution.Server.Packages.Reverse import Distribution.Server.Users.Types import Distribution.Package @@ -33,6 +32,7 @@ import Distribution.Pretty (prettyShow) import Distribution.Utils.ShortText (fromShortText) import Control.Concurrent +import qualified Data.List.NonEmpty as NE import Data.Maybe (mapMaybe) import Data.Map (Map) import qualified Data.Map as Map @@ -76,7 +76,7 @@ data PackageItem = PackageItem { itemDownloads :: !Int, -- The number of direct revdeps. (Likewise.) -- also: distinguish direct/flat? - -- [reverse index disabled] itemRevDepsCount :: !Int, + itemRevDepsCount :: !Int, -- Whether there's a library here. itemHasLibrary :: !Bool, -- How many executables (>=0) this package has. @@ -87,24 +87,24 @@ data PackageItem = PackageItem { itemNumBenchmarks :: !Int, -- Last upload date itemLastUpload :: !UTCTime, + -- Hotness = recent downloads + stars + 2 * no rev deps + itemHotness :: !Float, -- Last version itemLastVersion :: !String - -- Hotness: a more heuristic way to sort packages. presently non-existent. - --itemHotness :: Int } instance MemSize PackageItem where - memSize (PackageItem a b c d e f g h i j k l o) = memSize13 a b c d e f g h i j k l o + memSize (PackageItem a b c d e f g h i j k l _m n o) = memSize11 a b c d e f g h i j (k, l, n, o) emptyPackageItem :: PackageName -> PackageItem emptyPackageItem pkg = PackageItem pkg Set.empty Nothing "" [] - 0 0 False 0 0 0 (UTCTime (toEnum 0) 0) "" + 0 0 0 False 0 0 0 (UTCTime (toEnum 0) 0) 0 "" initListFeature :: ServerEnv -> IO (CoreFeature - -- [reverse index disabled] -> ReverseFeature + -> ReverseFeature -> DownloadFeature -> VotesFeature -> TagsFeature @@ -117,7 +117,7 @@ initListFeature _env = do itemUpdate <- newHook return $ \core@CoreFeature{..} - -- [reverse index disabled] revs + revs@ReverseFeature{revDirectCount, reverseHook} download votesf@VotesFeature{..} tagsf@TagsFeature{..} @@ -126,7 +126,7 @@ initListFeature _env = do uploads@UploadFeature{..} -> do let (feature, modifyItem, updateDesc) = - listFeature core download votesf tagsf versions users uploads + listFeature core revs download votesf tagsf versions users uploads itemCache itemUpdate registerHookJust packageChangeHook isPackageChangeAny $ \(pkgid, _) -> @@ -146,15 +146,15 @@ initListFeature _env = do runHook_ itemUpdate (Set.singleton pkgname) Nothing -> return () - {- [reverse index disabled] - votesf@VotesFeature{..} - registerHook (reverseUpdateHook revs) $ \mrev -> do - let pkgs = Map.keys mrev - forM_ pkgs $ \pkgname -> do - revCount <- query . GetReverseCount $ pkgname - modifyItem pkgname (updateReverseItem revCount) - runHook' itemUpdate $ Set.fromDistinctAscList pkgs - -} + registerHook reverseHook $ \pkginfos -> do + let + names = Set.fromDistinctAscList $ + map (pkgName . pkgInfoId . NE.head) + pkginfos + forM_ names $ \pkgname -> do + revDirect <- revDirectCount pkgname + modifyItem pkgname (updateReverseItem revDirect) + runHook_ itemUpdate names registerHook votesUpdated $ \(pkgname, _) -> do votes <- pkgNumScore pkgname @@ -175,6 +175,7 @@ initListFeature _env = do listFeature :: CoreFeature + -> ReverseFeature -> DownloadFeature -> VotesFeature -> TagsFeature @@ -188,6 +189,7 @@ listFeature :: CoreFeature PackageName -> IO ()) listFeature CoreFeature{..} + ReverseFeature{revDirectCount} DownloadFeature{..} VotesFeature{..} TagsFeature{..} @@ -253,7 +255,7 @@ listFeature CoreFeature{..} constructItem pkg = do let pkgname = packageName pkg desc = pkgDesc pkg - -- [reverse index disabled] revCount <- query . GetReverseCount $ pkgname + intRevDirectCount <- revDirectCount pkgname users <- queryGetUserDb tags <- queryTagsForPackage pkgname downs <- recentPackageDownloads @@ -266,9 +268,10 @@ listFeature CoreFeature{..} , itemMaintainer = map (userIdToName users) (UserIdSet.toList maintainers) , itemDeprecated = deprs , itemDownloads = cmFind pkgname downs - -- [reverse index disabled] , itemRevDepsCount = directReverseCount revCount , itemVotes = votes , itemLastUpload = fst (pkgOriginalUploadInfo pkg) + , itemRevDepsCount = intRevDirectCount + , itemHotness = votes + fromIntegral (cmFind pkgname downs) + fromIntegral intRevDirectCount * 2 , itemLastVersion = prettyShow $ pkgVersion $ pkgInfoId pkg } @@ -313,7 +316,8 @@ updateTagItem tags item = updateVoteItem :: Float -> PackageItem -> PackageItem updateVoteItem score item = item { - itemVotes = score + itemVotes = score, + itemHotness = fromIntegral (itemRevDepsCount item) * 2 + score + fromIntegral (itemDownloads item) } updateDeprecation :: Maybe [PackageName] -> PackageItem -> PackageItem @@ -322,16 +326,16 @@ updateDeprecation pkgs item = itemDeprecated = pkgs } -{- [reverse index disabled] -updateReverseItem :: ReverseCount -> PackageItem -> PackageItem -updateReverseItem revCount item = +updateReverseItem :: Int -> PackageItem -> PackageItem +updateReverseItem revDirectCount item = item { - itemRevDepsCount = directReverseCount revCount + itemRevDepsCount = revDirectCount, + itemHotness = fromIntegral revDirectCount * 2 + itemVotes item + fromIntegral (itemDownloads item) } --} updateDownload :: Int -> PackageItem -> PackageItem updateDownload count item = item { - itemDownloads = count + itemDownloads = count, + itemHotness = fromIntegral (itemRevDepsCount item) * 2 + itemVotes item + realToFrac count } diff --git a/src/Distribution/Server/Features/PreferredVersions.hs b/src/Distribution/Server/Features/PreferredVersions.hs index 4cd1b4ac9..78c5ddeac 100644 --- a/src/Distribution/Server/Features/PreferredVersions.hs +++ b/src/Distribution/Server/Features/PreferredVersions.hs @@ -11,6 +11,7 @@ module Distribution.Server.Features.PreferredVersions ( classifyVersions, PreferredRender(..), + preferredStateComponent, ) where import Distribution.Server.Framework @@ -49,6 +50,7 @@ data VersionsFeature = VersionsFeature { queryGetPreferredInfo :: forall m. MonadIO m => PackageName -> m PreferredInfo, queryGetDeprecatedFor :: forall m. MonadIO m => PackageName -> m (Maybe [PackageName]), + queryGetPreferredVersions :: forall m. MonadIO m => m PreferredVersions, versionsResource :: VersionsResource, deprecatedHook :: Hook (PackageName, Maybe [PackageName]) (), @@ -61,6 +63,7 @@ data VersionsFeature = VersionsFeature { doPreferredsRender :: forall m. MonadIO m => m [(PackageName, PreferredRender)], doDeprecatedsRender :: forall m. MonadIO m => m [(PackageName, [PackageName])], + withPackageVersion :: forall a. PackageId -> (PkgInfo -> ServerPartE a) -> ServerPartE a, withPackagePreferred :: forall a. PackageId -> (PkgInfo -> [PkgInfo] -> ServerPartE a) -> ServerPartE a, withPackagePreferredPath :: forall a. DynamicPath -> (PkgInfo -> [PkgInfo] -> ServerPartE a) -> ServerPartE a } @@ -157,6 +160,9 @@ versionsFeature ServerEnv{ serverVerbosity = verbosity } queryGetDeprecatedFor :: MonadIO m => PackageName -> m (Maybe [PackageName]) queryGetDeprecatedFor name = queryState preferredState (GetDeprecatedFor name) + queryGetPreferredVersions :: MonadIO m => m PreferredVersions + queryGetPreferredVersions = queryState preferredState GetPreferredVersions + updateDeprecatedTags = do pkgs <- deprecatedMap <$> queryState preferredState GetPreferredVersions setCalculatedTag (Tag "deprecated") (Map.keysSet pkgs) @@ -265,6 +271,17 @@ versionsFeature ServerEnv{ serverVerbosity = verbosity } runHook_ deprecatedHook (pkgname, deprs) updateDeprecatedTags + withPackageVersion :: PackageId -> (PkgInfo -> ServerPartE a) -> ServerPartE a + withPackageVersion pkgid func = do + pkgIndex <- queryGetPackageIndex + guard (packageVersion pkgid /= nullVersion) + case PackageIndex.lookupPackageName pkgIndex (packageName pkgid) of + [] -> packageError [MText $ "No such package in package index. ", MLink "Search for related terms instead?"$ "/packages/search?terms=" ++ (display $ pkgName pkgid)] + pkg -> case find ((== packageVersion pkgid) . packageVersion) pkg of + Nothing -> packageError [MText $ "No such package version for " ++ display (packageName pkgid)] + Just pkg' -> func pkg' + where packageError = errNotFound "Package not found" + --------------------------- -- This is a function used by the HTML feature to select the version to display. -- It could be enhanced by displaying a search page in the case of failure, diff --git a/src/Distribution/Server/Features/ReverseDependencies.hs b/src/Distribution/Server/Features/ReverseDependencies.hs index e25eb5c77..1a226b2c1 100644 --- a/src/Distribution/Server/Features/ReverseDependencies.hs +++ b/src/Distribution/Server/Features/ReverseDependencies.hs @@ -1,247 +1,293 @@ -{-# LANGUAGE RankNTypes, RecordWildCards #-} +{-# LANGUAGE DeriveGeneric, RankNTypes, NamedFieldPuns, RecordWildCards #-} module Distribution.Server.Features.ReverseDependencies ( - ReverseFeature, - reverseResource, + ReverseCount(..), + ReverseFeature(..), + ReversePageRender(..), + ReverseRender(..), ReverseResource(..), - reverseUpdateHook, initReverseFeature, - ReverseRender(..), - ReversePageRender(..), - revPackageId, - revPackageName, - renderReverseRecent, - renderReverseOld, - revPackageFlat, - revPackageStats, - revPackageSummary, - revSummary, - sortedRevSummary + reverseFeature ) where -import Distribution.Server.Acid (query, update) import Distribution.Server.Framework -import Distribution.Server.Framework.BackupRestore -import Distribution.Server.Framework.BackupDump (testRoundtripByQuery) import Distribution.Server.Features.Core import Distribution.Server.Features.PreferredVersions - -import Distribution.Server.Packages.State -import Distribution.Server.Packages.Reverse -import Distribution.Server.Packages.Preferred -import qualified Distribution.Server.Framework.Cache as Cache - +import Distribution.Server.Features.PreferredVersions.State (PreferredVersions) +import qualified Distribution.Server.Packages.PackageIndex as PackageIndex +import Distribution.Server.Packages.PackageIndex (PackageIndex, packageNames, allPackagesByNameNE) +import Distribution.Server.Packages.Types (PkgInfo) +import Distribution.Server.Features.ReverseDependencies.State import Distribution.Package import Distribution.Text (display) -import Distribution.Version - -import Data.List (mapAccumL, sortBy) -import Data.Maybe (catMaybes) -import Data.Function (fix, on) -import Data.Map (Map) +import Distribution.Version (Version) + +import Control.Monad.Catch (MonadThrow, MonadCatch) +import Data.Aeson +import Data.ByteString.Lazy (ByteString) +import Data.Containers.ListUtils (nubOrd) +import Data.List (mapAccumL, sortOn) +import qualified Data.List.NonEmpty as NE +import Data.Maybe (catMaybes, fromJust) +import Data.Function (fix) +import qualified Data.Bimap as Bimap +import qualified Data.Array as Arr +import qualified Data.Graph as Gr import qualified Data.Map as Map +import Data.Set (Set) import qualified Data.Set as Set -import Control.Monad (liftM, forever) -import Control.Monad.Trans (MonadIO) -import Control.Concurrent (forkIO) -import Control.Concurrent.Chan +import GHC.Generics hiding (packageName) data ReverseFeature = ReverseFeature { reverseFeatureInterface :: HackageFeature, reverseResource :: ReverseResource, - reverseUpdateHook :: Hook (Map PackageName [Version] -> IO ()) + + reverseHook :: Hook [NE.NonEmpty PkgInfo] (), + + queryReverseDeps :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m ([PackageName], [PackageName]), + revPackageId :: forall m. (MonadCatch m, MonadIO m) => PackageId -> m ReverseDisplay, + revPackageName :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m ReverseDisplay, + renderReverseRecent :: forall m. (MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> m ReversePageRender, + renderReverseOld :: forall m. (MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> m ReversePageRender, + revPackageFlat :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m [(PackageName, Int)], + revDirectCount :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m Int, + revPackageStats :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m ReverseCount, + revCountForAllPackages :: forall m. (MonadIO m, MonadCatch m) => m [(PackageName, ReverseCount)], + revJSON :: forall m. (MonadIO m, MonadThrow m) => m ByteString, + revDisplayInfo :: forall m. MonadIO m => m VersionIndex, + revForEachVersion :: forall m. (MonadIO m, MonadThrow m) => PackageName -> m (Map.Map Version (Set PackageIdentifier)) } instance IsHackageFeature ReverseFeature where getFeatureInterface = reverseFeatureInterface - data ReverseResource = ReverseResource { reversePackage :: Resource, reversePackageOld :: Resource, - reversePackageAll :: Resource, - reversePackageStats :: Resource, + reversePackageFlat :: Resource, + reversePackageVerbose :: Resource, reversePackages :: Resource, - reversePackagesAll :: Resource, reverseUri :: String -> PackageId -> String, reverseNameUri :: String -> PackageName -> String, reverseOldUri :: String -> PackageId -> String, - reverseOldNameUri :: String -> PackageName -> String, - reverseAllUri :: String -> PackageName -> String, - reverseStatsUri :: String -> PackageName -> String, - reversesUri :: String -> String, - reversesAllUri :: String -> String + reverseFlatUri :: String -> PackageName -> String, + reverseVerboseUri :: String -> PackageName -> String } initReverseFeature :: ServerEnv -> IO (CoreFeature + -> VersionsFeature -> IO ReverseFeature) -initReverseFeature ServerEnv{serverVerbosity = verbosity} = do - revChan <- newChan - registerHook (packageAddHook core) $ \pkg -> writeChan revChan $ - update $ AddReversePackage (packageId pkg) (getAllDependencies pkg) - registerHook (packageRemoveHook core) $ \pkg -> writeChan revChan $ - update $ RemoveReversePackage (packageId pkg) (getAllDependencies pkg) - registerHook (packageChangeHook core) $ \pkg pkg' -> writeChan revChan $ - update $ ChangeReversePackage (packageId pkg) - (getAllDependencies pkg) (getAllDependencies pkg') - - revHook <- newHook - let select (_, b, _) = b - sortedRevs = fmap (sortBy $ on (flip compare) select) revSummary - revTopCache <- Cache.newCacheable =<< sortedRevs - registerHook revHook $ \_ -> Cache.putCache revTopCache =<< sortedRevs - - return $ \core -> do - let feature = reverseFeature core - revChan revHook revTopCache +initReverseFeature _ = do + updateReverse <- newHook + + return $ \CoreFeature{queryGetPackageIndex,packageChangeHook} + VersionsFeature{queryGetPreferredVersions} -> do + idx <- queryGetPackageIndex + memState <- newMemStateWHNF =<< constructReverseIndex idx + + let feature = reverseFeature queryGetPackageIndex queryGetPreferredVersions memState updateReverse + + registerHookJust packageChangeHook isPackageChangeAny $ \(pkgid, mpkginfo) -> + case mpkginfo of + Nothing -> return () --PackageRemoveHook + Just pkginfo -> do + index <- queryGetPackageIndex + r <- readMemState memState + added <- addPackage index (packageName pkgid) (getDepNames pkginfo) r + writeMemState memState added + runHook_ updateReverse [pure pkginfo] return feature -reverseFeature :: CoreFeature - -> Chan (IO (Map PackageName [Version])) - -> Hook (Map PackageName [Version] -> IO ()) - -> Cache.Cache [(PackageName, Int, Int)] +data ReverseRender = ReverseRender { + rendRevPkg :: PackageId, + rendRevStatus :: Maybe VersionStatus, + rendRevCount :: Int +} deriving (Show, Eq, Ord) + +data ReversePageRender = ReversePageRender { + rendRevList :: [ReverseRender], + rendFilterCount :: (Int, Int), + rendPageTotal :: Int +} + +-- data Node = Node {id::Int, label::String} deriving Generic +data Edge = Edge { + id::Int, + name::String, + deps::[String] + } deriving Generic +-- data JGraph = JGraph { nodes::[Node], edges::[Edge]} deriving Generic +-- instance ToJSON Node +instance ToJSON Edge +-- instance ToJSON JGraph +-- instance ToJSON PackageName + +reverseFeature :: IO (PackageIndex PkgInfo) + -> IO PreferredVersions + -> MemState ReverseIndex + -> Hook [NE.NonEmpty PkgInfo] () -> ReverseFeature -reverseFeature CoreFeature{..} - reverseStream reverseUpdateHook reverseTopCache +reverseFeature queryGetPackageIndex + queryGetPreferredVersions + reverseMemState + reverseHook = ReverseFeature{..} where reverseFeatureInterface = (emptyHackageFeature "reverse") { - featureResources = map ($reverseResource) [] - , featurePostInit = forkIO transferReverse >> return () - , featureDumpRestore = Just (return [], restoreBackup, testRoundtripByQuery (query GetReverseIndex)) + featureResources = map ($ reverseResource) [] + , featurePostInit = initReverseIndex + , featureState = [] + , featureCaches = [ + CacheComponent { + cacheDesc = "reverse index", + getCacheMemSize = memSize <$> readMemState reverseMemState + } + ] } - transferReverse = forever $ do - revFunc <- readChan reverseStream - modded <- revFunc - runHook' reverseUpdateHook modded - - --TODO: this isn't a restore! - -- do we need a post init/restore hook for initialising caches? - restoreBackup = RestoreBackup - { restoreEntry = \_ -> return $ Right restoreBackup - , restoreFinalize = return $ Right restoreBackup - , restoreComplete = do - putStrLn "Calculating reverse dependencies" - index <- fmap packageList $ query GetPackagesState - let revs = constructReverseIndex index - update $ ReplaceReverseIndex revs - } + initReverseIndex :: IO () + initReverseIndex = do + index <- liftIO queryGetPackageIndex + -- We build the proper index earlier, this just fires the reverse hooks + runHook_ reverseHook $ allPackagesByNameNE index + reverseResource = fix $ \r -> ReverseResource { reversePackage = resourceAt "/package/:package/reverse.:format" , reversePackageOld = resourceAt "/package/:package/reverse/old.:format" - , reversePackageAll = resourceAt "/package/:package/reverse/all.:format" - , reversePackageStats = resourceAt "/package/:package/reverse/summary.:format" + , reversePackageFlat = resourceAt "/package/:package/reverse/flat.:format" + , reversePackageVerbose = resourceAt "/package/:package/reverse/verbose.:format" , reversePackages = resourceAt "/packages/reverse.:format" - , reversePackagesAll = resourceAt "/packages/reverse/all.:format" , reverseUri = \format pkg -> renderResource (reversePackage r) [display pkg, format] , reverseNameUri = \format pkg -> renderResource (reversePackage r) [display pkg, format] , reverseOldUri = \format pkg -> renderResource (reversePackageOld r) [display pkg, format] - , reverseOldNameUri = \format pkg -> renderResource (reversePackageOld r) [display pkg, format] - , reverseAllUri = \format pkg -> renderResource (reversePackageAll r) [display pkg, format] - , reverseStatsUri = \format pkg -> renderResource (reversePackageStats r) [display pkg, format] - , reversesUri = \format -> renderResource (reversePackages r) [format] - , reversesAllUri = \format -> renderResource (reversePackagesAll r) [format] + , reverseFlatUri = \format pkg -> renderResource (reversePackageFlat r) [display pkg, format] + , reverseVerboseUri = \format pkg -> renderResource (reversePackageVerbose r) [display pkg, format] } --textRevDisplay :: ReverseDisplay -> String --textRevDisplay m = unlines . map (\(n, (v, m)) -> display n ++ "-" ++ display v ++ ": " ++ show m) . Map.toList $ m --- If VersionStatus caching is used, revPackageId and revPackageName could be --- reduced to a single map lookup (see Distribution.Server.Packages.Reverse). -revPackageId :: MonadIO m => PackageId -> m ReverseDisplay -revPackageId pkgid = do - dispInfo <- revDisplayInfo - revs <- liftM reverseDependencies $ query GetReverseIndex - return $ perVersionReverse dispInfo revs pkgid - -revPackageName :: MonadIO m => PackageName -> m ReverseDisplay -revPackageName pkgname = do - dispInfo <- revDisplayInfo - revs <- liftM reverseDependencies $ query GetReverseIndex - return $ perPackageReverse dispInfo revs pkgname - -revDisplayInfo :: MonadIO m => m VersionIndex -revDisplayInfo = do - pkgIndex <- liftM packageList $ query GetPackagesState - prefs <- query GetPreferredVersions - return $ getDisplayInfo prefs pkgIndex - -data ReverseRender = ReverseRender { - rendRevPkg :: PackageId, - rendRevStatus :: Maybe VersionStatus, - rendRevCount :: Int -} deriving (Show, Eq, Ord) - -data ReversePageRender = ReversePageRender { - rendRevList :: [ReverseRender], - rendFilterCount :: (Int, Int), - rendPageTotal :: Int -} - -renderReverseWith :: MonadIO m => PackageName -> ReverseDisplay -> (Maybe VersionStatus -> Bool) -> m ReversePageRender -renderReverseWith pkg rev filterFunc = do - counts <- liftM reverseCount $ query GetReverseIndex - let toRender (i, i') (pkgname, (version, status)) = case filterFunc status of - False -> (,) (i, i'+1) Nothing - True -> (,) (i+1, i') $ Just $ ReverseRender { - rendRevPkg = PackageIdentifier pkgname version, - rendRevStatus = status, - rendRevCount = maybe 0 directReverseCount $ Map.lookup pkgname counts - } - (res, rlist) = mapAccumL toRender (0, 0) (Map.toList rev) - pkgCount = maybe 0 directReverseCount $ Map.lookup pkg counts - return $ ReversePageRender (catMaybes rlist) res pkgCount - -renderReverseRecent :: MonadIO m => PackageName -> ReverseDisplay -> m ReversePageRender -renderReverseRecent pkg rev = renderReverseWith pkg rev $ \status -> case status of - Just DeprecatedVersion -> False - Nothing -> False - _ -> True - -renderReverseOld :: MonadIO m => PackageName -> ReverseDisplay -> m ReversePageRender -renderReverseOld pkg rev = renderReverseWith pkg rev $ \status -> case status of - Just DeprecatedVersion -> True - Nothing -> True - _ -> False - --- This could also differentiate between direct and indirect dependencies --- with a bit more calculation. -revPackageFlat :: MonadIO m => PackageName -> m [(PackageName, Int)] -revPackageFlat pkgname = do - index <- query GetReverseIndex - let counts = reverseCount index - count pkg = maybe 0 flattenedReverseCount $ Map.lookup pkg counts - pkgs = maybe [] Set.toList $ Map.lookup pkgname $ flattenedReverse index - return $ map (\pkg -> (pkg, count pkg)) pkgs - -revPackageStats :: MonadIO m => PackageName -> m ReverseCount -revPackageStats = query . GetReverseCount - -revPackageSummary :: MonadIO m => PackageId -> m (Int, Int) -revPackageSummary (PackageIdentifier pkgname version) = do - ReverseCount direct _ versions <- revPackageStats pkgname - return (direct, Map.findWithDefault 0 version versions) - --- This returns a list of (package name, direct dependencies, flat dependencies) --- for all packages. An interesting fact: it even does so for packages which --- don't exist in the index, except the latter two fields are always zero. This is --- because no versions of these packages exist, so the union of no versions is --- still no versions. TODO: use this fact to make an index of dependencies which --- are not in Hackage at all, which might be useful for fixing accidentally --- broken packages. -revSummary :: MonadIO m => m [(PackageName, Int, Int)] -revSummary = do - counts <- liftM reverseCount $ query GetReverseIndex - return $ map (\(pkg, ReverseCount direct flat _) -> (pkg, direct, flat)) $ Map.toList counts - -sortedRevSummary :: MonadIO m => ReverseFeature -> m [(PackageName, Int, Int)] -sortedRevSummary revs = Cache.getCache $ reverseTopCache revs - + -- If VersionStatus caching is used, revPackageId and revPackageName could be + -- reduced to a single map lookup (see Distribution.Server.Packages.Reverse). + queryReverseIndex :: MonadIO m => m ReverseIndex + queryReverseIndex = readMemState reverseMemState + + queryReverseDeps :: (MonadIO m, MonadCatch m) => PackageName -> m ([PackageName], [PackageName]) + queryReverseDeps pkgname = do + ms <- readMemState reverseMemState + rdeps <- getDependencies pkgname ms + rdepsall <- getDependenciesFlat pkgname ms + let indirect = Set.difference rdepsall rdeps + return (Set.toList rdeps, Set.toList indirect) + + revPackageId :: (MonadCatch m, MonadIO m) => PackageId -> m ReverseDisplay + revPackageId pkgid = do + dispInfo <- revDisplayInfo + pkgIndex <- liftIO queryGetPackageIndex + revs <- queryReverseIndex + perVersionReverse dispInfo pkgIndex revs pkgid + + revPackageName :: (MonadIO m, MonadCatch m) => PackageName -> m ReverseDisplay + revPackageName pkgname = do + dispInfo <- revDisplayInfo + pkgIndex <- liftIO queryGetPackageIndex + revs <- queryReverseIndex + perPackageReverse dispInfo pkgIndex revs pkgname + + revJSON :: (MonadIO m, MonadThrow m) => m ByteString + revJSON = do + ReverseIndex revdeps nodemap _depmap <- queryReverseIndex + let assoc = takeWhile (\(a,_) -> a < Bimap.size nodemap) $ Arr.assocs . Gr.transposeG $ revdeps + nodeToString node = unPackageName (nodemap Bimap.!> node) + -- nodes = map (uncurry Node) $ map (\n -> (fst n, nodeToString (fst n))) assoc + edges = map (\(a,b) -> Edge a (nodeToString a) (map (\x-> nodeToString x) b)) assoc + return $ encode edges + + revDisplayInfo :: MonadIO m => m VersionIndex + revDisplayInfo = do + pkgIndex <- liftIO queryGetPackageIndex + prefs <- liftIO queryGetPreferredVersions + return $ getDisplayInfo prefs pkgIndex + + renderReverseWith :: (MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> (Maybe VersionStatus -> Bool) -> m ReversePageRender + renderReverseWith pkg rev filterFunc = do + let rev' = map fst $ Map.toList rev + directCounts <- mapM revDirectCount (pkg:rev') + let counts = zip (pkg:rev') directCounts + toRender (i, i') (pkgname, (version, status)) = if filterFunc status then (,) (i+1, i') $ Just ReverseRender { + rendRevPkg = PackageIdentifier pkgname version, + rendRevStatus = status, + rendRevCount = fromJust $ lookup pkgname counts + } else (,) (i, i'+1) Nothing + (res, rlist) = mapAccumL toRender (0, 0) (Map.toList rev) + pkgCount = fromJust $ lookup pkg counts + return $ ReversePageRender (catMaybes rlist) res pkgCount + + renderReverseRecent :: (MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> m ReversePageRender + renderReverseRecent pkg rev = renderReverseWith pkg rev $ \status -> case status of + Just DeprecatedVersion -> False + Nothing -> False + _ -> True + + renderReverseOld :: (MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> m ReversePageRender + renderReverseOld pkg rev = renderReverseWith pkg rev $ \status -> case status of + Just DeprecatedVersion -> True + Nothing -> True + _ -> False + + -- -- This could also differentiate between direct and indirect dependencies + -- -- with a bit more calculation. + revPackageFlat :: (MonadIO m, MonadCatch m) => PackageName -> m [(PackageName, Int)] + revPackageFlat pkgname = do + memState <- readMemState reverseMemState + deps <- getDependenciesFlat pkgname memState + let depList = Set.toList deps + counts <- mapM (`getTotalCount` memState) depList + return $ zip depList counts + + revPackageStats :: (MonadIO m, MonadCatch m) => PackageName -> m ReverseCount + revPackageStats pkgname = do + (direct, transitive) <- getReverseCount pkgname =<< readMemState reverseMemState + return $ ReverseCount direct transitive + + revDirectCount :: (MonadIO m, MonadCatch m) => PackageName -> m Int + revDirectCount pkgname = do + getDirectCount pkgname =<< readMemState reverseMemState + + -- This returns a list of (package name, direct dependencies, flat dependencies) + -- for all packages. An interesting fact: it even does so for packages which + -- don't exist in the index, except the latter two fields are always zero. This is + -- because no versions of these packages exist, so the union of no versions is + -- still no versions. TODO: use this fact to make an index of dependencies which + -- are not in Hackage at all, which might be useful for fixing accidentally + -- broken packages. + -- + -- The returned list is sorted ascendingly on directCount (see ReverseCount). + revCountForAllPackages :: (MonadIO m, MonadCatch m) => m [(PackageName, ReverseCount)] + revCountForAllPackages = do + index <- liftIO queryGetPackageIndex + let pkgnames = packageNames index + counts <- mapM revPackageStats pkgnames + return . sortOn (directCount . snd) $ zip pkgnames counts + + revForEachVersion :: (MonadThrow m, MonadIO m) => PackageName -> m (Map.Map Version (Set PackageIdentifier)) + revForEachVersion pkg = do + ReverseIndex revs nodemap depmap <- readMemState reverseMemState + index <- liftIO queryGetPackageIndex + nodeid <- Bimap.lookup pkg nodemap + revDepNames <- mapM (`Bimap.lookupR` nodemap) (Set.toList $ suc revs nodeid) + let -- The key is the version of 'pkg', and the values are specific + -- package versions that accept this version of pkg specified in the key + revDepVersions :: [(Version, Set PackageIdentifier)] + revDepVersions = do + x <- nubOrd revDepNames + pkginfo <- PackageIndex.lookupPackageName index pkg + pure (packageVersion pkginfo, dependsOnPkg index (packageId pkginfo) x depmap) + pure $ Map.fromListWith Set.union revDepVersions diff --git a/src/Distribution/Server/Features/ReverseDependencies/State.hs b/src/Distribution/Server/Features/ReverseDependencies/State.hs index 4a95db924..e4f9a9632 100644 --- a/src/Distribution/Server/Features/ReverseDependencies/State.hs +++ b/src/Distribution/Server/Features/ReverseDependencies/State.hs @@ -1,458 +1,258 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - -module Distribution.Server.Features.ReverseDependencies.State where - -import Distribution.Server.Packages.Types -import Distribution.Server.Packages.State () -import Distribution.Server.Packages.Preferred -import Distribution.Server.Packages.PackageIndex (PackageIndex) -import qualified Distribution.Server.Packages.PackageIndex as PackageIndex - -import Distribution.Package -import Distribution.PackageDescription -import Distribution.Version +{-# LANGUAGE TupleSections #-} + +module Distribution.Server.Features.ReverseDependencies.State + ( ReverseIndex(..) + , ReverseDisplay + , ReverseCount(..) + , VersionIndex + , addPackage + , constructReverseIndex + , dependsOnPkg + , emptyReverseIndex + , getDepNames + , getDependencies + , getDependenciesFlat + , getDependenciesFlatRaw + , getDependenciesRaw + , getDirectCount + , getDisplayInfo + , getReverseCount + , getTotalCount + , perPackageReverse + , perVersionReverse + , suc + ) + where -import Data.Acid (Query, Update, makeAcidic) -import Data.List (foldl', union) -import Data.Maybe (maybeToList, fromMaybe) -import Data.SafeCopy (base, deriveSafeCopy) -import Data.Typeable (Typeable) -import Data.Map (Map) +import Prelude hiding (lookup) + +import Control.Arrow ((&&&)) +import Control.Monad (forM) +import Control.Monad.Catch +import Control.Monad.Reader (MonadIO) +import qualified Data.Array as Arr ((!), assocs, accumArray) +import Data.Bimap (Bimap, lookup, lookupR) +import qualified Data.Bimap as Bimap +import Data.Containers.ListUtils (nubOrd) +import Data.List (union) +import Data.Map (Map) import qualified Data.Map as Map -import Data.Set (Set) +import Data.Maybe (catMaybes, mapMaybe, maybeToList) import qualified Data.Set as Set +import Data.Set (Set, fromList, toList, delete) +import Data.Typeable (Typeable) +import Data.Graph (Graph, Vertex) +import qualified Data.Graph as Gr + +import Distribution.Package +import Distribution.PackageDescription +import Distribution.Server.Packages.Types +import Distribution.Server.Framework.MemSize +import Distribution.Server.Features.PreferredVersions.State +import Distribution.Server.Packages.PackageIndex (PackageIndex) +import qualified Distribution.Server.Packages.PackageIndex as PackageIndex +import Distribution.Version -import Data.STRef -import Control.Monad.ST -import Control.Monad.State (put, get) -import Control.Monad.Reader (ask, asks) - --- The main reverse dependencies map is a drawn-out Map PackageId PackageId, --- with an extra component to store ranges (all ranges that *could* be revdeps, --- even if no packages in the index currently satisfy those ranges). --- --- For selected entries of the map (foo, (2.0, (bar, [1.0]), (bar, (1.0, <3)))): --- This means that bar-1.0 depends on a version of foo <3, and foo 2.0 meets this criterion. -type RevDeps = Map PackageName (Map Version (Map PackageName (Set Version)), Map PackageName (Map Version VersionRange)) - -type CombinedDeps = Map PackageName VersionRange - --- TODO: should this be (Maybe (Version, Maybe VersionStatus))? --- it should be possible, albeit with a bit more work, to determine all revdeps --- of a package which don't have any versions presently satisfying them. --- (for an entry (a, b) of RevDeps, take (union a \ b)) -type ReverseDisplay = Map PackageName (Version, Maybe VersionStatus) - -data ReverseIndex = ReverseIndex { - -- this field is a duplication of PackageIndex, so updates don't have - -- to use much information outside of this component, resulting in huge - -- happstack-state event files - duplicatedIndex :: PackageIndex PackageId, - - -- The main reverse dependencies map - reverseDependencies :: RevDeps, - - -- Generated from packageNameClosure. - flattenedReverse :: Map PackageName (Set PackageName), - - -- Cached counts for reverse dependencies. - reverseCount :: Map PackageName ReverseCount -} deriving (Eq, Show, Typeable) +import Data.Binary (encode) +import qualified Data.ByteString.Lazy as BS emptyReverseIndex :: ReverseIndex -emptyReverseIndex = ReverseIndex (PackageIndex.fromList []) - Map.empty Map.empty Map.empty - -data ReverseCount = ReverseCount { - directReverseCount :: Int, - flattenedReverseCount :: Int, - versionReverseCount :: Map Version Int -} deriving (Show, Eq, Typeable, Ord) -emptyReverseCount :: ReverseCount -emptyReverseCount = ReverseCount 0 0 Map.empty - -constructReverseIndex :: PackageIndex PkgInfo -> ReverseIndex -constructReverseIndex index = - let deps = constructRevDeps index - in updateReverseCount $ emptyReverseIndex { - duplicatedIndex = constructDupIndex index, - reverseDependencies = deps, - flattenedReverse = packageNameClosure deps - } - -constructDupIndex :: PackageIndex PkgInfo -> PackageIndex PackageId -constructDupIndex = PackageIndex.fromList - . map packageId - . PackageIndex.allPackages - -updateReverseCount :: ReverseIndex -> ReverseIndex -updateReverseCount index = - let deps = reverseDependencies index - flat = flattenedReverse index - in index { - reverseCount = flip Map.mapWithKey deps $ \pkg (versions, _) -> ReverseCount { - directReverseCount = Map.size . Map.unions $ Map.elems versions, - flattenedReverseCount = maybe 0 Set.size $ Map.lookup pkg flat, - versionReverseCount = Map.map Map.size versions +emptyReverseIndex = ReverseIndex (Gr.buildG (0,0) []) Bimap.empty mempty + +type NodeId = Int +type RevDeps = Graph + +data ReverseIndex = ReverseIndex + { reverseDependencies :: !RevDeps + , packageNodeIdMap :: !(Bimap PackageName NodeId) + , deps :: Map PackageIdentifier [Dependency] + } deriving (Eq, Show, Typeable) + +instance MemSize Dependency where + memSize = fromIntegral . BS.length . encode + +instance MemSize ReverseIndex where + memSize (ReverseIndex a b c) = memSize3 a b c + +constructReverseIndex :: MonadCatch m => PackageIndex PkgInfo -> m ReverseIndex +constructReverseIndex index = do + let nodePkgMap = foldr (uncurry Bimap.insert) Bimap.empty $ zip (PackageIndex.allPackageNames index) [0..] + (revs, dependencies) <- constructRevDeps index nodePkgMap + pure $ + ReverseIndex + { reverseDependencies = revs + , packageNodeIdMap = nodePkgMap + , deps = dependencies } - } - -addPackage :: PackageId -> CombinedDeps - -> ReverseIndex -> (ReverseIndex, Map PackageName [Version]) -addPackage pkgid deps revs = - let index = PackageIndex.insert pkgid (duplicatedIndex revs) - (rd, rec) = registerPackage (getAllVersions index) pkgid deps (reverseDependencies revs) - in (updateReverseCount $ revs { - duplicatedIndex = index, - reverseDependencies = rd, - flattenedReverse = packageNameClosure rd - }, rec) - -removePackage :: PackageId -> CombinedDeps - -> ReverseIndex -> (ReverseIndex, Map PackageName [Version]) -removePackage pkgid deps revs = - let index = PackageIndex.deletePackageId pkgid (duplicatedIndex revs) - (rd, rec) = unregisterPackage (getAllVersions index) pkgid deps (reverseDependencies revs) - in (updateReverseCount $ revs { - duplicatedIndex = index, - reverseDependencies = rd, - flattenedReverse = packageNameClosure rd - }, rec) - -changePackage :: PackageId -> CombinedDeps -> CombinedDeps - -> ReverseIndex -> (ReverseIndex, Map PackageName [Version]) -changePackage pkgid deps deps' revs = - let index = PackageIndex.insert pkgid (duplicatedIndex revs) - allVersions = getAllVersions index - (rd, rec) = unregisterPackage allVersions pkgid deps (reverseDependencies revs) - (rd', rec') = registerPackage allVersions pkgid deps' rd - in (updateReverseCount $ revs { - duplicatedIndex = index, - reverseDependencies = rd', - flattenedReverse = packageNameClosure rd' - }, Map.unionWith union rec rec') --------------------------------------------------------------------------------- --- Managing the RevDeps index. - -constructRevDeps :: PackageIndex PkgInfo -> RevDeps -constructRevDeps index = foldl' (\revs pkg -> fst $ registerPackage (getAllVersions index) (packageId pkg) (getAllDependencies pkg) revs) Map.empty $ PackageIndex.allPackages index - -getAllVersions :: Package pkg => PackageIndex pkg -> PackageName -> [Version] -getAllVersions index = map packageVersion . PackageIndex.lookupPackageName index - --- | Given a package id, modify the entries of the package's dependencies in --- the reverse dependencies mapping to include it. -registerPackage :: (PackageName -> [Version]) - -> PackageId -> CombinedDeps - -> RevDeps -> (RevDeps, Map PackageName [Version]) -registerPackage getVersions (PackageIdentifier name version) ranges revs = - let deps = getLinkedNodes getVersions ranges - revs' = foldl' goRegister revs $ Map.toList $ Map.intersectionWith (,) ranges deps - revs'' = backtrace revs' - in (revs'', deps) - where - pkgMap = Map.singleton name $ Set.singleton version - -- this takes each of the registered packages dependencies and puts an entry - -- of it there. e.g. a new version of base would not have much work to do - -- here because it has no dependencies - goRegister prev (pkgname, (range, versions)) = - -- revPackage encodes the dependency (name -> pkgname) in a way that can - -- be inserted into pkgname's reverse dependency mapping - let revPackage = Map.fromList $ map (\v -> (v, pkgMap)) versions - revRange = Map.singleton name (Map.singleton version range) - in Map.insertWith (\(small, small') (big, big') -> (Map.unionWith (Map.unionWith Set.union) big small, Map.unionWith (flip const) big' small')) pkgname (revPackage, revRange) prev - -- this uses the package's existing reverse dependencies, regardless of - -- version, to find reverse dependencies for this version in particular. - -- e.g. a new version of base would have to recalculate the dependencies - -- of nearly all of the packages in the index - backtrace prev = case Map.lookup name prev of - Nothing -> prev - Just (vs, rs) -> - let revVersion = Map.map (Set.fromList . map fst . filter (withinRange version . snd) . Map.toList) rs - in Map.insert name (Map.insertWith (\new old -> Map.unionWith Set.union old new) version revVersion vs, rs) prev - --- | Given a package id, modify the entries of the package's dependencies in --- the reverse dependencies mapping to exclude it. -unregisterPackage :: (PackageName -> [Version]) - -> PackageId -> CombinedDeps - -> RevDeps -> (RevDeps, Map PackageName [Version]) -unregisterPackage getVersions (PackageIdentifier name version) ranges revs = - let deps = getLinkedNodes getVersions ranges - revs' = foldl' goUnregister revs $ Map.toList $ Map.intersectionWith (,) ranges deps - revs'' = backtrace revs' - in (revs'', deps) - where - pkgMap = Map.singleton name $ Set.singleton version - goUnregister prev (pkgname, (range, versions)) = - let revPackage = Map.fromList $ map (\v -> (v, pkgMap)) versions - revRange = Map.singleton pkgname (Map.fromList $ map (\v -> (v, range)) versions) - -- there are possibly better ways to go about this - in Map.differenceWith (\(a, b) (c, d) -> keepMaps - ( Map.differenceWith (\e f -> - keepMap $ Map.differenceWith (\g h -> - keepSet $ Set.difference g h) - e f) - a c - , Map.differenceWith (\e f -> - keepMap $ Map.difference e f) - b d - )) prev (Map.singleton pkgname (revPackage, revRange)) - backtrace prev = Map.update (\(vs, rs) -> keepMaps (Map.delete version vs, rs)) name prev --------------------------------------------------------------------------------- --- Calculating dependencies and selecting versions - --- | Given a package, determine the packages on which it depends. --- For all such packages, return the specific versions that satisfy the --- dependency as indicated in the cabal file. -getLinkedNodes :: (PackageName -> [Version]) - -> CombinedDeps -> Map PackageName [Version] -getLinkedNodes getVersions pkgs = Map.mapWithKey (\pkg range -> selectVersions range $ getVersions pkg) pkgs - --- | Given a dependency (a package name and a version range), find all versions --- in the current package index that satisfy it. -selectVersions :: VersionRange -> [Version] -> [Version] -selectVersions range versions= filter (flip withinRange range) versions - --- | Collect all dependencies specified in a package's cabal file, considering --- all alternatives. --- --- This unions all version ranges together from different branches, which is --- imprecise but mostly good enough (a slightly better heuristic might be --- intersecting within a block, but unioning blocks together). -getAllDependencies :: PkgInfo -> CombinedDeps -getAllDependencies pkg = - let desc = pkgDesc pkg - in Map.fromListWith unionVersionRanges $ toDepsList (maybeToList $ condLibrary desc) - ++ toDepsList (map snd $ condExecutables desc) - where toDepsList :: [CondTree v [Dependency] a] -> [(PackageName, VersionRange)] - toDepsList l = [ (p, v) | Dependency p v <- concatMap harvestDependencies l ] +addPackage :: (MonadCatch m, MonadIO m) => PackageIndex PkgInfo -> PackageName -> [PackageName] + -> ReverseIndex -> m ReverseIndex +addPackage index pkgname dependencies ri@(ReverseIndex revs nodemap pkgIdToDeps) = do + let + npm = Bimap.tryInsert pkgname (Bimap.size nodemap) nodemap + new :: [(Int, [Int])] <- + forM dependencies $ \d -> + (,) <$> lookup d npm <*> fmap (:[]) (lookup pkgname npm) + let rd = insEdges (Bimap.size npm) new revs + pkginfos = PackageIndex.lookupPackageName index pkgname + newPackageDepMap = Map.fromList $ map (packageId &&& getDeps) pkginfos + pure + ri + { reverseDependencies = rd + , packageNodeIdMap = npm + , deps = Map.union newPackageDepMap pkgIdToDeps + } + +constructRevDeps :: forall m. MonadCatch m => PackageIndex PkgInfo -> Bimap PackageName NodeId -> m (RevDeps, Map PackageIdentifier [Dependency]) +constructRevDeps index nodemap = do + let allPackages :: [PkgInfo] + allPackages = concat $ PackageIndex.allPackagesByName index + nodeIdsOfDependencies :: PkgInfo -> m [(NodeId, NodeId)] + nodeIdsOfDependencies pkg = catMaybes <$> mapM findNodesIfPresent (getDepNames pkg) + where + findNodesIfPresent :: PackageName -> m (Maybe (NodeId, NodeId)) + findNodesIfPresent dep = do + eitherErrOrFound :: Either SomeException (NodeId, NodeId) <- + try $ (,) <$> lookup dep nodemap <*> lookup (packageName pkg) nodemap + pure $ either (const Nothing) Just eitherErrOrFound + -- This will mix dependencies of different versions of the same package, but that is intended. + edges <- traverse nodeIdsOfDependencies allPackages + let dependencies = Map.fromList $ map (packageId &&& getDeps) allPackages + + pure (Gr.buildG (0, Bimap.size nodemap) (nubOrd $ concat edges) + , dependencies + ) + +getDeps :: PkgInfo -> [Dependency] +getDeps pkg = + concatMap harvestDependencies (maybeToList $ condLibrary $ pkgDesc pkg) + +getDepNames :: PkgInfo -> [PackageName] +getDepNames pkg = + map depPkgName $ getDeps pkg + +-- | Returns [containers 0.5.0.0 to 0.6.0.1] for needle=ghc-prim-0.6.0.0 and packageHaystack=containers +-- | because these are the versions that could accept that version of ghc-prim as a dep +-- | Note that this doesn't include executables! Only the library. +dependsOnPkg :: PackageIndex PkgInfo -> PackageId -> PackageName -> Map PackageIdentifier [Dependency] -> Set PackageIdentifier +dependsOnPkg index needle packageHaystack dependencies = + fromList $ mapMaybe descPermits (PackageIndex.lookupPackageName index packageHaystack) + where + descPermits :: PkgInfo -> Maybe PackageIdentifier + descPermits pkginfo + | Just found <- Map.lookup (packageId pkginfo) dependencies + , any toDepsList found = Just (packageId pkginfo) + | otherwise = Nothing + toDepsList (Dependency name versionRange _) = + packageVersion needle `withinRange` versionRange + && packageName needle == name -- | Collect all dependencies from all branches of a condition tree. harvestDependencies :: CondTree v [Dependency] a -> [Dependency] -harvestDependencies (CondNode _ deps comps) = deps ++ concatMap forComponent comps - where forComponent (_, iftree, elsetree) = harvestDependencies iftree ++ maybe [] harvestDependencies elsetree +harvestDependencies (CondNode _ dependencies comps) = dependencies ++ concatMap forComponent comps + where forComponent (CondBranch _ iftree elsetree) = harvestDependencies iftree ++ maybe [] harvestDependencies elsetree + +---------------------------------ReverseDisplay -------------------------------------------------------------------------------- -- Calculating ReverseDisplays -type VersionIndex = (PackageName -> (PreferredInfo, [Version])) +data ReverseCount = ReverseCount + { directCount :: Int + , totalCount :: Int + } deriving (Show, Eq, Typeable, Ord) --- TODO: this should use the secondary PackageId -> VersionRange mapping in RevDeps, --- so it gets all possible versions, not just those currently in the index. -perPackageReverse :: VersionIndex -> RevDeps -> PackageName -> ReverseDisplay -perPackageReverse indexFunc revs pkg = case Map.lookup pkg revs of - Nothing -> Map.empty - Just (dict, _) -> constructReverseDisplay indexFunc (Map.unionsWith Set.union $ Map.elems dict) +instance MemSize ReverseCount where + memSize (ReverseCount a b) = memSize2 a b -perVersionReverse :: VersionIndex -> RevDeps -> PackageId -> ReverseDisplay -perVersionReverse indexFunc revs pkg = case Map.lookup (packageVersion pkg) . fst =<< Map.lookup (packageName pkg) revs of - Nothing -> Map.empty - Just dict -> constructReverseDisplay indexFunc dict +type ReverseDisplay = Map PackageName (Version, Maybe VersionStatus) -constructReverseDisplay :: VersionIndex -> Map PackageName (Set Version) -> ReverseDisplay -constructReverseDisplay indexFunc deps = - Map.mapMaybeWithKey (uncurry maybeBestVersion . indexFunc) deps +type VersionIndex = (PackageName -> (PreferredInfo, [Version])) + +perPackageReverse :: MonadCatch m => (PackageName -> (PreferredInfo, [Version])) -> PackageIndex PkgInfo -> ReverseIndex -> PackageName -> m (Map PackageName (Version, Maybe VersionStatus)) +perPackageReverse indexFunc index revdeps pkg = do + let pkgids = (packageVersion. packageId) <$> PackageIndex.lookupPackageName index pkg + let best :: PackageId + best = PackageIdentifier pkg (maximum pkgids) + perVersionReverse indexFunc index revdeps best + +perVersionReverse :: MonadCatch m => (PackageName -> (PreferredInfo, [Version])) -> PackageIndex PkgInfo -> ReverseIndex -> PackageId -> m (Map PackageName (Version, Maybe VersionStatus)) +perVersionReverse indexFunc index (ReverseIndex revs nodemap dependencies) pkg = do + found <- lookup (packageName pkg) nodemap + -- this will be too much, since we are throwing away the specific version + revDepNames :: Set PackageName <- fromList <$> mapM (`lookupR` nodemap) (toList $ suc revs found) + let packagemap :: Map PackageName (Set Version) + packagemap = Map.fromList $ map (\x -> (x, Set.map packageVersion $ dependsOnPkg index pkg x dependencies)) (toList revDepNames) + pure $ constructReverseDisplay indexFunc packagemap + +constructReverseDisplay :: (PackageName -> (PreferredInfo, [Version])) -> Map PackageName (Set Version) -> Map PackageName (Version, Maybe VersionStatus) +constructReverseDisplay indexFunc = + Map.mapMaybeWithKey (uncurry maybeBestVersion . indexFunc) getDisplayInfo :: PreferredVersions -> PackageIndex PkgInfo -> VersionIndex getDisplayInfo preferred index pkgname = (,) (Map.findWithDefault emptyPreferredInfo pkgname $ preferredMap preferred) (map packageVersion . PackageIndex.lookupPackageName index $ pkgname) --------------------------------------------------------------------------------- --- Keeping a cached map of selected versions. --- --- Currently it's rather quick to calculate each, and the majority of processing --- will probably go towards rendering it in HTML/JSON/whathaveyou anyway. So this --- is not used. --- --- Still, in a future Hackage where preferred-versions and deprecated versions --- are *very* widely used, incremental updates of an display index might become --- necessary. In such a future, there would be two maps in the ReverseIndex --- structure, both to ReverseDisplay from PackageName and PackageId. They would --- need to be updated with updatePackageReverse and updateVersionReverse, --- respectively, whenever a package is added, removed, or has its preferred info --- changed. - -constructPackageReverse :: VersionIndex -> RevDeps -> Map PackageName ReverseDisplay -constructPackageReverse indexFunc revs = - Map.fromList $ do - pkg <- Map.keys revs - rev <- maybeToList . keepMap $ perPackageReverse indexFunc revs pkg - return (pkg, rev) - -constructVersionReverse :: VersionIndex -> RevDeps -> Map PackageId ReverseDisplay -constructVersionReverse indexFunc revs = - Map.fromList $ do - pkg <- getNodes =<< Map.toList revs - rev <- maybeToList . keepMap $ perVersionReverse indexFunc revs pkg - return (pkg, rev) - where - getNodes :: (PackageName, (Map Version a, b)) -> [PackageId] - getNodes (name, (versions, _)) = map (PackageIdentifier name) $ Map.keys versions - --- | With a package which has just been updated, make sure the version displayed --- in its reverse display is the most recent. To do this, each of its dependencies --- needs its ReverseDisplay updated. -updateReverseDisplay :: VersionIndex -> PackageName -> Set Version -> ReverseDisplay -> ReverseDisplay -updateReverseDisplay indexFunc pkgname versions revDisplay = - let toVersions = uncurry maybeBestVersion . indexFunc - in case toVersions pkgname versions of - Nothing -> revDisplay - Just status -> Map.insert pkgname status revDisplay - --- If the RevDeps index is modified through registering/unregistering packages, --- updatePackageReverse and updateVersionReverse should be given a list of --- package names/package ids distrilled from the resultant (Map PackageName --- [Version]). The idea is to sync it with the just-updated RevDeps. --- --- If a package's PreferredVersions are modified, these functions should be --- called with the same information taken from the getLinkedNodes function. --- In this case, the RevDeps data structure hasn't changed. - -updatePackageReverse :: VersionIndex -> PackageName -> [PackageName] -> RevDeps -> Map PackageName ReverseDisplay -> Map PackageName ReverseDisplay -updatePackageReverse indexFunc updated deps revs nameMap = - foldl' (\revd pkg -> Map.alter (alterRevDisplay pkg . fromMaybe Map.empty) pkg revd) nameMap deps - where - lookupVersions :: PackageName -> Set Version - lookupVersions pkgname = maybe Set.empty (Set.unions . map (Map.findWithDefault Set.empty updated) . Map.elems . fst) $ Map.lookup pkgname revs - alterRevDisplay :: PackageName -> ReverseDisplay -> Maybe ReverseDisplay - alterRevDisplay pkgname rev = keepMap $ updateReverseDisplay indexFunc updated (lookupVersions pkgname) rev - -updateVersionReverse :: VersionIndex -> PackageName -> [PackageId] -> RevDeps -> Map PackageId ReverseDisplay -> Map PackageId ReverseDisplay -updateVersionReverse indexFunc updated deps revs pkgMap = - foldl' (\revd pkg -> Map.alter (alterRevDisplay pkg . fromMaybe Map.empty) pkg revd) pkgMap deps - where - lookupVersions :: PackageId -> Set Version - lookupVersions pkgid = maybe Set.empty (Map.findWithDefault Set.empty updated) $ Map.lookup (packageVersion pkgid) . fst =<< Map.lookup (packageName pkgid) revs - alterRevDisplay :: PackageId -> ReverseDisplay -> Maybe ReverseDisplay - alterRevDisplay pkgid rev = keepMap $ updateReverseDisplay indexFunc updated (lookupVersions pkgid) rev - --------------------------------------------------------------------------------- --- Flattening the graph --- Exposing indirect dependencies is as simple as taking the set difference --- of the edges of a node in the dependency graph G and its closure G+. - --- Collect all indirect versioned dependencies. This takes around 45 seconds --- on the current package index (in ghci). It probably isn't worth exposing. -packageIdClosure :: RevDeps -> Map PackageId (Set PackageId) -packageIdClosure revs = Map.fromDistinctAscList $ transitiveClosure - (concatMap getNodes $ Map.toList revs) - (\pkg -> maybe [] (concatMap getEdges . Map.toList) - $ Map.lookup (packageVersion pkg) . fst =<< Map.lookup (packageName pkg) revs) - where - getNodes :: (PackageName, (Map Version a, b)) -> [PackageId] - getNodes (name, (versions, _)) = map (PackageIdentifier name) $ Map.keys versions - - getEdges :: (PackageName, Set Version) -> [PackageId] - getEdges (name, versions) = map (PackageIdentifier name) $ Set.toList versions - --- Collect all indirect name dependencies. This takes around 2 seconds on the --- current package index. It should be fine to reconstruct from scratch every --- time the package index is updated, since code to incrementally update a --- transitive closure can be messy and stateful and complicated. -packageNameClosure :: RevDeps -> Map PackageName (Set PackageName) -packageNameClosure revs = Map.fromDistinctAscList $ transitiveClosure - (Map.keys revs) - (\pkg -> maybe [] (concatMap Map.keys . Map.elems . fst) - $ Map.lookup pkg revs) - --- Get the transitive closure of a graph from the set of nodes and a neighbor --- function. This implementation uses depth-first search in the ST monad --- where cycles are broken if a visited node has been seen before. --- --- The same basic algorithm could be used to make a DAG structure. -transitiveClosure :: forall a. Ord a => [a] -> (a -> [a]) -> [(a, Set a)] -transitiveClosure core edges = runST $ do - list <- mapM (\node -> newSTRef Nothing >>= \ref -> return (node, ref)) core - let visited = Map.fromList list - mapM_ (collect visited) core - list' <- mapM (\(node, ref) -> readSTRef ref >>= \val -> return (node, val)) list - return [ (node, nodes) | (node, Just nodes) <- list' ] - where - collect :: Map a (STRef s (Maybe (Set a))) -> a -> ST s (Set a) - collect links node = do - case Map.lookup node links of - -- attempting to visit a node which wasn't given to us - Nothing -> return Set.empty - Just ref -> readSTRef ref >>= \t -> case t of - -- the node has already been visited - Just calc -> return calc - Nothing -> do - -- Mark the node as visited with results pending. If a cycle - -- brings us back to collect the same node, it will yield - -- an empty list. This breaks the cycle for whatever node - -- was visited first. - -- - -- There are smarter algorithms that can - -- get transitive closures of cyclic graphs, but cycles are - -- highly pathological for the types of graphs we'll be - -- traversing, so no need to worry. - writeSTRef ref $ Just Set.empty - let outEdges = edges node - collected <- fmap Set.unions $ mapM (collect links) outEdges - let connected = Set.union (Set.fromList outEdges) collected - writeSTRef ref $ Just connected - return connected ------------------------------------ Utility --- For cases when, if a Map or Set is empty, it's as good as nothing at all. -keepMap :: Ord k => Map k a -> Maybe (Map k a) -keepMap con = if Map.null con then Nothing else Just con - -keepMaps :: (Ord k, Ord k') => (Map k a, Map k' b) -> Maybe (Map k a, Map k' b) -keepMaps con@(c, c') = if Map.null c && Map.null c' then Nothing else Just con - -keepSet :: Ord a => Set a -> Maybe (Set a) -keepSet con = if Set.null con then Nothing else Just con - --------------------------------------------------------------------------------- --- State --- --- Last but agnostic of other ranking schemes, --- methods for manipulating the global state. - -deriveSafeCopy 0 'base ''ReverseIndex -deriveSafeCopy 0 'base ''ReverseCount - -initialReverseIndex :: ReverseIndex -initialReverseIndex = emptyReverseIndex - -getReverseIndex :: Query ReverseIndex ReverseIndex -getReverseIndex = ask - -replaceReverseIndex :: ReverseIndex -> Update ReverseIndex () -replaceReverseIndex = put - -addReversePackage :: PackageId -> CombinedDeps -> Update ReverseIndex (Map PackageName [Version]) -addReversePackage pkgid deps = get >>= \revs -> - let (revs', rec) = addPackage pkgid deps revs - in put revs' >> return rec - -removeReversePackage :: PackageId -> CombinedDeps -> Update ReverseIndex (Map PackageName [Version]) -removeReversePackage pkgid deps = get >>= \revs -> - let (revs', rec) = removePackage pkgid deps revs - in put revs' >> return rec - -changeReversePackage :: PackageId -> CombinedDeps -> CombinedDeps -> Update ReverseIndex (Map PackageName [Version]) -changeReversePackage pkgid deps deps' = get >>= \revs -> - let (revs', rec) = changePackage pkgid deps deps' revs - in put revs' >> return rec - -getReverseCount :: PackageName -> Query ReverseIndex ReverseCount -getReverseCount pkg = asks $ Map.findWithDefault emptyReverseCount pkg . reverseCount - -getFlattenedReverse :: PackageName -> Query ReverseIndex (Set PackageName) -getFlattenedReverse pkg = asks $ Map.findWithDefault Set.empty pkg . flattenedReverse - -makeAcidic ''ReverseIndex ['getReverseIndex - ,'replaceReverseIndex - ,'addReversePackage - ,'removeReversePackage - ,'changeReversePackage - ,'getReverseCount - ,'getFlattenedReverse - ] +----------------------------Graph Utility---------- +suc :: RevDeps -> Vertex -> Set Vertex +suc g v = fromList $ g Arr.! v + +insEdges :: Int -> [(NodeId, [NodeId])] -> RevDeps -> RevDeps +insEdges nodesize edges revdeps = Arr.accumArray union [] (0, nodesize) (edges ++ Arr.assocs revdeps) + +-------------------------------------- + +getDependencies :: MonadCatch m => PackageName -> ReverseIndex -> m (Set PackageName) +getDependencies pkg revs = + names revs =<< getDependenciesRaw pkg revs + +getDependenciesRaw :: MonadCatch m => PackageName -> ReverseIndex -> m (Set NodeId) +getDependenciesRaw pkg (ReverseIndex revdeps nodemap _) = do + enodeid <- try (lookup pkg nodemap) + onRight enodeid $ \nodeid -> + nodeid `delete` suc revdeps nodeid + +-- | The flat/total/transitive/indirect reverse dependencies are all the packages that depend on something that depends on the given 'pkg' +getDependenciesFlat :: forall m. MonadCatch m => PackageName -> ReverseIndex -> m (Set PackageName) +getDependenciesFlat pkg revs = + names revs =<< getDependenciesFlatRaw pkg revs + +getDependenciesFlatRaw :: forall m. MonadCatch m => PackageName -> ReverseIndex -> m (Set NodeId) +getDependenciesFlatRaw pkg (ReverseIndex revdeps nodemap _) = do + enodeid <- try (lookup pkg nodemap) + onRight enodeid $ \nodeid -> + nodeid `delete` fromList (Gr.reachable revdeps nodeid) + +-- | The direct dependencies depend on the given 'pkg' directly, i.e. not transitively +getDirectCount :: MonadCatch m => PackageName -> ReverseIndex -> m Int +getDirectCount pkg revs = do + length <$> getDependenciesRaw pkg revs + +-- | Given a set of NodeIds, look up the package names for all of them +names :: MonadThrow m => ReverseIndex -> Set NodeId -> m (Set PackageName) +names (ReverseIndex _ nodemap _) ids = do + fromList <$> mapM (`lookupR` nodemap) (toList ids) + +onRight :: Monad m => Either SomeException t -> (t -> Set NodeId) -> m (Set NodeId) +onRight e fun = do + case e of + Left (_ :: SomeException) -> do + pure mempty + Right nodeid -> + pure $ fun nodeid + +-- | The flat/total/transitive/indirect dependency count is the amount of package names that depend transitively on the given 'pkg' +getTotalCount :: MonadCatch m => PackageName -> ReverseIndex -> m Int +getTotalCount pkg revs = do + length <$> getDependenciesFlatRaw pkg revs + +getReverseCount :: MonadCatch m => PackageName -> ReverseIndex -> m (Int, Int) +getReverseCount pkg revs = do + direct <- getDirectCount pkg revs + total <- getTotalCount pkg revs + pure (direct, total) diff --git a/src/Distribution/Server/Framework/MemSize.hs b/src/Distribution/Server/Framework/MemSize.hs index 4af5d251f..d98e00085 100644 --- a/src/Distribution/Server/Framework/MemSize.hs +++ b/src/Distribution/Server/Framework/MemSize.hs @@ -17,6 +17,10 @@ import Data.Set (Set) import qualified Data.IntSet as IntSet import Data.IntSet (IntSet) import Data.Sequence (Seq) +import qualified Data.Bimap as Bimap +import Data.Bimap (Bimap) +import qualified Data.Graph as Gr +import Data.Graph (Graph) import qualified Data.Foldable as Foldable import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS @@ -223,6 +227,12 @@ instance MemSize e => MemSize (V.Vector e) where memSizeUVector :: V.U.Unbox e => Int -> V.U.Vector e -> Int memSizeUVector sz a = 5 + (V.U.length a * sz) `div` wordSize +instance (MemSize a, MemSize b) => MemSize (Bimap a b) where + memSize m = sum [ 6 + memSize k + memSize v | (k,v) <- Bimap.toList m ] + +instance MemSize Graph where + memSize m = sum [ 6 + memSize v | v <- Gr.edges m ] + ---- diff --git a/src/Distribution/Server/Packages/PackageIndex.hs b/src/Distribution/Server/Packages/PackageIndex.hs index 4b862d649..228e5b768 100644 --- a/src/Distribution/Server/Packages/PackageIndex.hs +++ b/src/Distribution/Server/Packages/PackageIndex.hs @@ -44,7 +44,8 @@ module Distribution.Server.Packages.PackageIndex ( -- ** Bulk queries allPackageNames, allPackages, - allPackagesByName + allPackagesByName, + allPackagesByNameNE ) where import Distribution.Server.Prelude hiding (lookup) @@ -58,6 +59,7 @@ import qualified Data.Map.Strict as Map import Data.Map.Strict (Map) import qualified Data.Foldable as Foldable import Data.List (groupBy, find, isInfixOf) +import qualified Data.List.NonEmpty as NE import Data.SafeCopy import Distribution.Types.PackageName @@ -255,6 +257,9 @@ allPackages (PackageIndex m) = concat (Map.elems m) -- -- They are grouped by package name, case-sensitively. -- +allPackagesByNameNE :: Package pkg => PackageIndex pkg -> [NE.NonEmpty pkg] +allPackagesByNameNE (PackageIndex m) = map NE.fromList $ Map.elems m + allPackagesByName :: Package pkg => PackageIndex pkg -> [[pkg]] allPackagesByName (PackageIndex m) = Map.elems m diff --git a/src/Distribution/Server/Pages/Reverse.hs b/src/Distribution/Server/Pages/Reverse.hs index 69a371c0a..aa8a288aa 100644 --- a/src/Distribution/Server/Pages/Reverse.hs +++ b/src/Distribution/Server/Pages/Reverse.hs @@ -1,177 +1,141 @@ +{-# LANGUAGE NamedFieldPuns, RecordWildCards, BlockArguments #-} module Distribution.Server.Pages.Reverse ( - reversePackageRender, - reverseFlatRender, - reverseStatsRender, - reversePackagesRender, - reversePackageSummary + ReverseHtmlUtil(..) + , reverseHtmlUtil + , LatestOrOld(..) ) where import Distribution.Server.Features.ReverseDependencies -import Distribution.Server.Packages.Reverse -import Distribution.Server.Packages.Preferred +import Distribution.Server.Features.PreferredVersions + import Distribution.Package import Distribution.Text (display) import Distribution.Version +import Data.Function ((&)) import qualified Data.Map as Map +import Data.Set (Set, toList) import Text.XHtml.Strict -reversePackageRender :: PackageId -- ^ The package whose information is displayed. - -> (PackageId -> String) -- ^ Generating URIs for package pages. - -> ReverseResource -- ^ The resource for generating revdeps-related URIs. - -> Bool -- ^ Whether the ReverseDisplay was rendered for recent (True) or older (False) versions of the package. - -> ReversePageRender -- ^ Obtained from a ReverseDisplay-rendering function. - -> [Html] -reversePackageRender pkgid packageLink r isRecent (ReversePageRender renders counts total) = - let packageAnchor = anchor ! [href $ packageLink pkgid] << display pkgid - hasVersion = packageVersion pkgid /= Version [] [] - pkgname = packageName pkgid - statLinks = paragraph << - [ toHtml "Check out the " - , anchor ! [href $ reverseStatsUri r "" pkgname] << "statistics for specific versions" - , toHtml $ " of " ++ display pkgid ++ " and its " - , anchor ! [href $ reverseAllUri r "" pkgname] << "indirect dependencies", toHtml "." ] - versionBox = if hasVersion && total /= allCounts - then thediv ! [theclass "notification"] << [toHtml $ "These statistics only apply to this version of " ++ display pkgname ++ ". See also ", anchor ! [href $ reverseNameUri r "" pkgname] << [toHtml "packages which depend on ", emphasize << "any", toHtml " version"], toHtml $ " (all " ++ show total ++ " of them)."] - else noHtml - allCounts = uncurry (+) counts - otherCount = case total - allCounts of - diff | diff > 0 -> paragraph << [show diff ++ " packages depend on versions of " ++ display pkgid ++ " other than this one."] - _ -> noHtml - (pageText, nonPageText) = (if isRecent then id else uncurry $ flip (,)) (recentText, nonRecentText) - otherLink = if isRecent then reverseOldUri r "" pkgid else reverseUri r "" pkgid - in h2 << (display pkgid ++ ": " ++ num allCounts "reverse dependencies" "reverse dependency"):versionBox:case counts of - (0, 0) -> - [ paragraph << [toHtml "No packages depend on ", - packageAnchor, toHtml "."] - ] - (0, count) -> - [ paragraph << [toHtml "No packages depend on ", - if hasVersion then noHtml else toHtml "some version of ", - packageAnchor, - toHtml $ pageText 0 ++ " However, ", - altVersions count nonPageText otherLink, - toHtml "."] - ] ++ [otherCount, statLinks] - (count, count') -> - [ (paragraph<<) $ [ mainVersions count pageText packageAnchor, toHtml " (listed below)." ] - ++ if count' > 0 then [ toHtml " Additionally, " - , altVersions count' nonPageText otherLink - , toHtml $ ". That's " ++ show (count+count') ++ " in total." ] - else [] - ] ++ (if isRecent then [] else [paragraph << oldText]) ++ [otherCount, statLinks, reverseTable] - where - mainVersions count textFunc pkgLink = toHtml - [ toHtml $ num count "packages depend on " "package depends on " - , pkgLink - , toHtml $ textFunc count - ] - altVersions count textFunc altLink = toHtml - [ anchor ! [href altLink] << num count "packages" "package" - , toHtml $ num' count " depend on " " depends on " ++ display pkgid ++ textFunc count - ] - recentText count = ' ':num' count "in their latest versions" "in its latest version" - nonRecentText count = ' ':num' count "only in older or deprecated versions" "only in an older or deprecated version" - oldText = "The latest version of each package below, which doesn't depend on " ++ display pkgid ++ ", is linked from the first column. The version linked from the second column is the one which has a dependency on " ++ display pkgid ++", but it's no longer the preferred installation candidate. Note that packages which depend on versions of " ++ display pkgid ++ " not uploaded to Hackage are treated as not depending on it at all." - - reverseTable = thediv << table << reverseTableRows - reverseTableRows = - [ tr << [ th << "Package name", th << "Version", th << "Reverse dependencies" ] ] ++ - [ tr ! [theclass (if odd n then "odd" else "even")] << - [ td << anchor ! [href $ packageLink $ PackageIdentifier (packageName pkg) $ Version [] [] ] << display (packageName pkg) - , td << anchor ! (renderStatus status ++ [href $ packageLink pkg]) << display (packageVersion pkg) - , td << [ toHtml $ (show count) ++ " (", anchor ! [href $ reverseNameUri r "" $ packageName pkg] << "view", toHtml ")" ] ] - | (ReverseRender pkg status count, n) <- zip renders [(1::Int)..] ] - - renderStatus (Just DeprecatedVersion) = [theclass "deprecated"] - renderStatus (Just UnpreferredVersion) = [theclass "unpreferred"] - renderStatus _ = [] - -reverseFlatRender :: PackageName -> (PackageName -> String) -> ReverseResource -> ReverseCount -> [(PackageName, Int)] -> [Html] -reverseFlatRender pkgname packageLink r (ReverseCount total flat _) pairs = - h2 << (display pkgname ++ ": " ++ num flat "total reverse dependencies" "reverse dependency"):case (total, flat) of - (0, 0) -> [paragraph << [toHtml "No packages depend on ", toPackage pkgname]] - _ -> - [ paragraph << if total == flat - then [toHtml "All packages which use ", toPackage pkgname, toHtml " depend on it ", anchor ! [href $ reverseNameUri r "" pkgname] << "directly", toHtml $ ". " ++ onlyPackage total] - else [toPackage pkgname, toHtml " has ", anchor ! [href $ reverseNameUri r "" pkgname] << num total "packages" "package", toHtml $ " which directly " ++ num' total "depend" "depends" ++ " on it, but there are more packages which depend on ", emphasize << "those", toHtml $ " packages. If you flatten the tree of reverse dependencies, you'll find " ++ show flat ++ " packages which use " ++ display pkgname ++ ", and " ++ show (flat-total) ++ " which do so without depending directly on it. All of these packages are listed below."] - , paragraph << [toHtml "See also the ", anchor ! [href $ reverseStatsUri r "" pkgname] << "statistics for specific versions", toHtml $ " of " ++ display pkgname ++ "."] - , reverseTable - ] - where - toPackage pkg = anchor ! [href $ packageLink pkg] << display pkg +data LatestOrOld + = OnlyLatest + | OnlyOlder + deriving Eq - onlyPackage count = if count == 1 then "There's only one:" else "There are " ++ show count ++ ":" +data ReverseHtmlUtil = ReverseHtmlUtil { + reversePackageRender :: PackageId -> (PackageId -> String) -> LatestOrOld -> ReversePageRender -> [Html] + , reverseFlatRender :: PackageName -> (PackageName -> String) -> ReverseCount -> [(PackageName, Int)] -> [Html] + , reverseVerboseRender :: PackageName -> [Version] -> (PackageId -> String) -> ReverseCount -> (Map.Map Version (Set PackageIdentifier)) -> [Html] + , reversePackagesRender :: (PackageName -> String) -> Int -> [(PackageName, ReverseCount)] -> [Html] + } - reverseTable = thediv << table << reverseTableRows - reverseTableRows = - [ tr << [ th << "Package name", th << "Total reverse dependencies" ] ] ++ - [ tr ! [theclass (if odd n then "odd" else "even")] << - [ td << toPackage pkg - , td << [ toHtml $ (show count) ++ " (", anchor ! [href $ reverseAllUri r "" pkg] << "view", toHtml ")" ] - ] - | ((pkg, count), n) <- zip pairs [(1::Int)..] ] - --- /package/:package/reverse/summary -reverseStatsRender :: PackageName -> [Version] -> (PackageId -> String) -> ReverseResource -> ReverseCount -> [Html] -reverseStatsRender pkgname allVersions packageLink r (ReverseCount total flat versions) = - h2 << (display pkgname ++ ": reverse dependency statistics"): - [ case total of - 0 -> paragraph << [ toHtml "No packages depend on ", thisPackage, toHtml "." ] - _ -> toHtml - [ paragraph << [ anchor ! [href $ reverseNameUri r "" pkgname] << num total "packages" "package" - , toHtml $ num' total " depend" " depends" - , toHtml " directly on ", thisPackage, toHtml "." ] - , paragraph << [ toHtml $ num (flat-total) "packages depend" "package depends" ++ " indirectly on " ++ display pkgname ++ "." ] - , paragraph << [ anchor ! [href $ reverseAllUri r "" pkgname] << num flat "packages" "package" - , toHtml $ num' flat " depend" " depends" ++ " on " ++ display pkgname ++ " in total." - ] - ] - , versionTable ] +reverseHtmlUtil :: ReverseFeature -> ReverseHtmlUtil +reverseHtmlUtil ReverseFeature{reverseResource} = ReverseHtmlUtil{..} where - toPackage pkgid = anchor ! [href $ packageLink pkgid] << display pkgid - thisPackage = toPackage (PackageIdentifier pkgname $ Version [] []) - - versionTable = thediv << table << versionTableRows - versionTableRows = - [ tr << [ th << "Version", th << "Reverse dependency count" ] ] ++ - [ tr ! [theclass (if odd n then "odd" else "even")] << - [ td << anchor ! [href $ packageLink pkgid ] << display version - , td << [ toHtml $ show (Map.findWithDefault 0 version versions) ++ " (" - , anchor ! [href $ reverseUri r "" pkgid] << "view", toHtml ")" ] + reversePackageRender :: PackageId -- ^ The package whose information is displayed. + -> (PackageId -> String) -- ^ Generating URIs for package pages. + -> LatestOrOld -- ^ Whether the ReverseDisplay was rendered for recent (OnlyLatest) or older (OnlyOlder) versions of the package. + -> ReversePageRender -- ^ Obtained from a ReverseDisplay-rendering function. + -> [Html] + reversePackageRender pkgid packageLink isRecent (ReversePageRender renders (count, count') total) = + let allCounts = count + count' + firstTh = toHtml ("Depend on the " <> (if packageVersion pkgid == nullVersion then "latest" else "given") <> " version") + in h2 << (display pkgid ++ ": " ++ display allCounts ++ " reverse dependencies"): + [ if isRecent == OnlyLatest + then toHtml "No version specified, so showing reverse dependencies for latest version." + else toHtml "" + , table ! [ theclass "fancy" ] + << [ tr << [ th << firstTh, th << toHtml "Depend on other versions", th << toHtml "Total" ] + , tr << [ td << toHtml (display count), td << toHtml (display count'), td << toHtml (display total) ]] + , reverseTable + ] + where + reverseTable = thediv << table << reverseTableRows + reverseTableRows = + tr ! [theclass "fancy"] << [ th << "Package name", th << "Version", th << "Reverse dependencies" ] : + [ tr ! [theclass (if odd n then "odd" else "even")] << + [ td << anchor ! [href $ packageLink $ PackageIdentifier (packageName pkg) $ nullVersion ] << display (packageName pkg) + , td << anchor ! (renderStatus status ++ [href $ packageLink pkg]) << display (packageVersion pkg) + , td << [ toHtml $ (show count'') ++ " (", anchor ! [href $ reverseFlatUri reverseResource "" $ packageName pkg] << "view", toHtml ")" ] ] + | (ReverseRender pkg status count'', n) <- zip renders [(1::Int)..] ] + + renderStatus (Just DeprecatedVersion) = [theclass "deprecated"] + renderStatus (Just UnpreferredVersion) = [theclass "unpreferred"] + renderStatus _ = [] + + renderCount ReverseCount{totalCount, directCount} = + table ! [ theclass "fancy" ] + << [ tr << [ th << firstTh, th << secondTh, th << toHtml "Total" ] + , tr << [ td << toHtml (display directCount), td << toHtml (display (totalCount - directCount)), td << toHtml (display totalCount) ]] + where + firstTh = toHtml "Direct reverse dependencies" + secondTh = toHtml "Indirect reverse dependencies" + + reverseFlatRender :: PackageName -> (PackageName -> String) -> ReverseCount -> [(PackageName, Int)] -> [Html] + reverseFlatRender pkgname packageLink revCount pairs = + h2 << (display pkgname ++ ": " ++ "total reverse dependencies"):renderCount revCount:[reverseTable] + where + + toPackage pkg = anchor ! [href $ packageLink pkg] << display pkg + + reverseTable = thediv << table << reverseTableRows + reverseTableRows = + (tr ! [theclass "fancy"] << [ th << "Package name", th << "Total reverse dependencies" ]) : + [ tr ! [theclass (if odd n then "odd" else "even")] << + [ td << toPackage pkg + , td << [ toHtml $ (show count) ++ " (", anchor ! [href $ reverseFlatUri reverseResource "" pkg] << "view", toHtml ")" ] + ] + | ((pkg, count), n) <- zip pairs [(1::Int)..] ] + + -- /package/:package/reverse/verbose + reverseVerboseRender :: PackageName -> [Version] -> (PackageId -> String) -> ReverseCount -> (Map.Map Version (Set PackageIdentifier)) -> [Html] + reverseVerboseRender pkgname allVersions packageLink revCount versions = + h2 << (display pkgname ++ ": reverse dependencies per version"): + [ renderCount revCount + , versionTable + ] + + where + versionTable = thediv << (table ! [theclass "fancy"]) << versionTableRows + versionTableRows = + (tr << [ th << "Version", th << "Reverse dependencies" ]) : + [ tr ! [theclass (if odd n then "odd" else "even")] << + [ td << anchor ! [href $ packageLink pkgid ] << display version + , td << [ row ] + ] + | (version, n) <- zip allVersions [(1::Int)..] + , let + pkgid = PackageIdentifier pkgname version + mkListOfLinks :: Set PackageIdentifier -> [Html] + mkListOfLinks pkgIdSet = + toList pkgIdSet & + map + \revDepPkgId -> + li << + anchor ! [href $ packageLink revDepPkgId] << display revDepPkgId + row :: Html + row = + Map.lookup version versions & + maybe + (toHtml "This version has no reverse dependencies.") + ((ulist <<) . mkListOfLinks) ] - | (version, n) <- zip allVersions [(1::Int)..], let pkgid = PackageIdentifier pkgname version ] - -num, num' :: Int -> String -> String -> String -num n plural singular = show n ++ " " ++ num' n plural singular -num' n plural singular = if n == 1 then singular else plural - --- /packages/reverse -reversePackagesRender :: (PackageName -> String) -> ReverseResource -> Int -> [(PackageName, Int, Int)] -> [Html] -reversePackagesRender packageLink r pkgCount triples = - h2 << "Reverse dependencies" : - [ paragraph << [ "Hackage has " ++ show pkgCount ++ " packages. Here are all the packages that have package that depend on them:"] - , reverseTable ] - where - reverseTable = thediv << table << reverseTableRows - reverseTableRows = - [ tr << [ th << "Package name", th << "Total", th << "Direct" ] ] ++ - [ tr ! [theclass (if odd n then "odd" else "even")] << - [ td << anchor ! [href $ packageLink pkgname ] << display pkgname - , td << [ toHtml $ show flat ++ " (", anchor ! [href $ reverseStatsUri r "" pkgname ] << "view", toHtml ")" ] - , td << [ toHtml $ show total ++ " (", anchor ! [href $ reverseNameUri r "" pkgname ] << "view", toHtml ")" ] - ] -- and, indirect is flat-total, if those are ever explicitly served - | ((pkgname, total, flat), n) <- zip triples [(1::Int)..], flat /= 0 ] - -reversePackageSummary :: PackageId -> ReverseResource -> (Int, Int) -> (String, Html) -reversePackageSummary pkgid r (direct, version) = (,) "Reverse dependencies" $ - if direct == 0 - then toHtml "None" - else toHtml [ anchor ! [href $ reverseUri r "" pkgid ] << show version - , toHtml " for ", toHtml . display $ packageVersion pkgid - , toHtml " and " - , anchor ! [href $ reverseNameUri r "" (packageName pkgid) ] << show direct - , toHtml " total"] + -- /packages/reverse + reversePackagesRender :: (PackageName -> String) -> Int -> [(PackageName, ReverseCount)] -> [Html] + reversePackagesRender packageLink pkgCount namesWithCounts = + h2 << "Reverse dependencies" : + [ paragraph << [ "Hackage has " ++ show pkgCount ++ " packages. Here are all the packages that have package that depend on them:"] + , reverseTable ] + where + reverseTable = thediv << table << reverseTableRows + reverseTableRows = + (tr ! [theclass "fancy"] << [ th << "Package name", th << "Total", th << "Direct" ]) : + [ tr ! [theclass (if odd n then "odd" else "even")] << + [ td << anchor ! [href $ packageLink pkgname ] << display pkgname + , td << [ toHtml $ show totalCount ++ " (", anchor ! [href $ reverseVerboseUri reverseResource "" pkgname ] << "view", toHtml ")" ] + , td << [ toHtml $ show directCount ++ " (", anchor ! [href $ reverseNameUri reverseResource "" pkgname ] << "view", toHtml ")" ] + ] -- and, indirect is total-direct, if those are ever explicitly served + | ((pkgname, ReverseCount{directCount, totalCount}), n) <- zip namesWithCounts [(1::Int)..], totalCount /= 0 ] diff --git a/tests/RevDepCommon.hs b/tests/RevDepCommon.hs new file mode 100644 index 000000000..a7f79ff9c --- /dev/null +++ b/tests/RevDepCommon.hs @@ -0,0 +1,61 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +module RevDepCommon where + +import qualified Data.ByteString.Lazy as BSL +import qualified Data.ByteString.Char8 as Char8 +import qualified Data.Vector as Vector +import Data.Time (UTCTime(..), fromGregorian) +import GHC.Generics (Generic) + +import Distribution.Server.Users.Types (UserId(..)) +import Distribution.Package (PackageIdentifier(..), PackageName, mkPackageName, unPackageName) +import Distribution.Server.Packages.Types (PkgInfo(..)) +import Distribution.Server.Packages.Types (CabalFileText(..)) +import Distribution.Version (mkVersion, versionNumbers) + +data Package b = + Package + { pName :: b + , pVersion :: Int + , pDeps :: [ b ] + } + deriving (Ord, Show, Eq) + +packToPkgInfo :: Show b => Package b -> PkgInfo +packToPkgInfo Package {pName, pVersion, pDeps} = + mkPackage (mkPackageName $ show pName) [pVersion] (depsToBS pDeps) + +mkPackage :: PackageName -> [Int] -> [BSL.ByteString] -> PkgInfo +mkPackage name intVersion depends = + let + version = mkVersion intVersion + -- e.g. "2.3" for [2,3] + dotVersion :: BSL.ByteString + dotVersion = BSL.fromStrict . Char8.intercalate "." . map (Char8.pack . show) $ versionNumbers version + cabalFilePrefix :: BSL.ByteString + cabalFilePrefix = "\ +\name: " <> BSL.fromStrict (Char8.pack $ unPackageName name) <> "\n\ +\version: " <> dotVersion <> "\n" + cabalFile :: CabalFileText + cabalFile = CabalFileText $ cabalFilePrefix <> if depends /= [] then "library\n build-depends: " <> BSL.intercalate "," depends else "" + in + PkgInfo + (PackageIdentifier name version) + (Vector.fromList [(cabalFile, (UTCTime (fromGregorian 2020 1 1) 0, UserId 1))]) + mempty + +depsToBS :: Show b => [ b ] -> [BSL.ByteString] +depsToBS = + map (BSL.fromStrict . Char8.pack . show) + + +newtype TestPackage = TestPackage Word + deriving stock Generic + deriving newtype (Bounded, Enum, Eq, Ord) + +instance Show TestPackage where + show (TestPackage word) = "package" <> show word diff --git a/tests/ReverseDependenciesTest.hs b/tests/ReverseDependenciesTest.hs new file mode 100644 index 000000000..6c2bbf94d --- /dev/null +++ b/tests/ReverseDependenciesTest.hs @@ -0,0 +1,225 @@ +{-# LANGUAGE OverloadedStrings, NamedFieldPuns, TypeApplications, ScopedTypeVariables #-} +module Main where + +import Control.Monad (foldM) +import Control.Monad.Catch (MonadCatch, SomeException, catch) +import Control.Monad.IO.Class (MonadIO, liftIO) +import qualified Data.Array as Arr +import qualified Data.Bimap as Bimap +import Data.Foldable (for_) +import Data.List (partition) +import qualified Data.Map as Map +import qualified Data.Set as Set + +import Distribution.Package (mkPackageName, packageId, packageName) +import Distribution.Server.Features.PreferredVersions.State (PreferredVersions(..), VersionStatus(NormalVersion), PreferredInfo(..)) +import Distribution.Server.Features.ReverseDependencies (ReverseFeature(..), ReverseCount(..), reverseFeature) +import Distribution.Server.Features.ReverseDependencies.State (ReverseIndex(..), addPackage, constructReverseIndex, emptyReverseIndex, getDependenciesFlat, getDependencies, getDependenciesFlatRaw, getDependenciesRaw) +import Distribution.Server.Framework.Hook (newHook) +import Distribution.Server.Framework.MemState (newMemStateWHNF) +import Distribution.Server.Packages.PackageIndex as PackageIndex +import Distribution.Server.Packages.Types (PkgInfo(..)) +import Distribution.Version (mkVersion, version0) + +import Test.Tasty (TestTree, defaultMain, testGroup) +import Test.Tasty.HUnit + +import qualified Hedgehog.Range as Range +import qualified Hedgehog.Gen as Gen +import Hedgehog ((===), Group(Group), MonadGen, MonadTest, Property, PropertyT, checkSequential, failure, footnoteShow, forAll, property) + +import RevDepCommon (Package(..), TestPackage(..), mkPackage, packToPkgInfo) + +mtlBeelineLens :: [PkgInfo] +mtlBeelineLens = + [ mkPackage "base" [4,15] [] + , mkPackage "mtl" [2,3] ["base"] + -- Note that this example is a bit unrealistic + -- since these two do not depend on base... + , mkPackage "beeline" [0] ["mtl"] + , mkPackage "lens" [0] ["mtl"] + ] + +mkRevFeat :: [PkgInfo] -> IO ReverseFeature +mkRevFeat pkgs = do + let + idx = PackageIndex.fromList pkgs + preferredVersions = + PreferredVersions + { preferredMap = mempty + , deprecatedMap = mempty + , migratedEphemeralPrefs = False + } + updateReverse <- newHook + constructed <- constructReverseIndex idx + memState <- newMemStateWHNF constructed + pure $ + reverseFeature + (pure idx) + (pure preferredVersions) + memState + updateReverse + +allTests :: TestTree +allTests = testGroup "ReverseDependenciesTest" + [ testCase "with set [beeline->mtl] and querying for mtl, we get beeline" $ do + let pkgs = + [ mkPackage "base" [4,15] [] + , mkPackage "mtl" [2,3] ["base"] + , mkPackage "beeline" [0] ["mtl"] + ] + ReverseFeature{revPackageName} <- mkRevFeat pkgs + res <- revPackageName "mtl" + let ref = Map.fromList [("beeline", (version0, Just NormalVersion))] + assertEqual "reverse dependencies must be [beeline]" ref res + , testCase "revPackageName selects only the version with an actual dependency, even if it is not the newest" $ do + let pkgs = + [ mkPackage "base" [4,15] [] + , mkPackage "mtl" [2,3] ["base"] + , mkPackage "mtl-tf" [9000] ["base"] + , mkPackage "beeline" [0] ["mtl"] + , mkPackage "beeline" [1] ["mtl-tf"] + ] + ReverseFeature{revPackageName} <- mkRevFeat pkgs + res <- revPackageName "mtl" + let ref = Map.fromList [("beeline", (mkVersion [0], Nothing))] + assertEqual "reverse dependencies must be [beeline v0]" ref res + , testCase "revPackageId does select old version when queried with old reverse dependency" $ do + let mtl = mkPackage "mtl" [2,3] ["base"] + pkgs = + [ mkPackage "base" [4,15] [] + , mtl + , mkPackage "mtl-tf" [9000] ["base"] + , mkPackage "beeline" [0] ["mtl"] + , mkPackage "beeline" [1] ["mtl-tf"] + ] + ReverseFeature{revPackageId} <- mkRevFeat pkgs + res <- revPackageId (packageId mtl) + -- Nothing because it is not the 'best version' + let ref = Map.fromList [("beeline", (mkVersion [0], Nothing))] + assertEqual "reverse dependencies must be [beeline v0]" ref res + , testCase "revPackageName can find multiple packages" $ do + let pkgs = + [ mkPackage "base" [4,15] [] + , mkPackage "mtl" [2,3] ["base"] + , mkPackage "beeline" [0] ["mtl"] + , mkPackage "mario" [0] ["mtl"] + ] + ReverseFeature{revPackageName} <- mkRevFeat pkgs + res <- revPackageName "mtl" + let ref = Map.fromList [ ("beeline", (mkVersion [0], Just NormalVersion)) + , ("mario", (mkVersion [0], Just NormalVersion)) + ] + assertEqual "reverse dependencies must be [beeline v0, mario v0]" ref res + , testCase "with set [beeline->mtl->base, lens->mtl->base], revPackageFlat 'base' finds [beeline, lens, mtl]" $ do + ReverseFeature{revPackageFlat} <- mkRevFeat mtlBeelineLens + res <- revPackageFlat "base" + let ref = [ ("beeline", 0), ("lens", 0), ("mtl", 2) ] + assertEqual "reverse dependencies must be [beeline v0, mario v0]" ref res + , testCase "with set [beeline->mtl->base, lens->mtl->base], revPackageStats 'base' returns 1,3" $ do + ReverseFeature{revPackageStats} <- mkRevFeat mtlBeelineLens + res <- revPackageStats "base" + let ref = ReverseCount { directCount = 1, totalCount = 3 } + assertEqual "must be directCount=1, totalCount=3" ref res + , testCase "with set [beeline->mtl->base, lens->mtl->base], queryReverseDeps 'base' returns [mtl],[beeline,lens]" $ do + ReverseFeature{queryReverseDeps} <- mkRevFeat mtlBeelineLens + res <- queryReverseDeps "base" + let ref = (["mtl"], ["beeline", "lens"]) + assertEqual "must be direct=[mtl], indirect=[beeline,lens]" ref res + , testCase "with set [beeline->mtl->base, lens->mtl->base], revCountForAllPackages returns [(base,1,3),(mtl,2,2),(beeline,0,0),(lens,0,0)]" $ do + ReverseFeature{revCountForAllPackages} <- mkRevFeat mtlBeelineLens + res <- revCountForAllPackages + let ref = [("beeline",ReverseCount 0 0),("lens",ReverseCount 0 0),("base",ReverseCount 1 3),("mtl",ReverseCount 2 2)] + assertEqual "must match reference" ref res + , testCase "revDisplayInfo" $ do + ReverseFeature{revDisplayInfo} <- mkRevFeat mtlBeelineLens + res <- revDisplayInfo + assertEqual "beeline preferred is old" (PreferredInfo [] [] Nothing, [mkVersion [0]]) (res "beeline") + , testCase "hedgehogTests" $ do + res <- hedgehogTests + assertEqual "hedgehog test pass" True res + ] + +genPacks :: PropertyT IO [Package TestPackage] +genPacks = do + numPacks <- forAll $ Gen.int (Range.linear 1 10) + allowMultipleVersions <- forAll Gen.bool -- remember that this shrinks to False + packs <- forAll $ packsUntil allowMultipleVersions numPacks mempty + pure packs + +prop_constructRevDeps :: Property +prop_constructRevDeps = property $ do + packs <- genPacks + let idx = PackageIndex.fromList $ map packToPkgInfo packs + ReverseIndex foldedRevDeps foldedMap foldedDeps <- foldM (packageFolder @_ @TestPackage idx) emptyReverseIndex packs + Right (ReverseIndex constructedRevDeps constructedMap constructedDeps) <- pure $ constructReverseIndex idx + for_ (PackageIndex.allPackageNames idx) $ \name -> do + foundFolded :: Int <- Bimap.lookup name foldedMap + foundConstructed :: Int <- Bimap.lookup name constructedMap + -- they are not nessarily equal, since they may have been added in a different order! + -- so this doesn't necessarily hold: + -- foundFolded === foundConstructed + + -- but they should have the same deps + foldedPackNames <- mapM (`Bimap.lookupR` foldedMap) (foldedRevDeps Arr.! foundFolded) + constructedPackNames <- mapM (`Bimap.lookupR` constructedMap) (constructedRevDeps Arr.! foundConstructed) + Set.fromList foldedPackNames === Set.fromList constructedPackNames + + foldedDeps === constructedDeps + +prop_statsEqualsDeps :: Property +prop_statsEqualsDeps = property $ do + packs <- genPacks + let packages = map packToPkgInfo packs + Right revs <- pure $ constructReverseIndex $ PackageIndex.fromList packages + pkginfo <- forAll $ Gen.element packages + let name = packageName pkginfo + directSet <- getDependenciesRaw name revs + totalSet <- getDependenciesFlatRaw name revs + directNames <- getDependencies name revs + totalNames <- getDependenciesFlat name revs + length directSet === length directNames + length totalSet === length totalNames + +packageFolder :: (MonadCatch m, MonadIO m, MonadTest m, Show b) => PackageIndex PkgInfo -> ReverseIndex -> Package b -> m ReverseIndex +packageFolder index revindex pkg@(Package name _version deps) = + catch (liftIO $ addPackage index (mkPackageName $ show name) (map (mkPackageName . show) deps) revindex) + $ \(e :: SomeException) -> do + footnoteShow pkg + footnoteShow index + footnoteShow e + failure + + +genPackage :: forall m b. (MonadGen m, Enum b, Bounded b, Ord b) => b -> [Package b] -> m (Package b) +genPackage newName available = do + pVersion <- Gen.int (Range.linear 0 10) + depPacks :: [Package b] <- Gen.subsequence available + pure $ Package {pName = newName, pVersion, pDeps = map pName depPacks } + +packsUntil :: forall m b. (Ord b, Bounded b, MonadGen m, Enum b) => Bool -> Int -> [Package b] -> m [Package b] +packsUntil allowMultipleVersions limit generated | length generated < limit = do + makeNewPack <- Gen.bool -- if not new pack, just make a new version of an existing + toInsert <- + if makeNewPack || generated == mempty || not allowMultipleVersions + then + genPackage (toEnum $ length generated) generated + else do + Package { pName = prevName } <- Gen.element generated + let (prevNamePacks, nonPrevName) = partition ((== prevName) . pName) generated + depPacks :: [Package b] <- Gen.subsequence nonPrevName + let newVersion = 1 + maximum (map pVersion prevNamePacks) + pure $ Package {pName = prevName, pVersion = newVersion, pDeps = map pName depPacks} + let added = generated ++ [toInsert] + packsUntil allowMultipleVersions limit added +packsUntil _ _ generated = pure generated + +hedgehogTests :: IO Bool +hedgehogTests = + checkSequential $ Group "hedgehogTests" + [ ("prop_constructRevDeps", prop_constructRevDeps) + , ("prop_statsEqualsDeps", prop_statsEqualsDeps) + ] + +main :: IO () +main = defaultMain allTests From 0a2ffe8e5c5e0293a5bd5b0840f6ece80357926d Mon Sep 17 00:00:00 2001 From: Matthew Pickering Date: Sun, 1 Jan 2023 18:36:44 +0000 Subject: [PATCH 39/43] Add "Quick Jump" to candidate package page (#1122) This allows the testing of quick jump for a candidate. --- .../templates/Html/candidate-page.html.st | 8 +++++++ src/Distribution/Server/Features/Html.hs | 22 ++++++++++++++----- .../Server/Pages/PackageFromTemplate.hs | 9 +++----- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/datafiles/templates/Html/candidate-page.html.st b/datafiles/templates/Html/candidate-page.html.st index 940a8efbc..412591585 100644 --- a/datafiles/templates/Html/candidate-page.html.st +++ b/datafiles/templates/Html/candidate-page.html.st @@ -1,6 +1,9 @@ + $if(doc.hasQuickNavV1)$ + + $endif$ $hackageCssTheme()$ $package.name$$if(package.optional.hasSynopsis)$: $package.optional.synopsis$$endif$ @@ -152,5 +155,10 @@ $package.optional.readme$ $endif$ + $if(doc.hasQuickNavV1)$ + <script src="$doc.baseUrl$/quick-jump.min.js" type="text/javascript"></script> + <script type="text/javascript"> quickNav.init("$doc.baseUrl$", function(toggle) {var t = document.getElementById('quickjump-trigger');if (t) {t.onclick = function(e) { e.preventDefault(); toggle(); };}}); </script> + $endif$ + </body> </html> diff --git a/src/Distribution/Server/Features/Html.hs b/src/Distribution/Server/Features/Html.hs index 4b118d394..61ec25403 100644 --- a/src/Distribution/Server/Features/Html.hs +++ b/src/Distribution/Server/Features/Html.hs @@ -293,7 +293,7 @@ htmlFeature env@ServerEnv{..} htmlDocUploads = mkHtmlDocUploads utilities core docsCore templates htmlDownloads = mkHtmlDownloads utilities download htmlReports = mkHtmlReports utilities core upload user reportsCore templates - htmlCandidates = mkHtmlCandidates utilities core versions upload + htmlCandidates = mkHtmlCandidates env utilities core versions upload docsCandidates tarIndexCache candidates user templates htmlPreferred = mkHtmlPreferred utilities core versions @@ -1081,7 +1081,8 @@ data HtmlCandidates = HtmlCandidates { htmlCandidatesResources :: [Resource] } -mkHtmlCandidates :: HtmlUtilities +mkHtmlCandidates :: ServerEnv + -> HtmlUtilities -> CoreFeature -> VersionsFeature -> UploadFeature @@ -1091,7 +1092,7 @@ mkHtmlCandidates :: HtmlUtilities -> UserFeature -> Templates -> HtmlCandidates -mkHtmlCandidates utilities@HtmlUtilities{..} +mkHtmlCandidates ServerEnv{..} utilities@HtmlUtilities{..} CoreFeature{ coreResource = CoreResource{packageInPath} , queryGetPackageIndex } @@ -1250,9 +1251,20 @@ mkHtmlCandidates utilities@HtmlUtilities{..} mdocIndex <- maybe (return Nothing) (liftM Just . liftIO . cachedTarIndex) mdoctarblob - let docURL = packageDocsContentUri docs (packageId cand) mreadme <- makeReadme render + let loadDocMeta + | Just doctarblob <- mdoctarblob + , Just docIndex <- mdocIndex + = loadTarDocMeta + (BlobStorage.filepath serverBlobStore doctarblob) + docIndex + (packageId cand) + | otherwise + = return Nothing + mdocMeta <- loadDocMeta + + let docURL = packageDocsContentUri docs (packageId cand) -- also utilize hasIndexedPackage :: Bool let warningBox = case renderWarnings candRender of @@ -1270,7 +1282,7 @@ mkHtmlCandidates utilities@HtmlUtilities{..} , "maintainers" $= listGroupCompact (map (Users.userIdToName userDb) (Group.toList maintainerlist)) ] ++ PagesNew.packagePageTemplate render - mdocIndex Nothing mreadme + mdocIndex mdocMeta mreadme docURL Nothing [] Nothing utilities True diff --git a/src/Distribution/Server/Pages/PackageFromTemplate.hs b/src/Distribution/Server/Pages/PackageFromTemplate.hs index b922ab871..6cf3e2624 100644 --- a/src/Distribution/Server/Pages/PackageFromTemplate.hs +++ b/src/Distribution/Server/Pages/PackageFromTemplate.hs @@ -97,7 +97,7 @@ packagePageTemplate render , "doc" $= docFieldsTemplate ] ++ -- Miscellaneous things that could still stand to be refactored a bit. - [ "moduleList" $= Old.moduleSection render mdocIndex docURL mPkgId False + [ "moduleList" $= Old.moduleSection render mdocIndex docURL mPkgId hasQuickNav , "downloadSection" $= Old.downloadSection render ] else @@ -173,11 +173,8 @@ packagePageTemplate render ] docFieldsTemplate = - if isCandidate - then templateDict [ templateVal "baseUrl" docURL ] - else templateDict [ templateVal "hasQuickNavV1" hasQuickNavV1 - , templateVal "baseUrl" docURL - ] + templateDict [ templateVal "hasQuickNavV1" hasQuickNavV1 + , templateVal "baseUrl" docURL ] -- Fields that may be empty, along with booleans to see if they're present. -- Access via "$package.optional.varname$" From 2f9a4ee3e44a9341ee94a1c28a1f59da84a399d9 Mon Sep 17 00:00:00 2001 From: Levi Butcher <31522433+LeviButcher@users.noreply.github.com> Date: Sun, 1 Jan 2023 19:35:57 -0500 Subject: [PATCH 40/43] Solves #1029 - Adds paging to recent packages and recent revisions (#1055) paging on recent packages and uploads --- datafiles/static/browse.js | 24 +-- datafiles/static/hackage.css | 52 ++++++ datafiles/templates/Html/browse.html.st | 37 ---- hackage-server.cabal | 1 + src/Distribution/Server/Features.hs | 6 +- src/Distribution/Server/Features/Html.hs | 85 +++++++++- .../Server/Features/RecentPackages.hs | 94 +++------- src/Distribution/Server/Pages/Recent.hs | 160 +++++++++++++----- src/Distribution/Server/Util/Paging.hs | 89 ++++++++++ 9 files changed, 382 insertions(+), 166 deletions(-) create mode 100644 src/Distribution/Server/Util/Paging.hs diff --git a/datafiles/static/browse.js b/datafiles/static/browse.js index 64691d191..868768a44 100644 --- a/datafiles/static/browse.js +++ b/datafiles/static/browse.js @@ -253,14 +253,17 @@ const createPageLink = (num) => { return a; }; -const createPrevNext = (prevNextNum, cond, txt) => { - const el = d.createElement(cond ? "span" : "a"); - el.setAttribute("href", "#"); - el.addEventListener('click', (evt) => { - evt.preventDefault(); - changePage(prevNextNum); - }); - if (cond) el.classList.add("disabled"); +const createPrevNext = (prevNextNum, hasLink, txt) => { + const el = d.createElement("a"); + + if(hasLink) { + el.setAttribute("href", "#"); + el.addEventListener('click', (evt) => { + evt.preventDefault(); + changePage(prevNextNum); + }); + } + el.appendChild(d.createTextNode(txt)); return el; }; @@ -276,7 +279,7 @@ const createPaginator = () => { const pag = d.createElement("div"); pag.classList.add("paginator"); - pag.appendChild(createPrevNext(state.page - 1, state.page === 0, "Previous")); + pag.appendChild(createPrevNext(state.page - 1, state.page !== 0, "Previous")); // note that page is zero-indexed if (maxPage <= 4) { // No ellipsis @@ -308,7 +311,8 @@ const createPaginator = () => { pag.appendChild(createPageLink(maxPage)); } const isNowOnLastPage = state.page === maxPage; - pag.appendChild(createPrevNext(state.page + 1, isNowOnLastPage, "Next")); + + pag.appendChild(createPrevNext(state.page + 1, !isNowOnLastPage, "Next")); return pag; }; diff --git a/datafiles/static/hackage.css b/datafiles/static/hackage.css index 112d57596..578c1602a 100644 --- a/datafiles/static/hackage.css +++ b/datafiles/static/hackage.css @@ -1044,3 +1044,55 @@ a.deprecated[href]:visited { .versions a.normal[href]:visited { color: #61B01E; } + +/* Paginator */ +#paginatorContainer { + display: flex; + align-items: center; + flex-wrap: wrap; + justify-content: space-between; +} + +#paginatorContainer > div { + padding: 1em 0; +} + +.paginator { + display: flex; + flex-wrap: wrap; +} + +/* Styles Next/Prev when they have no href */ +.paginator a { + color: #666; + cursor: default; + background: none; + border: none; + padding: 0.5em 1em; + text-decoration: none; +} + +.paginator span { + color: #333; + padding: 0.5em 1em; +} + +.paginator a:link, .paginator a:visited { + color: #333; + border: 1px solid transparent; + border-radius: 2px; +} + +.paginator a:link:hover, .paginator a:visited:hover { + color: white; + border: 1px solid #111; + background: linear-gradient(to bottom, #585858 0%, #111 100%); + text-decoration: none; +} + +.paginator .current, +.paginator .current:hover { + color: #333; + border: 1px solid #979797; + background: linear-gradient(to bottom, #fff 0%, #dcdcdc 100%); +} diff --git a/datafiles/templates/Html/browse.html.st b/datafiles/templates/Html/browse.html.st index 19e585351..8f79ce634 100644 --- a/datafiles/templates/Html/browse.html.st +++ b/datafiles/templates/Html/browse.html.st @@ -51,38 +51,6 @@ #browseTable th.descending { background-image: url(/static/images/sort_desc.png); } - .paginator { - margin-left: auto; - } - .paginator a { - box-sizing: border-box; - display: inline-block; - min-width: 1.5em; - padding: 0.5em 1em; - margin-left: 2px; - text-align: center; - text-decoration: none !important; - color: #333 !important; - border: 1px solid transparent; - border-radius: 2px; - } - .paginator .current, .paginator .current:hover { - color: #333 !important; - border: 1px solid #979797; - background: linear-gradient(to bottom, #fff 0%, #dcdcdc 100%); - } - .paginator a:hover { - color: white !important; - border: 1px solid #111; - background: linear-gradient(to bottom, #585858 0%, #111 100%); - } - .paginator span { - padding: 0 1em; - cursor: default; - } - .paginator .disabled { - color: #666; - } .filterSuggestion { display: flex; align-items: center; @@ -102,11 +70,6 @@ .filterSuggestion > div > input { margin: 0; } - #paginatorContainer { - display: flex; - align-items: center; - flex-wrap: wrap; - } #fatalError { display: none; color: red; diff --git a/hackage-server.cabal b/hackage-server.cabal index 73aa484b8..8c0d42cda 100644 --- a/hackage-server.cabal +++ b/hackage-server.cabal @@ -266,6 +266,7 @@ library lib-server Distribution.Server.Util.Merge Distribution.Server.Util.ParseSpecVer Distribution.Server.Util.Markdown + Distribution.Server.Util.Paging Distribution.Server.Features Distribution.Server.Features.Browse diff --git a/src/Distribution/Server/Features.hs b/src/Distribution/Server/Features.hs index 5b1995b04..9d9d70aa4 100644 --- a/src/Distribution/Server/Features.hs +++ b/src/Distribution/Server/Features.hs @@ -193,7 +193,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do tarIndexCacheFeature usersFeature - packagesFeature <- mkRecentPackagesFeature + recentPackagesFeature <- mkRecentPackagesFeature usersFeature coreFeature @@ -313,6 +313,8 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do tarIndexCacheFeature reportsCoreFeature userDetailsFeature + recentPackagesFeature + editCabalFeature <- mkEditCabalFilesFeature usersFeature @@ -371,7 +373,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do #ifndef MINIMAL , getFeatureInterface tarIndexCacheFeature , getFeatureInterface packageContentsFeature - , getFeatureInterface packagesFeature + , getFeatureInterface recentPackagesFeature , getFeatureInterface userDetailsFeature , getFeatureInterface userSignupFeature , getFeatureInterface legacyPasswdsFeature diff --git a/src/Distribution/Server/Features/Html.hs b/src/Distribution/Server/Features/Html.hs index 61ec25403..21ab43c25 100644 --- a/src/Distribution/Server/Features/Html.hs +++ b/src/Distribution/Server/Features/Html.hs @@ -76,6 +76,11 @@ import qualified Text.XHtml.Strict as XHtml import Text.XHtml.Table (simpleTable) import Distribution.PackageDescription (hasLibs) import Distribution.PackageDescription.Configuration (flattenPackageDescription) +import qualified Distribution.Server.Pages.Recent as Pages +import qualified Distribution.Server.Util.Paging as Paging +import Distribution.Server.Features.RecentPackages (RecentPackagesFeature (RecentPackagesFeature, getRecentRevisions, getRecentPackages)) +import Data.Time (getCurrentTime) +import Text.Read (readMaybe) import Distribution.Server.Pages.Group (listGroupCompact) @@ -115,6 +120,7 @@ initHtmlFeature :: ServerEnv -> TarIndexCacheFeature -> ReportsFeature -> UserDetailsFeature + -> RecentPackagesFeature -> IO HtmlFeature) initHtmlFeature env@ServerEnv{serverTemplatesDir, serverTemplatesMode, @@ -153,7 +159,8 @@ initHtmlFeature env@ServerEnv{serverTemplatesDir, serverTemplatesMode, docsCore docsCandidates tarIndexCache reportsCore - usersdetails -> do + usersdetails + recentPackagesFeature -> do -- do rec, tie the knot rec let (feature, packageIndex, packagesPage) = htmlFeature env user core @@ -172,6 +179,7 @@ initHtmlFeature env@ServerEnv{serverTemplatesDir, serverTemplatesMode, (reverseHtmlUtil reversef) mainCache namesCache templates + recentPackagesFeature -- Index page caches mainCache <- newAsyncCacheNF packageIndex @@ -224,6 +232,7 @@ htmlFeature :: ServerEnv -> AsyncCache Response -> AsyncCache Response -> Templates + -> RecentPackagesFeature -> (HtmlFeature, IO Response, IO Response) htmlFeature env@ServerEnv{..} @@ -245,6 +254,7 @@ htmlFeature env@ServerEnv{..} reverseH@ReverseHtmlUtil{..} cachePackagesPage cacheNamesPage templates + recentPackagesFeature = (HtmlFeature{..}, packageIndex, packagesPage) where htmlFeatureInterface = (emptyHackageFeature "html") { @@ -288,6 +298,7 @@ htmlFeature env@ServerEnv{..} templates names candidates + recentPackagesFeature htmlUsers = mkHtmlUsers user usersdetails htmlUploads = mkHtmlUploads utilities upload htmlDocUploads = mkHtmlDocUploads utilities core docsCore templates @@ -419,6 +430,7 @@ mkHtmlCore :: ServerEnv -> Templates -> SearchFeature -> PackageCandidatesFeature + -> RecentPackagesFeature -> HtmlCore mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} utilities@HtmlUtilities{..} @@ -448,10 +460,11 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} templates SearchFeature{..} PackageCandidatesFeature{..} + RecentPackagesFeature{getRecentPackages, getRecentRevisions} = HtmlCore{..} where candidatesCore = candidatesCoreResource - cores@CoreResource{packageInPath, lookupPackageName, lookupPackageId} = coreResource + cores@CoreResource {packageInPath, lookupPackageName, lookupPackageId} = coreResource versions = versionsResource docs = documentationResource @@ -505,8 +518,76 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} , (resourceAt "/package/:package/revisions/.:format") { resourceGet = [("html", serveCabalRevisionsPage)] } + , (resourceAt "/packages/recent.:format") { + resourceGet = [("html", serveRecentPage),("rss", serveRecentRSS)] + } + , (resourceAt "/packages/recent/revisions.:format") { + resourceGet = [("html", serveRevisionPage), ("rss", serveRevisionRSS)] + } ] + readParamWithDefaultAndValid :: (Read a, HasRqData m, Monad m, Functor m, Alternative m) => + a -> (a -> Bool) -> String -> m a + readParamWithDefaultAndValid n f queryParam = do + m <- optional (look queryParam) + let parsed = m >>= readMaybe >>= (\x -> if f x then Just x else Nothing) + + return $ fromMaybe n parsed + + lookupPageSize :: (HasRqData m, Monad m, Functor m, Alternative m) => Int -> m Int + lookupPageSize def = readParamWithDefaultAndValid def validPageSize "pageSize" + where validPageSize x = x > 1 && x <= 200 + + lookupPage :: (HasRqData m, Monad m, Functor m, Alternative m) => Int -> m Int + lookupPage def = readParamWithDefaultAndValid def validPage "page" + where validPage = (>= 1) + + serveRecentPage :: DynamicPath -> ServerPartE Response + serveRecentPage _ = do + recentPackages <- getRecentPackages + users <- queryGetUserDb + page <- lookupPage 1 + pageSize <- lookupPageSize 20 + + let conf = Paging.createConf page pageSize recentPackages + + return . toResponse $ Pages.recentPage conf users recentPackages + + serveRecentRSS :: DynamicPath -> ServerPartE Response + serveRecentRSS _ = do + recentPackages <- getRecentPackages + users <- queryGetUserDb + page <- lookupPage 1 + pageSize <- lookupPageSize 20 + now <- liftIO getCurrentTime + + let conf = Paging.createConf page pageSize recentPackages + + return . toResponse $ Pages.recentFeed conf users serverBaseURI now recentPackages + + serveRevisionPage :: DynamicPath -> ServerPartE Response + serveRevisionPage _ = do + revisions <- getRecentRevisions + users <- queryGetUserDb + page <- lookupPage 1 + pageSize <- lookupPageSize 40 + + let conf = Paging.createConf page pageSize revisions + + return . toResponse $ Pages.revisionsPage conf users revisions + + serveRevisionRSS :: DynamicPath -> ServerPartE Response + serveRevisionRSS _ = do + revisions <- getRecentRevisions + users <- queryGetUserDb + page <- lookupPage 1 + pageSize <- lookupPageSize 40 + now <- liftIO getCurrentTime + + let conf = Paging.createConf page pageSize revisions + + return . toResponse $ Pages.recentRevisionsFeed conf users serverBaseURI now revisions + serveBrowsePage :: DynamicPath -> ServerPartE Response serveBrowsePage _dpath = do template <- getTemplate templates "browse.html" diff --git a/src/Distribution/Server/Features/RecentPackages.hs b/src/Distribution/Server/Features/RecentPackages.hs index d8e0ec430..51fe5366a 100644 --- a/src/Distribution/Server/Features/RecentPackages.hs +++ b/src/Distribution/Server/Features/RecentPackages.hs @@ -6,25 +6,17 @@ module Distribution.Server.Features.RecentPackages ( ) where import Distribution.Server.Framework - import Distribution.Server.Features.Core import Distribution.Server.Features.Users - import Distribution.Server.Packages.Types - import qualified Distribution.Server.Packages.PackageIndex as PackageIndex -import qualified Distribution.Server.Framework.ResponseContentTypes as Resource - -import Data.Time.Clock (getCurrentTime) -import Data.List (sortBy) -import Data.Ord (comparing) - --- the goal is to have the HTML modules import /this/ one, not the other way around -import qualified Distribution.Server.Pages.Recent as Pages +import Data.List (sortOn) +import Data.Ord (Down(Down)) data RecentPackagesFeature = RecentPackagesFeature { recentPackagesFeatureInterface :: HackageFeature, - recentPackagesResource :: RecentPackagesResource + getRecentPackages :: forall m. MonadIO m => m [PkgInfo], + getRecentRevisions :: forall m. MonadIO m => m [PkgInfo] -- other informational hooks: perhaps a simplified CondTree so a browser script can dynamically change the package page based on flags } @@ -51,9 +43,9 @@ initRecentPackagesFeature env@ServerEnv{serverCacheDelay, serverVerbosity = verb recentPackagesFeature env user core cacheRecent - cacheRecent <- newAsyncCacheNF updateRecentCache + cacheRecent <- newAsyncCacheWHNF updateRecentCache defaultAsyncCachePolicy { - asyncCacheName = "recent uploads and revisions (html,rss,html,rss)", + asyncCacheName = "recent uploads and revisions", asyncCacheUpdateDelay = serverCacheDelay, asyncCacheSyncInit = False, asyncCacheLogVerbosity = verbosity @@ -68,76 +60,40 @@ initRecentPackagesFeature env@ServerEnv{serverCacheDelay, serverVerbosity = verb recentPackagesFeature :: ServerEnv -> UserFeature -> CoreFeature - -> AsyncCache (Response, Response, Response, Response) - -> (RecentPackagesFeature, IO (Response, Response, Response, Response)) + -> AsyncCache ([PkgInfo], [PkgInfo]) + -> (RecentPackagesFeature, IO ([PkgInfo], [PkgInfo])) -recentPackagesFeature env +recentPackagesFeature _ UserFeature{..} CoreFeature{..} cacheRecent = (RecentPackagesFeature{..}, updateRecentCache) where recentPackagesFeatureInterface = (emptyHackageFeature "recentPackages") { - featureResources = map ($ recentPackagesResource) [recentPackages, recentRevisions] - , featureState = [] - , featureCaches = [ + featureState = [], + featureCaches = [ CacheComponent { - cacheDesc = "recents packages and revisions page (html, rss, html, rss)", + cacheDesc = "recent packages and revisions", getCacheMemSize = memSize <$> readAsyncCache cacheRecent } ] - , featurePostInit = syncAsyncCache cacheRecent } - recentPackagesResource = RecentPackagesResource { - recentPackages = (extendResourcePath "/recent.:format" (corePackagesPage coreResource)) { - resourceGet = [ - ("html", const $ liftM (\(x,_,_,_) -> x) $ readAsyncCache cacheRecent) - , ("rss", const $ addAllowOriginHeader >> (liftM (\(_,x,_,_) -> x) $ readAsyncCache cacheRecent)) - ] - }, - recentRevisions = (extendResourcePath "/recent/revisions.:format" (corePackagesPage coreResource)) { - resourceGet = [ - ("html", const $ liftM (\(_,_,x,_) -> x) $ readAsyncCache cacheRecent) - , ("rss", const $ addAllowOriginHeader >> (liftM (\(_,_,_,x) -> x) $ readAsyncCache cacheRecent)) - ] - } - } + getRecentPackages :: MonadIO m => m [PkgInfo] + getRecentPackages = fst <$> readAsyncCache cacheRecent + + getRecentRevisions :: MonadIO m => m [PkgInfo] + getRecentRevisions = snd <$> readAsyncCache cacheRecent + + updateRecentCache :: IO ([PkgInfo], [PkgInfo]) updateRecentCache = do - -- TODO: move the html version to the HTML feature pkgIndex <- queryGetPackageIndex - users <- queryGetUserDb - now <- getCurrentTime - let recentChanges = sortBy (flip $ comparing pkgOriginalUploadTime) - (PackageIndex.allPackages pkgIndex) - xmlRepresentation = toResponse $ Resource.XHtml $ Pages.recentPage users recentChanges - rssRepresentation = toResponse $ Pages.recentFeed users (serverBaseURI env) now recentChanges - - recentRevisions = sortBy (flip $ comparing revisionTime) . - filter isRevised $ (PackageIndex.allPackages pkgIndex) - revisionTime pkgInfo = pkgLatestUploadTime pkgInfo - isRevised pkgInfo = pkgNumRevisions pkgInfo > 1 - xmlRevisions = toResponse $ Resource.XHtml $ Pages.revisionsPage users recentRevisions - rssRevisions = toResponse $ Pages.recentRevisionsFeed users (serverBaseURI env) now recentRevisions - - return (xmlRepresentation, rssRepresentation, xmlRevisions, rssRevisions) - - -addAllowOriginHeader :: (FilterMonad Response m) => m () -addAllowOriginHeader = addHeaderM "Access-Control-Allow-Origin" "*" -{- -data SimpleCondTree = SimpleCondNode [Dependency] [(Condition ConfVar, SimpleCondTree, SimpleCondTree)] - | SimpleCondLeaf - deriving (Show, Eq) + let packages = PackageIndex.allPackages pkgIndex + isRevised pkgInfo = pkgNumRevisions pkgInfo > 1 + revisionTime pkgInfo = pkgLatestUploadTime pkgInfo + recentChanges = sortOn (Down . pkgOriginalUploadTime) packages + recentRevisions = sortOn (Down . revisionTime) . filter isRevised $ packages -doMakeCondTree :: GenericPackageDescription -> [(String, SimpleCondTree)] -doMakeCondTree desc = map (\lib -> ("library", makeCondTree lib)) (maybeToList $ condLibrary desc) - ++ map (\(exec, tree) -> (exec, makeCondTree tree)) (condExecutables desc) - where - makeCondTree (CondNode _ deps comps) = case deps of - [] -> SimpleCondLeaf - _ -> SimpleCondNode deps $ map makeCondComponents comps - makeCondComponents (cond, tree, mtree) = (cond, makeCondTree tree, maybe SimpleCondLeaf makeCondTree mtree) --} + return (recentChanges, recentRevisions) \ No newline at end of file diff --git a/src/Distribution/Server/Pages/Recent.hs b/src/Distribution/Server/Pages/Recent.hs index 708f63a36..19e7e75b0 100644 --- a/src/Distribution/Server/Pages/Recent.hs +++ b/src/Distribution/Server/Pages/Recent.hs @@ -1,10 +1,11 @@ -- Takes a reversed log file on the standard input and outputs web page. +{-# LANGUAGE NamedFieldPuns #-} module Distribution.Server.Pages.Recent ( recentPage, recentFeed, revisionsPage, - recentRevisionsFeed + recentRevisionsFeed, ) where import Distribution.Server.Packages.Types @@ -23,43 +24,109 @@ import Distribution.Text import Distribution.Utils.ShortText (fromShortText) import qualified Text.XHtml.Strict as XHtml -import Text.XHtml - ( Html, URL, (<<), (!) ) +import Text.XHtml ( Html, URL, (<<), (!) ) import qualified Text.RSS as RSS -import Text.RSS - ( RSS(RSS) ) -import Network.URI - ( URI(..), uriToString ) -import Data.Time.Clock - ( UTCTime, addUTCTime ) -import Data.Time.Format - ( defaultTimeLocale, formatTime ) -import Data.Maybe - ( listToMaybe) +import Text.RSS ( RSS(RSS) ) +import Network.URI ( URI(..), uriToString ) +import Data.Time.Clock ( UTCTime ) +import Data.Time.Format ( defaultTimeLocale, formatTime ) +import Data.Maybe ( listToMaybe, fromMaybe) +import Distribution.Server.Util.Paging (PaginatedConfiguration(..), hasNext, + hasPrev, nextURL, pageIndexRange, paginate, prevURL, toURL, allPagedURLs, pagingInfo) -- | Takes a list of package info, in reverse order by timestamp. --- -recentPage :: Users -> [PkgInfo] -> Html -recentPage users pkgs = - let log_rows = map (makeRow users) (take 25 pkgs) - docBody = [XHtml.h2 << "Recent additions", - XHtml.table ! [XHtml.align "center"] << log_rows, - XHtml.anchor ! [XHtml.href recentRevisionsURL] << XHtml.toHtml "Recent revisions"] - rss_link = XHtml.thelink ! [XHtml.rel "alternate", - XHtml.thetype "application/rss+xml", - XHtml.title "Hackage RSS Feed", - XHtml.href rssFeedURL] << XHtml.noHtml + +recentPage :: PaginatedConfiguration -> Users -> [PkgInfo] -> Html +recentPage conf users pkgs = + let log_rows = makeRow users <$> paginate conf pkgs + docBody = + [ XHtml.h2 << "Recent additions", + pageSizeForm recentURL, + XHtml.table ! [XHtml.align "center"] << log_rows, + paginator conf recentURL, + XHtml.anchor ! [XHtml.href recentRevisionsURL] << XHtml.toHtml "Recent revisions" + ] + rss_link = + XHtml.thelink + ! [ XHtml.rel "alternate", + XHtml.thetype "application/rss+xml", + XHtml.title "Hackage RSS Feed", + XHtml.href $ toURL rssFeedURL conf + ] + << XHtml.noHtml in hackagePageWithHead [rss_link] "recent additions" docBody -revisionsPage :: Users -> [PkgInfo] -> Html -revisionsPage users pkgs = - let log_rows = map (makeRevisionRow users) (take 40 pkgs) - docBody = [XHtml.h2 << "Recent cabal metadata revisions", - XHtml.table ! [XHtml.align "center"] << log_rows] - rss_link = XHtml.thelink ! [XHtml.rel "alternate", - XHtml.thetype "application/rss+xml", - XHtml.title "Hackage Revisions RSS Feed", - XHtml.href revisionsRssFeedURL] << XHtml.noHtml + +pageSizeForm :: URL -> Html +pageSizeForm base = + let pageSizeLabel = XHtml.label ! [XHtml.thefor "pageSize"] << "Page Size: " + pageSizeInput = XHtml.input ! [XHtml.thetype "number", XHtml.name "pageSize", XHtml.strAttr "min" "0"] + submitButton = XHtml.button ! [XHtml.thetype "submit"] << "Submit" + theForm = XHtml.form ! [XHtml.action base, XHtml.method "GET"] + in theForm << (pageSizeLabel <> pageSizeInput <> submitButton) + + +paginator :: PaginatedConfiguration -> URL -> Html +paginator pc@PaginatedConfiguration{currPage} baseUrl = + let + info = XHtml.thediv << pagingInfo pc + + next = XHtml.anchor ! [XHtml.href (fromMaybe "" (nextURL baseUrl pc)) | hasNext pc] << "Next" + prev = XHtml.anchor ! [XHtml.href (fromMaybe "" (prevURL baseUrl pc)) | hasPrev pc] << "Previous" + + + pagedURLS = zip [1..] (allPagedURLs baseUrl pc) + pagedLinks = (\(x,y) -> XHtml.anchor ! [XHtml.href y, + if currPage == x then XHtml.theclass "current" else noAttr ] << show x) <$> pagedURLS + + wrapper = XHtml.thediv ! [XHtml.theclass "paginator"] << + (prev <> reducePagedLinks pc pagedLinks <> next) + + + in XHtml.thediv ! [XHtml.identifier "paginatorContainer"] << mconcat [info, wrapper] + +noAttr :: XHtml.HtmlAttr +noAttr = XHtml.theclass "" + +-- | Generates a list of links of the current possible paging links, recreates the functionality of the paging links on the search page +reducePagedLinks :: PaginatedConfiguration -> [Html] -> Html +reducePagedLinks PaginatedConfiguration{currPage} xs + | length xs <= 5 = mconcat xs -- Do Nothing + | currPage >= (length xs - 3) = mconcat . keepLastPages .fillFirst $ xs -- Beginning ellipses + | currPage < 5 = mconcat . keepFirstPages . fillLast $ xs -- Ending ellipses + | otherwise = mconcat . keepMiddlePages . fillLast . fillFirst $ xs -- Begin and End ellipses + where filler = XHtml.thespan << "..." + fillFirst x = insertAt 1 filler x + fillLast x = insertAt (pred . length $ x) filler x + keepFirstPages x = case splitAt (length x - 2) x of (hts, hts') -> take 5 hts ++ hts' + keepLastPages x = case splitAt 2 x of (hts, hts') -> hts ++ takeLast 5 hts' + keepMiddlePages x = + case splitAt currPage x of (hts, hts') -> take 2 hts ++ [last hts] ++ take 2 hts' + ++ takeLast 2 hts' + +insertAt :: Int -> a -> [a] -> [a] +insertAt n a x = case splitAt n x of (hts, hts') -> hts ++ [a] ++ hts' + +takeLast :: Int -> [a] -> [a] +takeLast n = reverse . take n . reverse + +revisionsPage :: PaginatedConfiguration -> Users -> [PkgInfo] -> Html +revisionsPage conf users pkgs = + let log_rows = map (makeRevisionRow users) (paginate conf pkgs) + docBody = + [ XHtml.h2 << "Recent cabal metadata revisions", + pageSizeForm recentRevisionsURL, + XHtml.table ! [XHtml.align "center"] << log_rows, + paginator conf recentRevisionsURL + ] + rss_link = + XHtml.thelink + ! [ XHtml.rel "alternate", + XHtml.thetype "application/rss+xml", + XHtml.title "Hackage Revisions RSS Feed", + XHtml.href $ toURL revisionsRssFeedURL conf + ] + << XHtml.noHtml in hackagePageWithHead [rss_link] "recent revisions" docBody makeRow :: Users -> PkgInfo -> Html @@ -110,6 +177,9 @@ packageURL pkgid = "/package/" ++ display pkgid rssFeedURL :: URL rssFeedURL = "/recent.rss" +recentURL :: URL +recentURL = "/packages/recent.html" + recentAdditionsURL :: URL recentAdditionsURL = "/recent.html" @@ -120,36 +190,34 @@ recentRevisionsURL :: URL recentRevisionsURL = "/packages/recent/revisions.html" -recentFeed :: Users -> URI -> UTCTime -> [PkgInfo] -> RSS -recentFeed users hostURI now pkgs = RSS +recentFeed :: PaginatedConfiguration -> Users -> URI -> UTCTime -> [PkgInfo] -> RSS +recentFeed conf users hostURI now pkgs = RSS "Recent additions" (hostURI { uriPath = recentAdditionsURL}) desc (channel updated) (map (releaseItem users hostURI) pkgList) where - desc = "The 20 most recent additions to Hackage (or last 48 hours worth, whichever is greater), the Haskell package database." - twoDaysAgo = addUTCTime (negate $ 60 * 60 * 48) now - pkgListTwoDays = takeWhile (\p -> pkgLatestUploadTime p > twoDaysAgo) pkgs - pkgList = if length pkgListTwoDays > 20 then pkgListTwoDays else take 20 pkgs + (start,end) = pageIndexRange conf + desc = "Showing " ++ show start ++ " - " ++ show end ++ " most recent additions to Hackage, the Haskell package database." + pkgList = paginate conf pkgs updated = maybe now (fst . pkgOriginalUploadInfo) (listToMaybe pkgList) -recentRevisionsFeed :: Users -> URI -> UTCTime -> [PkgInfo] -> RSS -recentRevisionsFeed users hostURI now pkgs = RSS +recentRevisionsFeed :: PaginatedConfiguration -> Users -> URI -> UTCTime -> [PkgInfo] -> RSS +recentRevisionsFeed conf users hostURI now pkgs = RSS "Recent revisions" (hostURI { uriPath = recentRevisionsURL}) desc (channel updated) (map (revisionItem users hostURI) pkgList) where - desc = "The 40 most recent revisions to cabal metadata in Hackage (or last 48 hours worth, whichever is greater), the Haskell package database." - twoDaysAgo = addUTCTime (negate $ 60 * 60 * 48) now - pkgListTwoDays = takeWhile (\p -> pkgLatestUploadTime p > twoDaysAgo) pkgs - pkgList = if length pkgListTwoDays > 40 then pkgListTwoDays else take 40 pkgs + (start, end) = pageIndexRange conf + desc = "Showing " ++ show start ++ " - " ++ show end ++ " most recent revisions to cabal metadata in Hackage, the Haskell package database." + pkgList = paginate conf pkgs updated = maybe now (fst . pkgOriginalUploadInfo) (listToMaybe pkgList) channel :: UTCTime -> [RSS.ChannelElem] -channel updated = +channel updated = [ RSS.Language "en" , RSS.ManagingEditor email , RSS.WebMaster email diff --git a/src/Distribution/Server/Util/Paging.hs b/src/Distribution/Server/Util/Paging.hs new file mode 100644 index 000000000..07d83cab8 --- /dev/null +++ b/src/Distribution/Server/Util/Paging.hs @@ -0,0 +1,89 @@ +{-# LANGUAGE NamedFieldPuns #-} + +module Distribution.Server.Util.Paging +( + totalPages, + createConf, + hasNext, + hasPrev, + paginate, + pageIndexStart, + pageIndexRange, + pageIndexEnd, + allPagedURLs, + nextURL, + prevURL, + toURL, + pagingInfo, + PaginatedConfiguration(..), +) +where +import Text.XHtml (URL) +import Data.List (genericTake, genericDrop, genericLength) + +-- This could be better designed, perhaps turning PaginatedConfiguration into a +-- function that returns the paging info and the paged data +data PaginatedConfiguration = PaginatedConfiguration + { currPage :: Int, + pageSize :: Int, + totalAmount :: Int + } + +-- Assumes pageSize isn't 0, not the best design +totalPages :: PaginatedConfiguration -> Int +totalPages PaginatedConfiguration {pageSize, totalAmount} = + case totalAmount `quotRem` pageSize of + (x,r) + | r == 0 -> x + | otherwise -> succ x + +createConf :: Int -> Int -> [a] -> PaginatedConfiguration +createConf page pageSize xs = PaginatedConfiguration page pageSize (genericLength xs) + +paginate :: PaginatedConfiguration -> [a] -> [a] +paginate PaginatedConfiguration {currPage, pageSize} = genericTake pageSize . genericDrop toDrop + where + toDrop = pageSize * pred currPage + +hasNext,hasPrev :: PaginatedConfiguration -> Bool +hasNext pc@PaginatedConfiguration{currPage} = currPage < totalPages pc +hasPrev PaginatedConfiguration {currPage} = currPage > 1 + +-- | Returns the index positions that the current PaginatedConfiguration would show (Starts at 1) +pageIndexRange :: PaginatedConfiguration -> (Int, Int) +pageIndexRange conf@PaginatedConfiguration{currPage, pageSize, totalAmount} = (start, end) + where start = succ $ currPage * pageSize - pageSize + end = if currPage == totalPages conf then totalAmount else currPage * pageSize + +pageIndexStart, pageIndexEnd :: PaginatedConfiguration -> Int +pageIndexStart = fst . pageIndexRange +pageIndexEnd = snd . pageIndexRange + + +allPagedURLs :: URL -> PaginatedConfiguration -> [URL] +allPagedURLs base pc = toURL base . (\x -> pc{currPage=x}) <$> [1..totalPages pc] + + +-- | Converts the PaginatedConfiguration to a URL, Assumes no query params in url +toURL :: URL -> PaginatedConfiguration -> URL +toURL base PaginatedConfiguration{currPage, pageSize} = base ++ "?page=" ++ show currPage ++ "&pageSize=" ++ show pageSize + +nextURL :: URL -> PaginatedConfiguration -> Maybe URL +nextURL base conf@PaginatedConfiguration {currPage} + | page > totalPages conf = Nothing + | otherwise = Just $ toURL base conf{currPage = page} + where page = succ currPage + +prevURL :: URL -> PaginatedConfiguration -> Maybe URL +prevURL base conf@PaginatedConfiguration {currPage} + | page < 1 = Nothing + | otherwise = Just $ toURL base conf{currPage=page} + where page = pred currPage + + +pagingInfo :: PaginatedConfiguration -> String +pagingInfo pc@PaginatedConfiguration{totalAmount} = "Showing " ++ show start ++ " to " + ++ show end ++ " of " ++ show totalAmount ++ endingText + where (start, end) = pageIndexRange pc + endingText = if pageAmount > 0 then " entries" else " entry" + pageAmount = end - start -- Starts Indexing at 1 From 5f5b8144ead7476f6c8af89844d84f9771106221 Mon Sep 17 00:00:00 2001 From: Peter Becich <peterbecich@gmail.com> Date: Mon, 2 Jan 2023 15:52:03 -0800 Subject: [PATCH 41/43] support for `prefers-color-scheme` (#1008) * support for `prefers-color-scheme` * 2x brightness for captions and links in dark color scheme * table dark color scheme * prefers-color-scheme for links, footer, and table-of-contents * paginator css for `prefers-color-scheme` --- datafiles/static/hackage.css | 150 +++++++++++++++++++++--- datafiles/templates/Html/browse.html.st | 2 +- 2 files changed, 133 insertions(+), 19 deletions(-) diff --git a/datafiles/static/hackage.css b/datafiles/static/hackage.css index 578c1602a..7a9e05616 100644 --- a/datafiles/static/hackage.css +++ b/datafiles/static/hackage.css @@ -9,9 +9,19 @@ html { height: 100%; } +@media (prefers-color-scheme: dark) { + body { + background: #333; + color: #fefefe; + } +} +@media (prefers-color-scheme: light) { + body { + background: #fefefe; + color: #333; + } +} body { - background: #fefefe; - color: #333; text-align: left; min-height: 100vh; position: relative; @@ -59,8 +69,16 @@ dd { } a { text-decoration: none; } -a[href]:link { color: #9E358F; } -a[href]:visited {color: #6F5F9C; } + +@media (prefers-color-scheme: dark) { + a[href]:link { color: #EB82DC; } /* 30% brighter */ + a[href]:visited { color: #D5C5FF; } /* 40% brighter */ +} +@media (prefers-color-scheme: light) { + a[href]:link { color: #9E358F; } + a[href]:visited {color: #6F5F9C; } +} + a[href]:hover { text-decoration:underline; } /* @end */ @@ -142,9 +160,19 @@ pre, code, kbd, samp, .src { /* @group Common */ +@media (prefers-color-scheme: dark) { + .caption, h1, h2, h3, h4, h5, h6 { + color: #5E5184; + filter: brightness(2.0); + } +} +@media (prefers-color-scheme: light) { + .caption, h1, h2, h3, h4, h5, h6 { + color: #5E5184; + } +} .caption, h1, h2, h3, h4, h5, h6 { font-weight: bold; - color: #5E5184; margin: 1.33em 0 0.7em 0; line-height: 1.05em; } @@ -194,10 +222,19 @@ ul.links li a, ul.links li form { cursor: pointer; } +@media (prefers-color-scheme: dark) { + pre { + background-color: #474747; /* 20% brighter than background */ + } +} +@media (prefers-color-scheme: light) { + pre { + background-color: #f7f7f7; + } +} pre { padding: 0.5rem 1rem; margin: 1em 0; - background-color: #f7f7f7; overflow: auto; } @@ -402,7 +439,6 @@ table.properties td, table.properties th { } div #properties { - background: #fefefe; width: 40%; margin-bottom: 2em; } @@ -583,11 +619,21 @@ div#style-menu-holder { display: block; } +@media (prefers-color-scheme: dark) { + #footer { + background: #222; + color: #ededed; + } +} +@media (prefers-color-scheme: light) { + #footer { + background: #ededed; + color: #222; + } +} #footer { - background: #ededed; border-top: 1px solid #aaa; padding: 0.5em 0; - color: #222; text-align: center; width: 100%; height: 3em; @@ -625,8 +671,17 @@ ul.links li form button { /* @group Front Matter */ +@media (prefers-color-scheme: dark) { + #table-of-contents { + background: #222; + } +} +@media (prefers-color-scheme: light) { + #table-of-contents { + background: #f7f7f7; + } +} #table-of-contents { - background: #f7f7f7; padding: 1em; margin: 0; margin-top: 1em; @@ -986,6 +1041,17 @@ table.fancy th { background: #f0f0f0; } +@media (prefers-color-scheme: dark) { + table.fancy td, table.properties td, + table.fancy th, table.properties th { + background: #333; + } + table.dataTable.compact.fancy tbody th, + table.dataTable.compact.fancy tbody td { + background: #333; + } +} + table.fancy td, table.properties td, table.fancy th, table.properties th { padding: 0.15em 0.45em; @@ -997,8 +1063,8 @@ table.fancy tr.even td { table.dataTable.compact.fancy tbody th, table.dataTable.compact.fancy tbody td { - padding: 6px 10px; - line-height: normal; + padding: 6px 10px; + line-height: normal; } ul.searchresults li { @@ -1063,8 +1129,20 @@ a.deprecated[href]:visited { } /* Styles Next/Prev when they have no href */ + +@media (prefers-color-scheme: dark) { + .paginator a { + color: #474747; + } +} + +@media (prefers-color-scheme: light) { + .paginator a { + color: #666 !important; + } +} + .paginator a { - color: #666; cursor: default; background: none; border: none; @@ -1077,22 +1155,58 @@ a.deprecated[href]:visited { padding: 0.5em 1em; } + +@media (prefers-color-scheme: dark) { + .paginator a:link:hover, .paginator a:visited:hover { + color: #474747; + } +} + +@media (prefers-color-scheme: light) { + .paginator a:link:hover, .paginator a:visited:hover { + color: #333 !important; + } +} + .paginator a:link, .paginator a:visited { - color: #333; border: 1px solid transparent; border-radius: 2px; } +@media (prefers-color-scheme: dark) { + .paginator a:link:hover, .paginator a:visited:hover { + color: #585858; + background: linear-gradient(to bottom, #fff 0%, #dcdcdc 100%); + } +} + +@media (prefers-color-scheme: light) { + .paginator a:link:hover, .paginator a:visited:hover { + color: white !important; + background: linear-gradient(to bottom, #585858 0%, #111 100%); + } +} + .paginator a:link:hover, .paginator a:visited:hover { - color: white; border: 1px solid #111; - background: linear-gradient(to bottom, #585858 0%, #111 100%); text-decoration: none; } +@media (prefers-color-scheme: dark) { + .paginator .current, .paginator .current:hover { + color: #474747; + background: linear-gradient(to bottom, #585858 0%, #111 100%); + } +} + +@media (prefers-color-scheme: light) { + .paginator .current, .paginator .current:hover { + color: #666 !important; + background: linear-gradient(to bottom, #fff 0%, #dcdcdc 100%); + } +} + .paginator .current, .paginator .current:hover { - color: #333; border: 1px solid #979797; - background: linear-gradient(to bottom, #fff 0%, #dcdcdc 100%); } diff --git a/datafiles/templates/Html/browse.html.st b/datafiles/templates/Html/browse.html.st index 8f79ce634..a8b8e6afa 100644 --- a/datafiles/templates/Html/browse.html.st +++ b/datafiles/templates/Html/browse.html.st @@ -173,7 +173,7 @@ <dd>Only show packages with more than 1000 downloads within the last 30 days. The download count is inexact because Hackage uses a <a href="https://en.wikipedia.org/wiki/Content_delivery_network" target=_blank>content delivery network</a>.</dd> <dt>(lastUpload < 2021-10-29)</dt> <dd>Only show packages for which the last upload was before (i.e. excluding) the given UTC date in <a target=_blank href="https://www.w3.org/TR/NOTE-datetime">the 'complete date' format as specified using ISO 8601</a>.</dd> - <dt>(lastUpload = 2021-10-29)</dt> + <dt>(lastUpload = 2021-10-29)</dt>x <dd>Only show packages for which the last upload was within the 24 hours of the given UTC date.</dd> <dt>(maintainer:SimonMarlow)</dt> <dd>Only show packages for which the maintainers list includes the user name <a target=_blank href="/user/SimonMarlow">SimonMarlow</a>.</dd> From b13bc6e91026c13277fb2c9a6fcd4601aa02cc16 Mon Sep 17 00:00:00 2001 From: Alias Qli <2576814881@qq.com> Date: Wed, 4 Jan 2023 12:33:39 +0800 Subject: [PATCH 42/43] Maintainer notifications Co-authored-by: Gershom <gershomb@gmail.com> --- .github/workflows/nix-shell.yml | 2 +- .../UserNotify/user-notify-form.html.st | 52 ++ datafiles/templates/Users/manage.html.st | 3 + datafiles/templates/accounts.html.st | 3 +- hackage-server.cabal | 1 + src/Distribution/Server/Features.hs | 13 + src/Distribution/Server/Features/AdminLog.hs | 6 +- src/Distribution/Server/Features/Tags.hs | 18 +- .../Server/Features/UserNotify.hs | 631 ++++++++++++++++++ .../Server/Features/UserSignup.hs | 12 +- 10 files changed, 727 insertions(+), 14 deletions(-) create mode 100644 datafiles/templates/UserNotify/user-notify-form.html.st create mode 100644 src/Distribution/Server/Features/UserNotify.hs diff --git a/.github/workflows/nix-shell.yml b/.github/workflows/nix-shell.yml index 0e8376f9b..f6c20790c 100644 --- a/.github/workflows/nix-shell.yml +++ b/.github/workflows/nix-shell.yml @@ -24,4 +24,4 @@ jobs: # https://nix.dev/tutorials/continuous-integration-github-actions#setting-up-github-actions name: hackage-server authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - - run: nix-shell --pure --run ./.github/workflows/test-nix-shell.sh + - run: nix-shell --pure --run ./.github/workflows/test-nix-shell.sh \ No newline at end of file diff --git a/datafiles/templates/UserNotify/user-notify-form.html.st b/datafiles/templates/UserNotify/user-notify-form.html.st new file mode 100644 index 000000000..989af5a92 --- /dev/null +++ b/datafiles/templates/UserNotify/user-notify-form.html.st @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<html> +<head> +$hackageCssTheme()$ +<title>Set user notification preferences | Hackage + + + +$hackagePageHeader()$ + +
+

Change notification preferences

+ +$if(showConfirmationOfSave)$ +

+ Notification preferences saved! The updated preferences are shown below. +

+$endif$ + +
+ + + + + + + + + + +
$notifyEnabled$ + +
$notifyRevisionRange$ + +
$notifyUpload$ + +
$notifyMaintainerGroup$ + +
$notifyDocBuilderReport$ + +
$notifyPendingTags$ +
+ +
+
+ diff --git a/datafiles/templates/Users/manage.html.st b/datafiles/templates/Users/manage.html.st index 0f52f342e..9ebb91ba5 100644 --- a/datafiles/templates/Users/manage.html.st +++ b/datafiles/templates/Users/manage.html.st @@ -15,6 +15,9 @@ $hackagePageHeader(deauthUser="1")$

Change full name or e-mail address

You can change your full name or e-mail address.

+

Change notification preferences

+

You can change your notification preferences.

+

Authentication Tokens

You can register API authentication token to use them to for example have services like continuous integration upload packages on your behalf without providing them your username and/or password. diff --git a/datafiles/templates/accounts.html.st b/datafiles/templates/accounts.html.st index 1dbcc7221..9fc1584b7 100644 --- a/datafiles/templates/accounts.html.st +++ b/datafiles/templates/accounts.html.st @@ -26,7 +26,8 @@ maintainer group.

Account Management

You can modify various settings for your account, including changing - the email address and password, as well as creating authentication + the email address and password, setting email notification + preferences, as well as creating authentication tokens, at the account management page. diff --git a/hackage-server.cabal b/hackage-server.cabal index 0f80ca11f..ceb7a6049 100644 --- a/hackage-server.cabal +++ b/hackage-server.cabal @@ -291,6 +291,7 @@ library lib-server Distribution.Server.Features.Upload.State Distribution.Server.Features.Upload.Backup Distribution.Server.Features.Users + Distribution.Server.Features.UserNotify if flag(minimal) diff --git a/src/Distribution/Server/Features.hs b/src/Distribution/Server/Features.hs index 9d9d70aa4..410a6117f 100644 --- a/src/Distribution/Server/Features.hs +++ b/src/Distribution/Server/Features.hs @@ -49,6 +49,7 @@ import Distribution.Server.Features.AdminLog (initAdminLogFeature) import Distribution.Server.Features.HoogleData (initHoogleDataFeature) import Distribution.Server.Features.Votes (initVotesFeature) import Distribution.Server.Features.Sitemap (initSitemapFeature) +import Distribution.Server.Features.UserNotify (initUserNotifyFeature) import Distribution.Server.Features.PackageFeed (initPackageFeedFeature) #endif import Distribution.Server.Features.ServerIntrospect (serverIntrospectFeature) @@ -154,6 +155,8 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do initAdminLogFeature env mkSitemapFeature <- logStartup "sitemap" $ initSitemapFeature env + mkUserNotifyFeature <- logStartup "user notify" $ + initUserNotifyFeature env mkPackageFeedFeature <- logStartup "package feed" $ initPackageFeedFeature env mkBrowseFeature <- logStartup "browse" $ @@ -341,6 +344,15 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do tagsFeature tarIndexCacheFeature + userNotifyFeature <- mkUserNotifyFeature + usersFeature + coreFeature + uploadFeature + adminLogFeature + userDetailsFeature + reportsCoreFeature + tagsFeature + packageFeedFeature <- mkPackageFeedFeature coreFeature usersFeature @@ -399,6 +411,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do , getFeatureInterface votesFeature , getFeatureInterface adminLogFeature , getFeatureInterface siteMapFeature + , getFeatureInterface userNotifyFeature , getFeatureInterface packageFeedFeature , getFeatureInterface packageInfoJSONFeature #endif diff --git a/src/Distribution/Server/Features/AdminLog.hs b/src/Distribution/Server/Features/AdminLog.hs index e32ac14eb..35f01fcf0 100755 --- a/src/Distribution/Server/Features/AdminLog.hs +++ b/src/Distribution/Server/Features/AdminLog.hs @@ -1,6 +1,6 @@ {-# LANGUAGE DeriveDataTypeable, TypeFamilies, TemplateHaskell, BangPatterns, GeneralizedNewtypeDeriving, NamedFieldPuns, RecordWildCards, - PatternGuards #-} + PatternGuards, RankNTypes #-} module Distribution.Server.Features.AdminLog where @@ -77,6 +77,7 @@ makeAcidic ''AdminLog ['getAdminLog data AdminLogFeature = AdminLogFeature { adminLogFeatureInterface :: HackageFeature + , queryGetAdminLog :: forall m. MonadIO m => m AdminLog } instance IsHackageFeature AdminLogFeature where @@ -117,6 +118,9 @@ adminLogFeature UserFeature{..} adminLogState resourceGet = [("html", serveAdminLogGet)] } + queryGetAdminLog :: MonadIO m => m AdminLog + queryGetAdminLog = queryState adminLogState GetAdminLog + serveAdminLogGet _ = do aLog <- queryState adminLogState GetAdminLog users <- queryGetUserDb diff --git a/src/Distribution/Server/Features/Tags.hs b/src/Distribution/Server/Features/Tags.hs index 6654aac82..d6c526e8e 100644 --- a/src/Distribution/Server/Features/Tags.hs +++ b/src/Distribution/Server/Features/Tags.hs @@ -39,7 +39,6 @@ import Data.Function (fix) import Data.List (foldl') import Data.Char (toLower) - data TagsFeature = TagsFeature { tagsFeatureInterface :: HackageFeature, @@ -64,6 +63,8 @@ data TagsFeature = TagsFeature { -- initial import. setCalculatedTag :: Tag -> Set PackageName -> IO (), + tagProposalLog :: MemState (Map PackageName (Set Tag, Set Tag)), + withTagPath :: forall a. DynamicPath -> (Tag -> Set PackageName -> ServerPartE a) -> ServerPartE a, collectTags :: forall m. MonadIO m => Set PackageName -> m (Map PackageName (Set Tag)), putTags :: Maybe String -> Maybe String -> Maybe String -> Maybe String -> PackageName -> ServerPartE (), @@ -97,9 +98,10 @@ initTagsFeature ServerEnv{serverStateDir} = do tagAlias <- tagsAliasComponent serverStateDir specials <- newMemStateWHNF emptyPackageTags updateTag <- newHook + tagProposalLog <- newMemStateWHNF Map.empty return $ \core@CoreFeature{..} upload user -> do - let feature = tagsFeature core upload user tagsState tagAlias specials updateTag + let feature = tagsFeature core upload user tagsState tagAlias specials updateTag tagProposalLog registerHookJust packageChangeHook isPackageChangeAny $ \(pkgid, mpkginfo) -> case mpkginfo of @@ -148,6 +150,7 @@ tagsFeature :: CoreFeature -> StateComponent AcidState TagAlias -> MemState PackageTags -> Hook (Set PackageName, Set Tag) () + -> MemState (Map PackageName (Set Tag, Set Tag)) -> TagsFeature tagsFeature CoreFeature{ queryGetPackageIndex } @@ -157,6 +160,7 @@ tagsFeature CoreFeature{ queryGetPackageIndex } tagsAlias calculatedTags tagsUpdated + tagProposalLog = TagsFeature{..} where tagsResource = fix $ \r -> TagsResource @@ -277,8 +281,10 @@ tagsFeature CoreFeature{ queryGetPackageIndex } Nothing -> [] addRev = Set.difference (fst revTags) (Set.fromList add `Set.union` Set.fromList radd') delRev = Set.difference (snd revTags) (Set.fromList del `Set.union` Set.fromList rdel') - void $ updateState tagsState $ SetPackageTags pkgname tagSet - void $ updateState tagsState $ InsertReviewTags' pkgname addRev delRev + modifyTags (a, d) = (a `Set.intersection` addRev, d `Set.intersection` delRev) + updateState tagsState $ SetPackageTags pkgname tagSet + updateState tagsState $ InsertReviewTags' pkgname addRev delRev + modifyMemState tagProposalLog (Map.adjust modifyTags pkgname) runHook_ tagsUpdated (Set.singleton pkgname, tagSet) return () else if user @@ -287,7 +293,9 @@ tagsFeature CoreFeature{ queryGetPackageIndex } calcTags <- queryTagsForPackage pkgname let addTags = Set.fromList aliases `Set.difference` calcTags delTags = Set.fromList del `Set.intersection` calcTags - void $ updateState tagsState $ InsertReviewTags pkgname addTags delTags + updateState tagsState $ InsertReviewTags pkgname addTags delTags + modifyMemState tagProposalLog (Map.insertWith (<>) pkgname (addTags, delTags)) + return () else errBadRequest "Authorization Error" [MText "You need to be logged in to propose tags"] _ -> errBadRequest "Tags not recognized" [MText "Couldn't parse your tag list. It should be comma separated with any number of alphanumerical tags. Tags can also also have -+#*."] Nothing -> errBadRequest "Tags not recognized" [MText "Couldn't parse your tag list. It should be comma separated with any number of alphanumerical tags. Tags can also also have -+#*."] diff --git a/src/Distribution/Server/Features/UserNotify.hs b/src/Distribution/Server/Features/UserNotify.hs new file mode 100644 index 000000000..bfcb0716a --- /dev/null +++ b/src/Distribution/Server/Features/UserNotify.hs @@ -0,0 +1,631 @@ +{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving, + TypeFamilies, TemplateHaskell, + RankNTypes, NamedFieldPuns, RecordWildCards, BangPatterns, + DefaultSignatures, OverloadedStrings #-} +{-# LANGUAGE TupleSections #-} +module Distribution.Server.Features.UserNotify ( + initUserNotifyFeature, + UserNotifyFeature(..), + NotifyPref(..), + ) where + +import Distribution.Package +import Distribution.Pretty + +import Distribution.Server.Users.Types(UserId, UserInfo (..)) +import Distribution.Server.Users.UserIdSet as UserIdSet +import qualified Distribution.Server.Users.Users as Users +import Distribution.Server.Users.Group + +import Distribution.Server.Packages.Types +import qualified Distribution.Server.Packages.PackageIndex as PackageIndex + +import Distribution.Server.Framework +import Distribution.Server.Framework.Templating +import Distribution.Server.Framework.BackupDump +import Distribution.Server.Framework.BackupRestore + +import Distribution.Server.Features.AdminLog +import Distribution.Server.Features.BuildReports +import qualified Distribution.Server.Features.BuildReports.BuildReport as BuildReport +import Distribution.Server.Features.Core +import Distribution.Server.Features.Tags +import Distribution.Server.Features.Users +import Distribution.Server.Features.UserDetails +import Distribution.Server.Features.Upload + +import qualified Data.Map as Map +import qualified Data.Set as Set + +import Data.Typeable (Typeable) +import Control.Monad.Reader (ask) +import Control.Monad.State (get, put) +import Data.SafeCopy (base, deriveSafeCopy) +import Distribution.Text (display) +import Text.CSV (CSV, Record) +import Text.XHtml hiding (base, text, ()) +import Text.PrettyPrint +import Data.List(intercalate) +import Data.Hashable (Hashable(..)) +import Data.Aeson.TH ( defaultOptions, deriveJSON ) + +import Data.Time (UTCTime(..), getCurrentTime, diffUTCTime, addUTCTime, defaultTimeLocale, formatTime) +import Data.Time.Format.Internal (buildTime) + +import Data.Bifunctor ( Bifunctor(second) ) +import Data.Maybe(fromMaybe, mapMaybe, fromJust, listToMaybe, maybeToList) + +import Network.Mail.Mime +import Network.URI(uriAuthority, uriRegName) + +import qualified Data.ByteString.Lazy.Char8 as BS +import qualified Data.ByteString.Char8 as BSS +import qualified Data.Text as T +import qualified Data.Vector as Vec +import qualified Data.Aeson as Aeson +import qualified Data.Aeson.Types as Aeson + +-- A feature to manage notifications to users when package metadata, etc is updated. + +{- +Some missing features: + -- better formatting with mail templates +-} + +data UserNotifyFeature = UserNotifyFeature { + userNotifyFeatureInterface :: HackageFeature, + + queryGetUserNotifyPref :: forall m. MonadIO m => UserId -> m (Maybe NotifyPref), + updateSetUserNotifyPref :: forall m. MonadIO m => UserId -> NotifyPref -> m () +} + +instance IsHackageFeature UserNotifyFeature where + getFeatureInterface = userNotifyFeatureInterface + +------------------------- +-- Types of stored data +-- + +data NotifyPref = NotifyPref + { + notifyOptOut :: Bool, + notifyRevisionRange :: NotifyRevisionRange, + notifyUpload :: Bool, + notifyMaintainerGroup :: Bool, + notifyDocBuilderReport :: Bool, + notifyPendingTags :: Bool + } + deriving (Eq, Read, Show, Typeable) + +defaultNotifyPrefs :: NotifyPref +defaultNotifyPrefs = NotifyPref { + notifyOptOut = True, -- TODO when we're comfortable with this we can change to False. + notifyRevisionRange = NotifyAllVersions, + notifyUpload = True, + notifyMaintainerGroup = True, + notifyDocBuilderReport = True, + notifyPendingTags = True + } + +data NotifyRevisionRange = NotifyAllVersions | NotifyNewestVersion | NoNotifyRevisions deriving (Eq, Enum, Read, Show, Typeable) + +instance Pretty NotifyRevisionRange where + pretty NoNotifyRevisions = text "No" + pretty NotifyAllVersions = text "All Versions" + pretty NotifyNewestVersion = text "Newest Version" + +instance Hashable NotifyRevisionRange where + hash = fromEnum + hashWithSalt s x = s `hashWithSalt` hash x + +instance MemSize NotifyPref where memSize _ = memSize ((True,True,True),(True,True, True)) + +data NotifyData = NotifyData {unNotifyData :: (Map.Map UserId NotifyPref, UTCTime)} deriving (Eq, Show, Typeable) + +instance MemSize NotifyData where memSize (NotifyData x) = memSize x + +emptyNotifyData :: IO NotifyData +emptyNotifyData = getCurrentTime >>= \x-> return (NotifyData (Map.empty, x)) + +$(deriveSafeCopy 0 'base ''NotifyRevisionRange) +$(deriveSafeCopy 0 'base ''NotifyPref) +$(deriveSafeCopy 0 'base ''NotifyData) +$(deriveJSON defaultOptions ''NotifyRevisionRange) + +------------------------------ +-- UI +-- + +-- | `Bool`'s 'FromJSON' instance can't parse strings: +-- +-- >>> import qualified Data.Aeson as Aeson +-- >>> import qualified Data.ByteString.Lazy.Char8 as BS +-- >>> Aeson.decode (BS.pack "\"true\"") :: Maybe Bool +-- Nothing +-- +-- However, form2json will pass JSON bool values as strings to the decoder. +-- So we define a newtype wrapping it up. +newtype OK = OK {unOK :: Bool} deriving (Eq, Show, Enum) + +instance Pretty OK where + pretty (OK True) = text "Yes" + pretty (OK False) = text "No" + +instance Aeson.ToJSON OK where + toJSON = Aeson.toJSON . unOK + +instance Aeson.FromJSON OK where + parseJSON (Aeson.Bool b) = pure (OK b) + parseJSON (Aeson.String "true") = pure (OK True) + parseJSON (Aeson.String "false") = pure (OK False) + parseJSON s@(Aeson.String _) = Aeson.prependFailure "parsing OK failed, " (Aeson.unexpected s) + parseJSON invalid = Aeson.prependFailure "parsing OK failed, " (Aeson.typeMismatch "Bool or String" invalid) + +instance Hashable OK where + hashWithSalt s x = s `hashWithSalt` fromEnum x + +data NotifyPrefUI + = NotifyPrefUI + { ui_notifyEnabled :: OK + , ui_notifyRevisionRange :: NotifyRevisionRange + , ui_notifyUpload :: OK + , ui_notifyMaintainerGroup :: OK + , ui_notifyDocBuilderReport :: OK + , ui_notifyPendingTags :: OK + } + deriving (Eq, Show, Typeable) + +$(deriveJSON (compatAesonOptionsDropPrefix "ui_") ''NotifyPrefUI) + +instance Hashable NotifyPrefUI where + hashWithSalt s NotifyPrefUI{..} = s + `hashWithSalt` hash ui_notifyEnabled + `hashWithSalt` hash ui_notifyRevisionRange + `hashWithSalt` hash ui_notifyUpload + `hashWithSalt` hash ui_notifyMaintainerGroup + `hashWithSalt` hash ui_notifyDocBuilderReport + `hashWithSalt` hash ui_notifyPendingTags + +notifyPrefToUI :: NotifyPref -> NotifyPrefUI +notifyPrefToUI NotifyPref{..} = NotifyPrefUI + { ui_notifyEnabled = OK (not notifyOptOut) + , ui_notifyRevisionRange = notifyRevisionRange + , ui_notifyUpload = OK notifyUpload + , ui_notifyMaintainerGroup = OK notifyMaintainerGroup + , ui_notifyDocBuilderReport = OK notifyDocBuilderReport + , ui_notifyPendingTags = OK notifyPendingTags + } + +notifyPrefFromUI :: NotifyPrefUI -> NotifyPref +notifyPrefFromUI NotifyPrefUI{..} = NotifyPref + { notifyOptOut = not (unOK ui_notifyEnabled) + , notifyRevisionRange = ui_notifyRevisionRange + , notifyUpload = unOK ui_notifyUpload + , notifyMaintainerGroup = unOK ui_notifyMaintainerGroup + , notifyDocBuilderReport = unOK ui_notifyDocBuilderReport + , notifyPendingTags = unOK ui_notifyPendingTags + } + +class ToRadioButtons a where + toRadioButtons :: String -> a -> Html + +renderRadioButtons :: (Eq a, Aeson.ToJSON a, Pretty a) => [a] -> String -> a -> Html +renderRadioButtons choices nm def = foldr1 (+++) $ map renderRadioButton choices + where + renderRadioButton choice = toHtml + [ input ! (if (def == choice) then (checked :) else id) + [thetype "radio", identifier htmlId, name nm, value choiceName] + , label ! [thefor htmlId] << display choice + ] + where + jsonName = Aeson.encode choice + -- try to strip quotes + choiceName = BS.unpack $ if BS.head jsonName == '"' && BS.last jsonName == '"' + then BS.init (BS.tail jsonName) + else jsonName + htmlId = nm ++ "." ++ choiceName + +instance ToRadioButtons NotifyRevisionRange where + toRadioButtons = renderRadioButtons [NoNotifyRevisions, NotifyAllVersions, NotifyNewestVersion] + +instance ToRadioButtons OK where + toRadioButtons = renderRadioButtons [OK True, OK False] + +------------------------------ +-- State queries and updates +-- + +getNotifyData :: Query NotifyData NotifyData +getNotifyData = ask + +replaceNotifyData :: NotifyData -> Update NotifyData () +replaceNotifyData = put + +getNotifyTime :: Query NotifyData UTCTime +getNotifyTime = fmap (snd . unNotifyData) ask + +setNotifyTime :: UTCTime -> Update NotifyData () +setNotifyTime t = do + NotifyData (m,_) <- get + put $! NotifyData (m,t) + +lookupNotifyPref :: UserId -> Query NotifyData (Maybe NotifyPref) +lookupNotifyPref uid = do + NotifyData (m,_) <- ask + return $! Map.lookup uid m + +addNotifyPref :: UserId -> NotifyPref -> Update NotifyData () +addNotifyPref uid info = do + NotifyData (m,t) <- get + put $! NotifyData (Map.insert uid info m,t) + +makeAcidic ''NotifyData [ + --queries + 'getNotifyData, + 'lookupNotifyPref, + 'getNotifyTime, + --updates + 'replaceNotifyData, + 'addNotifyPref, + 'setNotifyTime + ] + + +---------------------------- +-- Data backup and restore +-- + +userNotifyBackup :: RestoreBackup NotifyData +userNotifyBackup = go [] + where + go :: [(UserId, NotifyPref)] -> RestoreBackup NotifyData + go st = + RestoreBackup { + restoreEntry = \entry -> case entry of + BackupByteString ["notifydata.csv"] bs -> do + csv <- importCSV "notifydata.csv" bs + prefs <- importNotifyPref csv + return (go (prefs ++ st)) + + _ -> return (go st) + + , restoreFinalize = + return (NotifyData (Map.fromList st, fromJust (buildTime defaultTimeLocale []))) -- defaults to unixstart time + } + +importNotifyPref :: CSV -> Restore [(UserId, NotifyPref)] +importNotifyPref = sequence . map fromRecord . drop 2 + where + fromRecord :: Record -> Restore (UserId, NotifyPref) + fromRecord [uid,o,rr,ul,g,db,t] = do + puid <- parseText "user id" uid + po <- parseRead "notify opt out" o + prr <- parseRead "notify revsion" rr + pul <- parseRead "notify upload" ul + pg <- parseRead "notify group mod" g + pd <- parseRead "notify docbuilder" db + pt <- parseRead "notify pending tags" t + return (puid, NotifyPref po prr pul pg pd pt) + fromRecord x = fail $ "Error processing notify record: " ++ show x + +notifyDataToCSV :: BackupType -> NotifyData -> CSV +notifyDataToCSV _backuptype (NotifyData (tbl,_)) + = ["0.1"] + : [ "uid","freq","revisionrange","upload","group"] + : flip map (Map.toList tbl) (\(uid,np) -> + [display uid, show (notifyOptOut np), show (notifyRevisionRange np), show (notifyUpload np), show (notifyMaintainerGroup np), show (notifyDocBuilderReport np), show (notifyPendingTags np)] + ) + +---------------------------- +-- State Component +-- + +notifyStateComponent :: FilePath -> IO (StateComponent AcidState NotifyData) +notifyStateComponent stateDir = do + st <- openLocalStateFrom (stateDir "db" "UserNotify") =<< emptyNotifyData + return StateComponent { + stateDesc = "State to keep track of revision notifications" + , stateHandle = st + , getState = query st GetNotifyData + , putState = update st . ReplaceNotifyData + , backupState = \backuptype tbl -> + [csvToBackup ["notifydata.csv"] (notifyDataToCSV backuptype tbl)] + , restoreState = userNotifyBackup + , resetState = notifyStateComponent + } + +---------------------------- +-- Core Feature +-- + +initUserNotifyFeature :: ServerEnv + -> IO (UserFeature + -> CoreFeature + -> UploadFeature + -> AdminLogFeature + -> UserDetailsFeature + -> ReportsFeature + -> TagsFeature + -> IO UserNotifyFeature) +initUserNotifyFeature env@ServerEnv{ serverStateDir, serverTemplatesDir, + serverTemplatesMode } = do + -- Canonical state + notifyState <- notifyStateComponent serverStateDir + + -- Page templates + templates <- loadTemplates serverTemplatesMode + [serverTemplatesDir, serverTemplatesDir "UserNotify"] + [ "user-notify-form.html" ] + + return $ \users core uploadfeature adminlog userdetails reports tags -> do + let feature = userNotifyFeature env + users core uploadfeature adminlog userdetails reports tags + notifyState templates + return feature + + +userNotifyFeature :: ServerEnv + -> UserFeature + -> CoreFeature + -> UploadFeature + -> AdminLogFeature + -> UserDetailsFeature + -> ReportsFeature + -> TagsFeature + -> StateComponent AcidState NotifyData + -> Templates + -> UserNotifyFeature +userNotifyFeature ServerEnv{serverBaseURI, serverCron} + UserFeature{..} + CoreFeature{..} + UploadFeature{..} + AdminLogFeature{..} + UserDetailsFeature{..} + ReportsFeature{..} + TagsFeature{..} + notifyState templates + = UserNotifyFeature {..} + + where + userNotifyFeatureInterface = (emptyHackageFeature "user-notify") { + featureDesc = "Notifications to users on metadata updates." + , featureResources = [userNotifyResource] -- TODO we can add json features here for updating prefs + , featureState = [abstractAcidStateComponent notifyState] + , featureCaches = [] + , featureReloadFiles = reloadTemplates templates + , featurePostInit = setupNotifyCronJob + } + + -- Resources + -- + + userNotifyResource = + (resourceAt "/user/:username/notify.:format") { + resourceDesc = [ (GET, "get the notify preference of a user account") + , (PUT, "set the notify preference of a user account") + ] + , resourceGet = [ ("json", handlerGetUserNotify) + , ("html", handlerGetUserNotifyHtml) + ] + , resourcePut = [ ("json", handlerPutUserNotify) ] + } + + -- Queries and updates + -- + + queryGetUserNotifyPref :: MonadIO m => UserId -> m (Maybe NotifyPref) + queryGetUserNotifyPref uid = queryState notifyState (LookupNotifyPref uid) + + updateSetUserNotifyPref :: MonadIO m => UserId -> NotifyPref -> m () + updateSetUserNotifyPref uid np = updateState notifyState (AddNotifyPref uid np) + + -- Request handlers + -- + handlerGetUserNotify dpath = do + uid <- lookupUserName =<< userNameInPath dpath + guardAuthorised_ [IsUserId uid, InGroup adminGroup] + nprefui <- notifyPrefToUI . fromMaybe defaultNotifyPrefs <$> queryGetUserNotifyPref uid + return $ toResponse (Aeson.toJSON nprefui) + + handlerGetUserNotifyHtml dpath = do + (uid, uinfo) <- lookupUserNameFull =<< userNameInPath dpath + guardAuthorised_ [IsUserId uid, InGroup adminGroup] + nprefui@NotifyPrefUI{..} <- notifyPrefToUI . fromMaybe defaultNotifyPrefs <$> queryGetUserNotifyPref uid + showConfirmationOfSave <- not . Prelude.null <$> queryString (lookBSs "showConfirmationOfSave") + template <- getTemplate templates "user-notify-form.html" + cacheControl [Private] $ etagFromHash (nprefui, showConfirmationOfSave) + ok . toResponse $ + template + [ "username" $= display (userName uinfo) + , "showConfirmationOfSave" $= showConfirmationOfSave + , "notifyEnabled" $= toRadioButtons "notifyEnabled=%s" ui_notifyEnabled + , "notifyRevisionRange" $= toRadioButtons "notifyRevisionRange=%s" ui_notifyRevisionRange + , "notifyUpload" $= toRadioButtons "notifyUpload=%s" ui_notifyUpload + , "notifyMaintainerGroup" $= toRadioButtons "notifyMaintainerGroup=%s" ui_notifyMaintainerGroup + , "notifyDocBuilderReport" $= toRadioButtons "notifyDocBuilderReport=%s" ui_notifyDocBuilderReport + , "notifyPendingTags" $= toRadioButtons "notifyPendingTags=%s" ui_notifyPendingTags + ] + + handlerPutUserNotify dpath = do + uid <- lookupUserName =<< userNameInPath dpath + guardAuthorised_ [IsUserId uid, InGroup adminGroup] + nprefui <- expectAesonContent + updateSetUserNotifyPref uid (notifyPrefFromUI nprefui) + noContent $ toResponse () + + -- Engine + -- + setupNotifyCronJob = + addCronJob serverCron CronJob { + cronJobName = "send notifications", + cronJobFrequency = TestJobFrequency (60*60*2), -- 2hr (for testing you can decrease this) + cronJobOneShot = False, + cronJobAction = notifyCronAction + } + + notifyCronAction = do + (notifyPrefs, lastNotifyTime) <- unNotifyData <$> queryState notifyState GetNotifyData + now <- getCurrentTime + let trimLastTime = if diffUTCTime now lastNotifyTime > (60*60*6) -- cap at 6hr + then addUTCTime (negate $ (60*60*6)) now + else lastNotifyTime -- for testing you can increase this + users <- queryGetUserDb + + revisionsAndUploads <- collectRevisionsAndUploads trimLastTime now + revisionUploadNotifications <- foldM (genRevUploadList notifyPrefs) Map.empty revisionsAndUploads + let revisionUploadEmails = map (describeRevision users trimLastTime now) <$> revisionUploadNotifications + + groupActions <- collectAdminActions trimLastTime now + groupActionNotifications <- foldM (genGroupUploadList notifyPrefs) Map.empty groupActions + let groupActionEmails = mapMaybe (describeGroupAction users) <$> groupActionNotifications + + docReports <- collectDocReport trimLastTime now + docReportNotifications <- foldM (genDocReportList notifyPrefs) Map.empty docReports + let docReportEmails = map describeDocReport <$> docReportNotifications + + tagProposals <- collectTagProposals + tagProposalNotifications <- foldM (genTagProposalList notifyPrefs) Map.empty tagProposals + let tagProposalEmails = map describeTagProposal <$> tagProposalNotifications + + mapM_ sendNotifyEmail . Map.toList $ foldr1 (Map.unionWith (++)) [revisionUploadEmails, groupActionEmails, docReportEmails, tagProposalEmails] + updateState notifyState (SetNotifyTime now) + + formatTimeUser users t u = + display (Users.userIdToName users u) ++ " [" ++ + (formatTime defaultTimeLocale "%c" t) ++ "]" + + collectRevisionsAndUploads earlier now = do + pkgIndex <- queryGetPackageIndex + let isRecent pkgInfo = + let rt = pkgLatestUploadTime pkgInfo + in rt > earlier && rt <= now + return $ filter isRecent $ (PackageIndex.allPackages pkgIndex) + + collectAdminActions earlier now = do + aLog <- adminLog <$> queryGetAdminLog + let isRecent (t,_,_,_) = t > earlier && t <= now + return $ filter isRecent $ aLog + + collectDocReport earlier now = do + pkgs <- PackageIndex.allPackages <$> queryGetPackageIndex + pkgRpts <- forM pkgs $ \pkg -> do + rpts <- queryPackageReports (packageId pkg) + pure $ (pkg,) $ do + -- List monad, filter out recent docbuilds + (_, rpt@BuildReport.BuildReport{..}) <- rpts + t <- maybeToList time + guard $ docsOutcome /= BuildReport.NotTried && t > earlier && t <= now + pure rpt + let isBuildOk BuildReport.BuildReport{..} = docsOutcome == BuildReport.Ok + pure $ map (second (all isBuildOk)) $ filter (not . Prelude.null . snd) pkgRpts + + collectTagProposals = do + logs <- readMemState tagProposalLog + writeMemState tagProposalLog Map.empty + pure $ Map.toList logs + + genRevUploadList notifyPrefs mp pkg = do + pkgIndex <- queryGetPackageIndex + let actor = pkgLatestUploadUser pkg + isRevision = pkgNumRevisions pkg > 1 + pkgName = packageName . pkgInfoId $ pkg + mbLatest = listToMaybe . take 1 . reverse $ PackageIndex.lookupPackageName pkgIndex pkgName + isLatestVersion = maybe False (\x -> pkgInfoId pkg == pkgInfoId x) mbLatest + addNotification uid m = + if not (notifyOptOut npref) && + (isRevision && + ( notifyRevisionRange npref == NotifyAllVersions || + ((notifyRevisionRange npref == NotifyNewestVersion) && isLatestVersion)) + || + not isRevision && notifyUpload npref) + then Map.insertWith (++) uid [pkg] m + else m + where npref = fromMaybe defaultNotifyPrefs (Map.lookup uid notifyPrefs) + maintainers <- queryUserGroup $ maintainersGroup (packageName . pkgInfoId $ pkg) + return $ foldr addNotification mp (toList (delete actor maintainers)) + + genGroupUploadList notifyPrefs mp ga = + let (actor,gdesc) = case ga of (_,uid,Admin_GroupAddUser _ gd,_) -> (uid, gd) + (_,uid,Admin_GroupDelUser _ gd,_) -> (uid, gd) + addNotification uid m = if not (notifyOptOut npref) && notifyMaintainerGroup npref + then Map.insertWith (++) uid [ga] m + else m + where npref = fromMaybe defaultNotifyPrefs (Map.lookup uid notifyPrefs) + in case gdesc of + (MaintainerGroup pkg) -> do + maintainers <- queryUserGroup $ maintainersGroup (mkPackageName $ BS.unpack pkg) + return $ foldr addNotification mp (toList (delete actor maintainers)) + _ -> return mp + + genDocReportList notifyPrefs mp pkgDoc = do + let addNotification uid m = + if not (notifyOptOut npref) && notifyDocBuilderReport npref + then Map.insertWith (++) uid [pkgDoc] m + else m + where npref = fromMaybe defaultNotifyPrefs (Map.lookup uid notifyPrefs) + maintainers <- queryUserGroup $ maintainersGroup (packageName . pkgInfoId . fst $ pkgDoc) + return $ foldr addNotification mp (toList maintainers) + + genTagProposalList notifyPrefs mp pkgTags = do + let addNotification uid m = + if not (notifyOptOut npref) && notifyPendingTags npref + then Map.insertWith (++) uid [pkgTags] m + else m + where npref = fromMaybe defaultNotifyPrefs (Map.lookup uid notifyPrefs) + maintainers <- queryUserGroup $ maintainersGroup (fst pkgTags) + return $ foldr addNotification mp (toList maintainers) + + describeRevision users earlier now pkg = + if pkgNumRevisions pkg <= 1 + then "Package upload, " ++ display (packageName pkg) ++ ", by " ++ + formatTimeUser users (pkgLatestUploadTime pkg) (pkgLatestUploadUser pkg) + else "Package metadata revision(s), " ++ display (packageName pkg) ++ ":\n" ++ + unlines (map (uncurry (formatTimeUser users) . snd) recentRevs) + where + revs = reverse $ Vec.toList (pkgMetadataRevisions pkg) + recentRevs = filter ((\x -> x > earlier && x <= now) . fst . snd) revs + + describeGroupAction users (time, uid, act, descr) = + case act of + (Admin_GroupAddUser tn (MaintainerGroup pkg)) -> Just $ + "Group modified by " ++ formatTimeUser users time uid ++ ":\n" ++ + display (Users.userIdToName users tn) ++ " added to maintainers for " ++ BS.unpack pkg ++ + "\n" ++ "reason: " ++ BS.unpack descr + (Admin_GroupDelUser tn (MaintainerGroup pkg)) -> Just $ + "Group modified by " ++ formatTimeUser users time uid ++ ":\n" ++ + display (Users.userIdToName users tn) ++ " removed from maintainers for " ++ BS.unpack pkg ++ + "\n" ++ "reason: " ++ BS.unpack descr + _ -> Nothing + + describeDocReport (pkg, doc) = + "Package doc build for " ++ display (packageName pkg) ++ ":\n" ++ + if doc + then "Build successful." + else "Build failed." + + describeTagProposal (pkgName, (addTags, delTags)) = + "Pending tag propasal for " ++ display pkgName ++ ":\n" ++ + "Addition: " ++ showTags addTags ++ "\n" ++ + "Deletion: " ++ showTags delTags + where + showTags = intercalate ", " . map display . Set.toList + + sendNotifyEmail :: (UserId, [String]) -> IO () + sendNotifyEmail (uid, ebody) = do + mudetails <- queryUserDetails uid + case mudetails of + Nothing -> return () + Just (AccountDetails{accountContactEmail=eml, accountName=aname})-> do + let mailFrom = Address (Just (T.pack "Hackage website")) + (T.pack ("noreply@" ++ uriRegName ourHost)) + mail = (emptyMail mailFrom) { + mailTo = [Address (Just aname) eml], + mailHeaders = [(BSS.pack "Subject", + T.pack "[Hackage] Maintainer Notifications")], + mailParts = [[Part (T.pack "text/plain; charset=utf-8") + None DefaultDisposition [] (PartContent $ BS.pack $ intercalate ("\n\n") ebody)]] + } + Just ourHost = uriAuthority serverBaseURI + + renderSendMail mail --TODO: if we need any configuration of + -- sendmail stuff, has to go here diff --git a/src/Distribution/Server/Features/UserSignup.hs b/src/Distribution/Server/Features/UserSignup.hs index a1f4a00a6..10b7e99ea 100644 --- a/src/Distribution/Server/Features/UserSignup.hs +++ b/src/Distribution/Server/Features/UserSignup.hs @@ -336,8 +336,8 @@ userSignupFeature ServerEnv{serverBaseURI, serverCron} , resourceGet = [ ("", handlerGetSignupRequestNew) ] , resourcePost = [ ("", handlerPostSignupRequestNew) ] } - - captchaResource = + + captchaResource = (resourceAt "/users/register/captcha") { resourceDesc = [ (GET, "Get a new captcha") ] , resourceGet = [ ("json", handlerGetCaptcha) ] @@ -443,16 +443,16 @@ userSignupFeature ServerEnv{serverBaseURI, serverCron} handlerGetSignupRequestNew _ = do (timestamp, hash, base64image) <- liftIO makeCaptchaHash template <- getTemplate templates "SignupRequest.html" - ok $ toResponse $ template + ok $ toResponse $ template [ "timestamp" $= timestamp , "hash" $= hash , "base64image" $= base64image ] - + handlerGetCaptcha :: DynamicPath -> ServerPartE Response handlerGetCaptcha _ = do (timestamp, hash, base64image) <- liftIO makeCaptchaHash - ok $ toResponse $ Object $ KeyMap.fromList $ + ok $ toResponse $ Object $ KeyMap.fromList $ [ (Key.fromString "timestamp" , String (T.pack (show timestamp))) , (Key.fromString "hash" , String (T.decodeUtf8 hash)) , (Key.fromString "base64image", String (T.decodeUtf8 base64image)) @@ -517,7 +517,7 @@ userSignupFeature ServerEnv{serverBaseURI, serverCron} guardValidLookingName realname guardValidLookingEmail useremail - timestamp <- maybe (errBadRequest "Invalid request" [MText "Seems something went wrong with your request."]) + timestamp <- maybe (errBadRequest "Invalid request" [MText "Seems something went wrong with your request."]) pure (readMaybe timestampStr) when (diffUTCTime now timestamp > secondsToNominalDiffTime (10 * 60)) $ From 6384905db73bd73740d88f60eb9d2adcfd0390a0 Mon Sep 17 00:00:00 2001 From: gbaz Date: Wed, 4 Jan 2023 11:55:41 -0500 Subject: [PATCH 43/43] cleanup buncha partial functions for revdeps, elim use of MonadThrow (#1156) * cleanup partial functions for revdeps, elim use of MonadThrow, MonadCatch * fix tests enablement link --- benchmarks/RevDeps.hs | 4 +- datafiles/templates/Html/maintain.html.st | 2 +- src/Distribution/Server/Features/Html.hs | 18 +-- .../Server/Features/ReverseDependencies.hs | 95 ++++++------ .../Features/ReverseDependencies/State.hs | 143 ++++++++---------- tests/ReverseDependenciesTest.hs | 33 ++-- 6 files changed, 129 insertions(+), 166 deletions(-) diff --git a/benchmarks/RevDeps.hs b/benchmarks/RevDeps.hs index 25e3222c3..4585306ab 100644 --- a/benchmarks/RevDeps.hs +++ b/benchmarks/RevDeps.hs @@ -62,7 +62,7 @@ main :: IO () main = do packs :: Vector.Vector (Package TestPackage) <- randomPacks globalStdGen 20000 mempty let idx = PackageIndex.fromList $ map packToPkgInfo (Vector.toList packs) - Right revs <- pure $ constructReverseIndex idx + let revs = constructReverseIndex idx let numPacks = length packs defaultMain $ (:[]) $ @@ -70,7 +70,7 @@ main = do flip nfAppIO revs $ \revs' -> do select <- uniformRM (0, numPacks - 1) globalStdGen -- TODO why are there so many transitive deps? - length <$> + pure $ length $ getDependenciesFlat (packageName $ packToPkgInfo (packs Vector.! select)) revs' diff --git a/datafiles/templates/Html/maintain.html.st b/datafiles/templates/Html/maintain.html.st index 2a39e0cdc..256e69be1 100644 --- a/datafiles/templates/Html/maintain.html.st +++ b/datafiles/templates/Html/maintain.html.st @@ -48,7 +48,7 @@ package after its been released.
Test settings
If your package contains tests that can't run on hackage, you can disable them here. -

$versions:{pkgid|$pkgid$}; separator=", "$

+

$versions:{pkgid|$pkgid$}; separator=", "$

Trigger rebuild
diff --git a/src/Distribution/Server/Features/Html.hs b/src/Distribution/Server/Features/Html.hs index 21ab43c25..694093dc5 100644 --- a/src/Distribution/Server/Features/Html.hs +++ b/src/Distribution/Server/Features/Html.hs @@ -232,7 +232,7 @@ htmlFeature :: ServerEnv -> AsyncCache Response -> AsyncCache Response -> Templates - -> RecentPackagesFeature + -> RecentPackagesFeature -> (HtmlFeature, IO Response, IO Response) htmlFeature env@ServerEnv{..} @@ -526,7 +526,7 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} } ] - readParamWithDefaultAndValid :: (Read a, HasRqData m, Monad m, Functor m, Alternative m) => + readParamWithDefaultAndValid :: (Read a, HasRqData m, Monad m, Functor m, Alternative m) => a -> (a -> Bool) -> String -> m a readParamWithDefaultAndValid n f queryParam = do m <- optional (look queryParam) @@ -550,7 +550,7 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} pageSize <- lookupPageSize 20 let conf = Paging.createConf page pageSize recentPackages - + return . toResponse $ Pages.recentPage conf users recentPackages serveRecentRSS :: DynamicPath -> ServerPartE Response @@ -560,9 +560,9 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} page <- lookupPage 1 pageSize <- lookupPageSize 20 now <- liftIO getCurrentTime - + let conf = Paging.createConf page pageSize recentPackages - + return . toResponse $ Pages.recentFeed conf users serverBaseURI now recentPackages serveRevisionPage :: DynamicPath -> ServerPartE Response @@ -571,7 +571,7 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} users <- queryGetUserDb page <- lookupPage 1 pageSize <- lookupPageSize 40 - + let conf = Paging.createConf page pageSize revisions return . toResponse $ Pages.revisionsPage conf users revisions @@ -583,7 +583,7 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} page <- lookupPage 1 pageSize <- lookupPageSize 40 now <- liftIO getCurrentTime - + let conf = Paging.createConf page pageSize revisions return . toResponse $ Pages.recentRevisionsFeed conf users serverBaseURI now revisions @@ -614,7 +614,7 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} serveGraphJSON :: DynamicPath -> ServerPartE Response serveGraphJSON _ = do - graph <- revJSON + graph <- liftIO revJSON --TODO: use proper type for graph with ETag cacheControl [Public, maxAgeMinutes 30] (etagFromHash graph) ok . toResponse $ graph @@ -2178,7 +2178,7 @@ mkHtmlReverse HtmlUtilities{..} let pkgname = pkgName pkg pkgids <- lookupPackageName pkgname revCount <- revPackageStats pkgname - versions <- revForEachVersion pkgname + versions <- liftIO $ revForEachVersion pkgname return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ " - Reverse dependency statistics") $ reverseVerboseRender pkgname (map packageVersion pkgids) (corePackageIdUri "") revCount versions diff --git a/src/Distribution/Server/Features/ReverseDependencies.hs b/src/Distribution/Server/Features/ReverseDependencies.hs index 1a226b2c1..bff4c7f8e 100644 --- a/src/Distribution/Server/Features/ReverseDependencies.hs +++ b/src/Distribution/Server/Features/ReverseDependencies.hs @@ -21,13 +21,12 @@ import Distribution.Package import Distribution.Text (display) import Distribution.Version (Version) -import Control.Monad.Catch (MonadThrow, MonadCatch) import Data.Aeson import Data.ByteString.Lazy (ByteString) import Data.Containers.ListUtils (nubOrd) import Data.List (mapAccumL, sortOn) import qualified Data.List.NonEmpty as NE -import Data.Maybe (catMaybes, fromJust) +import Data.Maybe (catMaybes, mapMaybe, fromMaybe) import Data.Function (fix) import qualified Data.Bimap as Bimap import qualified Data.Array as Arr @@ -44,18 +43,18 @@ data ReverseFeature = ReverseFeature { reverseHook :: Hook [NE.NonEmpty PkgInfo] (), - queryReverseDeps :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m ([PackageName], [PackageName]), - revPackageId :: forall m. (MonadCatch m, MonadIO m) => PackageId -> m ReverseDisplay, - revPackageName :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m ReverseDisplay, - renderReverseRecent :: forall m. (MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> m ReversePageRender, - renderReverseOld :: forall m. (MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> m ReversePageRender, - revPackageFlat :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m [(PackageName, Int)], - revDirectCount :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m Int, - revPackageStats :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m ReverseCount, - revCountForAllPackages :: forall m. (MonadIO m, MonadCatch m) => m [(PackageName, ReverseCount)], - revJSON :: forall m. (MonadIO m, MonadThrow m) => m ByteString, + queryReverseDeps :: forall m. MonadIO m => PackageName -> m ([PackageName], [PackageName]), + revPackageId :: forall m. MonadIO m => PackageId -> m ReverseDisplay, + revPackageName :: forall m. MonadIO m => PackageName -> m ReverseDisplay, + renderReverseRecent :: forall m. MonadIO m => PackageName -> ReverseDisplay -> m ReversePageRender, + renderReverseOld :: forall m. MonadIO m => PackageName -> ReverseDisplay -> m ReversePageRender, + revPackageFlat :: forall m. MonadIO m => PackageName -> m [(PackageName, Int)], + revDirectCount :: forall m. MonadIO m => PackageName -> m Int, + revPackageStats :: forall m. MonadIO m => PackageName -> m ReverseCount, + revCountForAllPackages :: forall m. MonadIO m => m [(PackageName, ReverseCount)], + revJSON :: IO ByteString, revDisplayInfo :: forall m. MonadIO m => m VersionIndex, - revForEachVersion :: forall m. (MonadIO m, MonadThrow m) => PackageName -> m (Map.Map Version (Set PackageIdentifier)) + revForEachVersion :: PackageName -> IO (Map.Map Version (Set PackageIdentifier)) } instance IsHackageFeature ReverseFeature where @@ -86,7 +85,7 @@ initReverseFeature _ = do return $ \CoreFeature{queryGetPackageIndex,packageChangeHook} VersionsFeature{queryGetPreferredVersions} -> do idx <- queryGetPackageIndex - memState <- newMemStateWHNF =<< constructReverseIndex idx + memState <- newMemStateWHNF $ constructReverseIndex idx let feature = reverseFeature queryGetPackageIndex queryGetPreferredVersions memState updateReverse @@ -95,9 +94,7 @@ initReverseFeature _ = do Nothing -> return () --PackageRemoveHook Just pkginfo -> do index <- queryGetPackageIndex - r <- readMemState memState - added <- addPackage index (packageName pkgid) (getDepNames pkginfo) r - writeMemState memState added + modifyMemState memState $ addPackage index (packageName pkgid) (getDepNames pkginfo) runHook_ updateReverse [pure pkginfo] return feature @@ -179,29 +176,29 @@ reverseFeature queryGetPackageIndex queryReverseIndex :: MonadIO m => m ReverseIndex queryReverseIndex = readMemState reverseMemState - queryReverseDeps :: (MonadIO m, MonadCatch m) => PackageName -> m ([PackageName], [PackageName]) + queryReverseDeps :: MonadIO m => PackageName -> m ([PackageName], [PackageName]) queryReverseDeps pkgname = do ms <- readMemState reverseMemState - rdeps <- getDependencies pkgname ms - rdepsall <- getDependenciesFlat pkgname ms - let indirect = Set.difference rdepsall rdeps - return (Set.toList rdeps, Set.toList indirect) + let rdeps = getDependencies pkgname ms + rdepsall = getDependenciesFlat pkgname ms + indirect = Set.difference rdepsall rdeps + pure (Set.toList rdeps, Set.toList indirect) - revPackageId :: (MonadCatch m, MonadIO m) => PackageId -> m ReverseDisplay + revPackageId :: MonadIO m => PackageId -> m ReverseDisplay revPackageId pkgid = do dispInfo <- revDisplayInfo pkgIndex <- liftIO queryGetPackageIndex revs <- queryReverseIndex - perVersionReverse dispInfo pkgIndex revs pkgid + pure $ perVersionReverse dispInfo pkgIndex revs pkgid - revPackageName :: (MonadIO m, MonadCatch m) => PackageName -> m ReverseDisplay + revPackageName :: MonadIO m => PackageName -> m ReverseDisplay revPackageName pkgname = do dispInfo <- revDisplayInfo pkgIndex <- liftIO queryGetPackageIndex revs <- queryReverseIndex - perPackageReverse dispInfo pkgIndex revs pkgname + pure $ perPackageReverse dispInfo pkgIndex revs pkgname - revJSON :: (MonadIO m, MonadThrow m) => m ByteString + revJSON :: IO ByteString revJSON = do ReverseIndex revdeps nodemap _depmap <- queryReverseIndex let assoc = takeWhile (\(a,_) -> a < Bimap.size nodemap) $ Arr.assocs . Gr.transposeG $ revdeps @@ -216,7 +213,7 @@ reverseFeature queryGetPackageIndex prefs <- liftIO queryGetPreferredVersions return $ getDisplayInfo prefs pkgIndex - renderReverseWith :: (MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> (Maybe VersionStatus -> Bool) -> m ReversePageRender + renderReverseWith :: MonadIO m => PackageName -> ReverseDisplay -> (Maybe VersionStatus -> Bool) -> m ReversePageRender renderReverseWith pkg rev filterFunc = do let rev' = map fst $ Map.toList rev directCounts <- mapM revDirectCount (pkg:rev') @@ -224,19 +221,19 @@ reverseFeature queryGetPackageIndex toRender (i, i') (pkgname, (version, status)) = if filterFunc status then (,) (i+1, i') $ Just ReverseRender { rendRevPkg = PackageIdentifier pkgname version, rendRevStatus = status, - rendRevCount = fromJust $ lookup pkgname counts + rendRevCount = fromMaybe 0 $ lookup pkgname counts } else (,) (i, i'+1) Nothing (res, rlist) = mapAccumL toRender (0, 0) (Map.toList rev) - pkgCount = fromJust $ lookup pkg counts + pkgCount = fromMaybe 0 $ lookup pkg counts return $ ReversePageRender (catMaybes rlist) res pkgCount - renderReverseRecent :: (MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> m ReversePageRender + renderReverseRecent :: MonadIO m => PackageName -> ReverseDisplay -> m ReversePageRender renderReverseRecent pkg rev = renderReverseWith pkg rev $ \status -> case status of Just DeprecatedVersion -> False Nothing -> False _ -> True - renderReverseOld :: (MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> m ReversePageRender + renderReverseOld :: MonadIO m => PackageName -> ReverseDisplay -> m ReversePageRender renderReverseOld pkg rev = renderReverseWith pkg rev $ \status -> case status of Just DeprecatedVersion -> True Nothing -> True @@ -244,22 +241,20 @@ reverseFeature queryGetPackageIndex -- -- This could also differentiate between direct and indirect dependencies -- -- with a bit more calculation. - revPackageFlat :: (MonadIO m, MonadCatch m) => PackageName -> m [(PackageName, Int)] + revPackageFlat :: MonadIO m => PackageName -> m [(PackageName, Int)] revPackageFlat pkgname = do memState <- readMemState reverseMemState - deps <- getDependenciesFlat pkgname memState - let depList = Set.toList deps - counts <- mapM (`getTotalCount` memState) depList - return $ zip depList counts + let depList = Set.toList $ getDependenciesFlat pkgname memState + pure $ map (\d -> (d, getTotalCount d memState)) depList - revPackageStats :: (MonadIO m, MonadCatch m) => PackageName -> m ReverseCount + revPackageStats :: MonadIO m => PackageName -> m ReverseCount revPackageStats pkgname = do - (direct, transitive) <- getReverseCount pkgname =<< readMemState reverseMemState + (direct, transitive) <- getReverseCount pkgname <$> readMemState reverseMemState return $ ReverseCount direct transitive - revDirectCount :: (MonadIO m, MonadCatch m) => PackageName -> m Int + revDirectCount :: MonadIO m => PackageName -> m Int revDirectCount pkgname = do - getDirectCount pkgname =<< readMemState reverseMemState + getDirectCount pkgname <$> readMemState reverseMemState -- This returns a list of (package name, direct dependencies, flat dependencies) -- for all packages. An interesting fact: it even does so for packages which @@ -270,24 +265,26 @@ reverseFeature queryGetPackageIndex -- broken packages. -- -- The returned list is sorted ascendingly on directCount (see ReverseCount). - revCountForAllPackages :: (MonadIO m, MonadCatch m) => m [(PackageName, ReverseCount)] + revCountForAllPackages :: MonadIO m => m [(PackageName, ReverseCount)] revCountForAllPackages = do index <- liftIO queryGetPackageIndex let pkgnames = packageNames index counts <- mapM revPackageStats pkgnames return . sortOn (directCount . snd) $ zip pkgnames counts - revForEachVersion :: (MonadThrow m, MonadIO m) => PackageName -> m (Map.Map Version (Set PackageIdentifier)) + revForEachVersion :: PackageName -> IO (Map.Map Version (Set PackageIdentifier)) revForEachVersion pkg = do - ReverseIndex revs nodemap depmap <- readMemState reverseMemState - index <- liftIO queryGetPackageIndex - nodeid <- Bimap.lookup pkg nodemap - revDepNames <- mapM (`Bimap.lookupR` nodemap) (Set.toList $ suc revs nodeid) - let -- The key is the version of 'pkg', and the values are specific + ReverseIndex revs nodemap depmap <- readMemState reverseMemState + index <- queryGetPackageIndex + let revDepNames :: [PackageName] + revDepNames = case Bimap.lookup pkg nodemap of + Nothing -> [] + Just nodeid -> mapMaybe (`Bimap.lookupR` nodemap) (Set.toList $ suc revs nodeid) + let -- The key is the version of 'pkg', and the values are specific -- package versions that accept this version of pkg specified in the key revDepVersions :: [(Version, Set PackageIdentifier)] revDepVersions = do x <- nubOrd revDepNames pkginfo <- PackageIndex.lookupPackageName index pkg pure (packageVersion pkginfo, dependsOnPkg index (packageId pkginfo) x depmap) - pure $ Map.fromListWith Set.union revDepVersions + pure $ Map.fromListWith Set.union revDepVersions diff --git a/src/Distribution/Server/Features/ReverseDependencies/State.hs b/src/Distribution/Server/Features/ReverseDependencies/State.hs index e4f9a9632..b66c65fdf 100644 --- a/src/Distribution/Server/Features/ReverseDependencies/State.hs +++ b/src/Distribution/Server/Features/ReverseDependencies/State.hs @@ -29,9 +29,6 @@ module Distribution.Server.Features.ReverseDependencies.State import Prelude hiding (lookup) import Control.Arrow ((&&&)) -import Control.Monad (forM) -import Control.Monad.Catch -import Control.Monad.Reader (MonadIO) import qualified Data.Array as Arr ((!), assocs, accumArray) import Data.Bimap (Bimap, lookup, lookupR) import qualified Data.Bimap as Bimap @@ -39,7 +36,7 @@ import Data.Containers.ListUtils (nubOrd) import Data.List (union) import Data.Map (Map) import qualified Data.Map as Map -import Data.Maybe (catMaybes, mapMaybe, maybeToList) +import Data.Maybe (mapMaybe, maybeToList) import qualified Data.Set as Set import Data.Set (Set, fromList, toList, delete) import Data.Typeable (Typeable) @@ -76,52 +73,43 @@ instance MemSize Dependency where instance MemSize ReverseIndex where memSize (ReverseIndex a b c) = memSize3 a b c -constructReverseIndex :: MonadCatch m => PackageIndex PkgInfo -> m ReverseIndex -constructReverseIndex index = do +constructReverseIndex :: PackageIndex PkgInfo -> ReverseIndex +constructReverseIndex index = let nodePkgMap = foldr (uncurry Bimap.insert) Bimap.empty $ zip (PackageIndex.allPackageNames index) [0..] - (revs, dependencies) <- constructRevDeps index nodePkgMap - pure $ - ReverseIndex + (revs, dependencies) = constructRevDeps index nodePkgMap + in ReverseIndex { reverseDependencies = revs , packageNodeIdMap = nodePkgMap , deps = dependencies } -addPackage :: (MonadCatch m, MonadIO m) => PackageIndex PkgInfo -> PackageName -> [PackageName] - -> ReverseIndex -> m ReverseIndex -addPackage index pkgname dependencies ri@(ReverseIndex revs nodemap pkgIdToDeps) = do - let - npm = Bimap.tryInsert pkgname (Bimap.size nodemap) nodemap - new :: [(Int, [Int])] <- - forM dependencies $ \d -> - (,) <$> lookup d npm <*> fmap (:[]) (lookup pkgname npm) - let rd = insEdges (Bimap.size npm) new revs +addPackage :: PackageIndex PkgInfo -> PackageName -> [PackageName] + -> ReverseIndex -> ReverseIndex +addPackage index pkgname dependencies (ReverseIndex revs nodemap pkgIdToDeps) = + let npm = Bimap.tryInsert pkgname (Bimap.size nodemap) nodemap + pn = (:[]) <$> lookup pkgname npm + new :: [(Int, [Int])] + new = mapMaybe (\d -> (,) <$> lookup d npm <*> pn) dependencies + rd = insEdges (Bimap.size npm) new revs pkginfos = PackageIndex.lookupPackageName index pkgname newPackageDepMap = Map.fromList $ map (packageId &&& getDeps) pkginfos - pure - ri + in ReverseIndex { reverseDependencies = rd , packageNodeIdMap = npm , deps = Map.union newPackageDepMap pkgIdToDeps } -constructRevDeps :: forall m. MonadCatch m => PackageIndex PkgInfo -> Bimap PackageName NodeId -> m (RevDeps, Map PackageIdentifier [Dependency]) -constructRevDeps index nodemap = do +constructRevDeps :: PackageIndex PkgInfo -> Bimap PackageName NodeId -> (RevDeps, Map PackageIdentifier [Dependency]) +constructRevDeps index nodemap = let allPackages :: [PkgInfo] allPackages = concat $ PackageIndex.allPackagesByName index - nodeIdsOfDependencies :: PkgInfo -> m [(NodeId, NodeId)] - nodeIdsOfDependencies pkg = catMaybes <$> mapM findNodesIfPresent (getDepNames pkg) - where - findNodesIfPresent :: PackageName -> m (Maybe (NodeId, NodeId)) - findNodesIfPresent dep = do - eitherErrOrFound :: Either SomeException (NodeId, NodeId) <- - try $ (,) <$> lookup dep nodemap <*> lookup (packageName pkg) nodemap - pure $ either (const Nothing) Just eitherErrOrFound - -- This will mix dependencies of different versions of the same package, but that is intended. - edges <- traverse nodeIdsOfDependencies allPackages - let dependencies = Map.fromList $ map (packageId &&& getDeps) allPackages - - pure (Gr.buildG (0, Bimap.size nodemap) (nubOrd $ concat edges) + nodeIdsOfDependencies :: PkgInfo -> [(NodeId, NodeId)] + nodeIdsOfDependencies pkg = mapMaybe (\dep -> (,) <$> lookup dep nodemap <*> lookup (packageName pkg) nodemap) (getDepNames pkg) + -- This will mix dependencies of different versions of the same package, but that is intended. + edges = map nodeIdsOfDependencies allPackages + dependencies = Map.fromList $ map (packageId &&& getDeps) allPackages + + in (Gr.buildG (0, Bimap.size nodemap) (nubOrd $ concat edges) , dependencies ) @@ -170,21 +158,22 @@ type ReverseDisplay = Map PackageName (Version, Maybe VersionStatus) type VersionIndex = (PackageName -> (PreferredInfo, [Version])) -perPackageReverse :: MonadCatch m => (PackageName -> (PreferredInfo, [Version])) -> PackageIndex PkgInfo -> ReverseIndex -> PackageName -> m (Map PackageName (Version, Maybe VersionStatus)) -perPackageReverse indexFunc index revdeps pkg = do +perPackageReverse :: (PackageName -> (PreferredInfo, [Version])) -> PackageIndex PkgInfo -> ReverseIndex -> PackageName -> Map PackageName (Version, Maybe VersionStatus) +perPackageReverse indexFunc index revdeps pkg = let pkgids = (packageVersion. packageId) <$> PackageIndex.lookupPackageName index pkg - let best :: PackageId + best :: PackageId best = PackageIdentifier pkg (maximum pkgids) - perVersionReverse indexFunc index revdeps best - -perVersionReverse :: MonadCatch m => (PackageName -> (PreferredInfo, [Version])) -> PackageIndex PkgInfo -> ReverseIndex -> PackageId -> m (Map PackageName (Version, Maybe VersionStatus)) -perVersionReverse indexFunc index (ReverseIndex revs nodemap dependencies) pkg = do - found <- lookup (packageName pkg) nodemap - -- this will be too much, since we are throwing away the specific version - revDepNames :: Set PackageName <- fromList <$> mapM (`lookupR` nodemap) (toList $ suc revs found) - let packagemap :: Map PackageName (Set Version) - packagemap = Map.fromList $ map (\x -> (x, Set.map packageVersion $ dependsOnPkg index pkg x dependencies)) (toList revDepNames) - pure $ constructReverseDisplay indexFunc packagemap + in perVersionReverse indexFunc index revdeps best + +perVersionReverse :: (PackageName -> (PreferredInfo, [Version])) -> PackageIndex PkgInfo -> ReverseIndex -> PackageId -> Map PackageName (Version, Maybe VersionStatus) +perVersionReverse indexFunc index (ReverseIndex revs nodemap dependencies) pkg = case lookup (packageName pkg) nodemap of + Nothing -> Map.empty + Just found -> + -- this will be too much, since we are throwing away the specific version + let revDepNames = mapMaybe (`lookupR` nodemap) (toList $ suc revs found) + packagemap :: Map PackageName (Set Version) + packagemap = Map.fromList $ map (\x -> (x, Set.map packageVersion $ dependsOnPkg index pkg x dependencies)) revDepNames + in constructReverseDisplay indexFunc packagemap constructReverseDisplay :: (PackageName -> (PreferredInfo, [Version])) -> Map PackageName (Set Version) -> Map PackageName (Version, Maybe VersionStatus) constructReverseDisplay indexFunc = @@ -207,52 +196,38 @@ insEdges nodesize edges revdeps = Arr.accumArray union [] (0, nodesize) (edges + -------------------------------------- -getDependencies :: MonadCatch m => PackageName -> ReverseIndex -> m (Set PackageName) -getDependencies pkg revs = - names revs =<< getDependenciesRaw pkg revs +getDependencies :: PackageName -> ReverseIndex -> Set PackageName +getDependencies pkg revs = names revs $ getDependenciesRaw pkg revs -getDependenciesRaw :: MonadCatch m => PackageName -> ReverseIndex -> m (Set NodeId) -getDependenciesRaw pkg (ReverseIndex revdeps nodemap _) = do - enodeid <- try (lookup pkg nodemap) - onRight enodeid $ \nodeid -> - nodeid `delete` suc revdeps nodeid +getDependenciesRaw :: PackageName -> ReverseIndex -> Set NodeId +getDependenciesRaw pkg (ReverseIndex revdeps nodemap _) = + case lookup pkg nodemap of + Nothing -> mempty + Just nodeid -> delete nodeid (suc revdeps nodeid) -- | The flat/total/transitive/indirect reverse dependencies are all the packages that depend on something that depends on the given 'pkg' -getDependenciesFlat :: forall m. MonadCatch m => PackageName -> ReverseIndex -> m (Set PackageName) -getDependenciesFlat pkg revs = - names revs =<< getDependenciesFlatRaw pkg revs +getDependenciesFlat :: PackageName -> ReverseIndex -> Set PackageName +getDependenciesFlat pkg revs = names revs $ getDependenciesFlatRaw pkg revs -getDependenciesFlatRaw :: forall m. MonadCatch m => PackageName -> ReverseIndex -> m (Set NodeId) +getDependenciesFlatRaw :: PackageName -> ReverseIndex -> Set NodeId getDependenciesFlatRaw pkg (ReverseIndex revdeps nodemap _) = do - enodeid <- try (lookup pkg nodemap) - onRight enodeid $ \nodeid -> - nodeid `delete` fromList (Gr.reachable revdeps nodeid) + case lookup pkg nodemap of + Nothing -> mempty + Just nodeid -> delete nodeid $ fromList (Gr.reachable revdeps nodeid) -- | The direct dependencies depend on the given 'pkg' directly, i.e. not transitively -getDirectCount :: MonadCatch m => PackageName -> ReverseIndex -> m Int -getDirectCount pkg revs = do - length <$> getDependenciesRaw pkg revs +getDirectCount :: PackageName -> ReverseIndex -> Int +getDirectCount pkg revs = length $ getDependenciesRaw pkg revs -- | Given a set of NodeIds, look up the package names for all of them -names :: MonadThrow m => ReverseIndex -> Set NodeId -> m (Set PackageName) +names :: ReverseIndex -> Set NodeId -> Set PackageName names (ReverseIndex _ nodemap _) ids = do - fromList <$> mapM (`lookupR` nodemap) (toList ids) + fromList $ mapMaybe (`lookupR` nodemap) (toList ids) -onRight :: Monad m => Either SomeException t -> (t -> Set NodeId) -> m (Set NodeId) -onRight e fun = do - case e of - Left (_ :: SomeException) -> do - pure mempty - Right nodeid -> - pure $ fun nodeid -- | The flat/total/transitive/indirect dependency count is the amount of package names that depend transitively on the given 'pkg' -getTotalCount :: MonadCatch m => PackageName -> ReverseIndex -> m Int -getTotalCount pkg revs = do - length <$> getDependenciesFlatRaw pkg revs - -getReverseCount :: MonadCatch m => PackageName -> ReverseIndex -> m (Int, Int) -getReverseCount pkg revs = do - direct <- getDirectCount pkg revs - total <- getTotalCount pkg revs - pure (direct, total) +getTotalCount :: PackageName -> ReverseIndex -> Int +getTotalCount pkg revs = length $ getDependenciesFlatRaw pkg revs + +getReverseCount :: PackageName -> ReverseIndex -> (Int, Int) +getReverseCount pkg revs = (getDirectCount pkg revs, getTotalCount pkg revs) diff --git a/tests/ReverseDependenciesTest.hs b/tests/ReverseDependenciesTest.hs index 6c2bbf94d..3cf3b42dc 100644 --- a/tests/ReverseDependenciesTest.hs +++ b/tests/ReverseDependenciesTest.hs @@ -1,13 +1,10 @@ {-# LANGUAGE OverloadedStrings, NamedFieldPuns, TypeApplications, ScopedTypeVariables #-} module Main where -import Control.Monad (foldM) -import Control.Monad.Catch (MonadCatch, SomeException, catch) -import Control.Monad.IO.Class (MonadIO, liftIO) import qualified Data.Array as Arr import qualified Data.Bimap as Bimap import Data.Foldable (for_) -import Data.List (partition) +import Data.List (partition, foldl') import qualified Data.Map as Map import qualified Data.Set as Set @@ -26,7 +23,7 @@ import Test.Tasty.HUnit import qualified Hedgehog.Range as Range import qualified Hedgehog.Gen as Gen -import Hedgehog ((===), Group(Group), MonadGen, MonadTest, Property, PropertyT, checkSequential, failure, footnoteShow, forAll, property) +import Hedgehog ((===), Group(Group), MonadGen, Property, PropertyT, checkSequential, forAll, property) import RevDepCommon (Package(..), TestPackage(..), mkPackage, packToPkgInfo) @@ -51,7 +48,7 @@ mkRevFeat pkgs = do , migratedEphemeralPrefs = False } updateReverse <- newHook - constructed <- constructReverseIndex idx + let constructed = constructReverseIndex idx memState <- newMemStateWHNF constructed pure $ reverseFeature @@ -151,8 +148,8 @@ prop_constructRevDeps :: Property prop_constructRevDeps = property $ do packs <- genPacks let idx = PackageIndex.fromList $ map packToPkgInfo packs - ReverseIndex foldedRevDeps foldedMap foldedDeps <- foldM (packageFolder @_ @TestPackage idx) emptyReverseIndex packs - Right (ReverseIndex constructedRevDeps constructedMap constructedDeps) <- pure $ constructReverseIndex idx + let ReverseIndex foldedRevDeps foldedMap foldedDeps = foldl' (packageFolder idx) emptyReverseIndex packs + let (ReverseIndex constructedRevDeps constructedMap constructedDeps) = constructReverseIndex idx for_ (PackageIndex.allPackageNames idx) $ \name -> do foundFolded :: Int <- Bimap.lookup name foldedMap foundConstructed :: Int <- Bimap.lookup name constructedMap @@ -171,24 +168,18 @@ prop_statsEqualsDeps :: Property prop_statsEqualsDeps = property $ do packs <- genPacks let packages = map packToPkgInfo packs - Right revs <- pure $ constructReverseIndex $ PackageIndex.fromList packages + let revs = constructReverseIndex $ PackageIndex.fromList packages pkginfo <- forAll $ Gen.element packages let name = packageName pkginfo - directSet <- getDependenciesRaw name revs - totalSet <- getDependenciesFlatRaw name revs - directNames <- getDependencies name revs - totalNames <- getDependenciesFlat name revs + let directSet = getDependenciesRaw name revs + totalSet = getDependenciesFlatRaw name revs + directNames = getDependencies name revs + totalNames = getDependenciesFlat name revs length directSet === length directNames length totalSet === length totalNames -packageFolder :: (MonadCatch m, MonadIO m, MonadTest m, Show b) => PackageIndex PkgInfo -> ReverseIndex -> Package b -> m ReverseIndex -packageFolder index revindex pkg@(Package name _version deps) = - catch (liftIO $ addPackage index (mkPackageName $ show name) (map (mkPackageName . show) deps) revindex) - $ \(e :: SomeException) -> do - footnoteShow pkg - footnoteShow index - footnoteShow e - failure +packageFolder :: Show b => PackageIndex PkgInfo -> ReverseIndex -> Package b -> ReverseIndex +packageFolder index revindex (Package name _version deps) = addPackage index (mkPackageName $ show name) (map (mkPackageName . show) deps) revindex genPackage :: forall m b. (MonadGen m, Enum b, Bounded b, Ord b) => b -> [Package b] -> m (Package b)