text:
{-# LANGUAGE DeriveGeneric       #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Main where

import           Control.Applicative
import           Control.Monad
import           Data.Aeson
import qualified  Data.HashMap.Strict      as HM
import           Data.List                 (nub)
import           Data.List.NonEmpty        (NonEmpty (..))
import qualified Data.List.NonEmpty        as NE
import qualified Data.Map.Strict           as M
import           Data.Text                 (Text)
import qualified Data.Text                 as T
import           Data.Time.Calendar        (Day (..))
import           Data.Time.Clock           (UTCTime (..), secondsToDiffTime)
import qualified Data.Vector               as V
import           Database.Bloodhound
import           GHC.Generics              (Generic)
import           Network.HTTP.Client
import qualified Network.HTTP.Types.Status as NHTS
import           Prelude                   hiding (filter, putStrLn)
import           Test.Hspec

import           Test.Hspec.QuickCheck     (prop)
import           Test.QuickCheck

testServer  :: Server
testServer  = Server "http://localhost:9200"
testIndex   :: IndexName
testIndex   = IndexName "bloodhound-tests-twitter-1"
testMapping :: MappingName
testMapping = MappingName "tweet"

validateStatus :: Response body -> Int -> Expectation
validateStatus resp expected =
  (NHTS.statusCode $ responseStatus resp)
  `shouldBe` (expected :: Int)

createExampleIndex :: IO Reply
createExampleIndex = createIndex testServer defaultIndexSettings testIndex
deleteExampleIndex :: IO Reply
deleteExampleIndex = deleteIndex testServer testIndex

data ServerVersion = ServerVersion Int Int Int deriving (Show, Eq, Ord)

es14 :: ServerVersion
es14 = ServerVersion 1 4 0

es13 :: ServerVersion
es13 = ServerVersion 1 3 0

es12 :: ServerVersion
es12 = ServerVersion 1 2 0

es11 :: ServerVersion
es11 = ServerVersion 1 1 0

es10 :: ServerVersion
es10 = ServerVersion 1 0 0

serverBranch :: ServerVersion -> ServerVersion
serverBranch (ServerVersion majorVer minorVer patchVer) =
  ServerVersion majorVer minorVer patchVer

mkServerVersion :: [Int] -> Maybe ServerVersion
mkServerVersion [majorVer, minorVer, patchVer] =
  Just (ServerVersion majorVer minorVer patchVer)
mkServerVersion _                 = Nothing

getServerVersion :: Server -> IO (Maybe ServerVersion)
getServerVersion s = liftM extractVersion (getStatus s)
  where
    version'                    = T.splitOn "." . number . version
    toInt                       = read . T.unpack
    parseVersion v              = map toInt (version' v)
    extractVersion              = join . liftM (mkServerVersion . parseVersion)

testServerBranch :: IO (Maybe ServerVersion)
testServerBranch = getServerVersion testServer >>= \v -> return $ liftM serverBranch v

atleast :: ServerVersion -> IO Bool
atleast v = testServerBranch >>= \x -> return $ x >= Just (serverBranch v)

atmost :: ServerVersion -> IO Bool
atmost v = testServerBranch >>= \x -> return $ x <= Just (serverBranch v)

is :: ServerVersion -> IO Bool
is v = testServerBranch >>= \x -> return $ x == Just (serverBranch v)

when' :: Monad m => m Bool -> m () -> m ()
when' b f = b >>= \x -> when x f

data Location = Location { lat :: Double
                         , lon :: Double } deriving (Eq, Generic, Show)

data Tweet = Tweet { user     :: Text
                   , postDate :: UTCTime
                   , message  :: Text
                   , age      :: Int
                   , location :: Location }
           deriving (Eq, Generic, Show)

instance ToJSON   Tweet
instance FromJSON Tweet
instance ToJSON   Location
instance FromJSON Location

data TweetMapping = TweetMapping deriving (Eq, Show)

instance ToJSON TweetMapping where
  toJSON TweetMapping =
    object ["tweet" .=
      object ["properties" .=
        object ["location" .= object ["type" .= ("geo_point" :: Text)]]]]

exampleTweet :: Tweet
exampleTweet = Tweet { user     = "bitemyapp"
                     , postDate = UTCTime
                                  (ModifiedJulianDay 55000)
                                  (secondsToDiffTime 10)
                     , message  = "Use haskell!"
                     , age      = 10000
                     , location = Location 40.12 (-71.34) }

otherTweet :: Tweet
otherTweet = Tweet { user     = "notmyapp"
                   , postDate = UTCTime
                                (ModifiedJulianDay 55000)
                                (secondsToDiffTime 11)
                   , message  = "Use haskell!"
                   , age      = 1000
                   , location = Location 40.12 (-71.34) }

insertData :: IO ()
insertData = do
  _ <- deleteExampleIndex
  _ <- createExampleIndex
  _ <- putMapping testServer testIndex testMapping TweetMapping
  _ <- indexDocument testServer testIndex testMapping exampleTweet (DocId "1")
  _ <- refreshIndex testServer testIndex
  return ()

insertOther :: IO ()
insertOther = do
  _ <- indexDocument testServer testIndex testMapping otherTweet (DocId "2")
  _ <- refreshIndex testServer testIndex
  return ()

searchTweet :: Search -> IO (Either String Tweet)
searchTweet search = do
  reply <- searchByIndex testServer testIndex search
  let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
  let myTweet = fmap (hitSource . head . hits . searchHits) result
  return myTweet

searchExpectNoResults :: Search -> IO ()
searchExpectNoResults search = do
  reply <- searchByIndex testServer testIndex search
  let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
  let emptyHits = fmap (hits . searchHits) result
  emptyHits `shouldBe` Right []

searchExpectAggs :: Search -> IO ()
searchExpectAggs search = do
  reply <- searchAll testServer search
  let isEmpty x = return (M.null x)
  let result = decode (responseBody reply) :: Maybe (SearchResult Tweet)
  (result >>= aggregations >>= isEmpty) `shouldBe` Just False

searchValidBucketAgg :: (BucketAggregation a, FromJSON a, Show a) => Search -> Text -> (Text -> AggregationResults -> Maybe (Bucket a)) -> IO ()
searchValidBucketAgg search aggKey extractor = do
  reply <- searchAll testServer search
  let bucketDocs = docCount . head . buckets
  let result = decode (responseBody reply) :: Maybe (SearchResult Tweet)
  let count = result >>= aggregations >>= extractor aggKey >>= \x -> return (bucketDocs x)
  count `shouldBe` Just 1

searchTermsAggHint :: [ExecutionHint] -> IO ()
searchTermsAggHint hints = do
      let terms hint = TermsAgg $ (mkTermsAggregation "user") { termExecutionHint = Just hint }
      let search hint = mkAggregateSearch Nothing $ mkAggregations "users" $ terms hint
      forM_ hints $ searchExpectAggs . search
      forM_ hints (\x -> searchValidBucketAgg (search x) "users" toTerms)

searchTweetHighlight :: Search -> IO (Either String (Maybe HitHighlight))
searchTweetHighlight search = do
  reply <- searchByIndex testServer testIndex search
  let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
  let myHighlight = fmap (hitHighlight . head . hits . searchHits) result
  return myHighlight

data BulkTest = BulkTest { name :: Text } deriving (Eq, Generic, Show)
instance FromJSON BulkTest
instance ToJSON BulkTest

noDuplicates :: Eq a => [a] -> Bool
noDuplicates xs = nub xs == xs

instance Arbitrary RegexpFlags where
  arbitrary = oneof [ pure AllRegexpFlags
                    , pure NoRegexpFlags
                    , SomeRegexpFlags <$> arbitrary
                    ]

instance Arbitrary a => Arbitrary (NonEmpty a) where
  arbitrary = liftA2 (:|) arbitrary arbitrary

instance Arbitrary RegexpFlag where
  arbitrary = oneof [ pure AnyString
                    , pure Automaton
                    , pure Complement
                    , pure Empty
                    , pure Intersection
                    , pure Interval
                    ]

main :: IO ()
main = hspec $ do

  describe "index create/delete API" $ do
    it "creates and then deletes the requested index" $ do
      -- priming state.
      _ <- deleteExampleIndex
      resp <- createExampleIndex
      deleteResp <- deleteExampleIndex
      validateStatus resp 200
      validateStatus deleteResp 200

  describe "document API" $ do
    it "indexes, gets, and then deletes the generated document" $ do
      _ <- insertData
      docInserted <- getDocument testServer testIndex testMapping (DocId "1")
      let newTweet = eitherDecode
                     (responseBody docInserted) :: Either String (EsResult Tweet)
      fmap _source newTweet `shouldBe` Right exampleTweet

  describe "bulk API" $ do
    it "inserts all documents we request" $ do
      _ <- insertData
      let firstTest = BulkTest "blah"
      let secondTest = BulkTest "bloo"
      let firstDoc = BulkIndex testIndex
                     testMapping (DocId "2") (toJSON firstTest)
      let secondDoc = BulkCreate testIndex
                     testMapping (DocId "3") (toJSON secondTest)
      let stream = V.fromList [firstDoc, secondDoc]
      _ <- bulk testServer stream
      _ <- refreshIndex testServer testIndex
      fDoc <- getDocument testServer testIndex testMapping (DocId "2")
      sDoc <- getDocument testServer testIndex testMapping (DocId "3")
      let maybeFirst  = eitherDecode $ responseBody fDoc :: Either String (EsResult BulkTest)
      let maybeSecond = eitherDecode $ responseBody sDoc :: Either String (EsResult BulkTest)
      fmap _source maybeFirst `shouldBe` Right firstTest
      fmap _source maybeSecond `shouldBe` Right secondTest

  describe "query API" $ do
    it "returns document for term query and identity filter" $ do
      _ <- insertData
      let query = TermQuery (Term "user" "bitemyapp") Nothing
      let filter = IdentityFilter <&&> IdentityFilter
      let search = mkSearch (Just query) (Just filter)
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

    it "returns document for terms query and identity filter" $ do
      _ <- insertData
      let query = TermsQuery (NE.fromList [(Term "user" "bitemyapp")])
      let filter = IdentityFilter <&&> IdentityFilter
      let search = mkSearch (Just query) (Just filter)
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

    it "returns document for match query" $ do
      _ <- insertData
      let query = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
      let search = mkSearch (Just query) Nothing
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

    it "returns document for multi-match query" $ do
      _ <- insertData
      let fields = [FieldName "user", FieldName "message"]
      let query = QueryMultiMatchQuery $ mkMultiMatchQuery fields (QueryString "bitemyapp")
      let search = mkSearch (Just query) Nothing
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

    it "returns document for bool query" $ do
      _ <- insertData
      let innerQuery = QueryMatchQuery $
                       mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
      let query = QueryBoolQuery $
                  mkBoolQuery [innerQuery] [] []
      let search = mkSearch (Just query) Nothing
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

    it "returns document for boosting query" $ do
      _ <- insertData
      let posQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
      let negQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "notmyapp")
      let query = QueryBoostingQuery $ BoostingQuery posQuery negQuery (Boost 0.2)
      let search = mkSearch (Just query) Nothing
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

    it "returns document for common terms query" $ do
      _ <- insertData
      let query = QueryCommonTermsQuery $
                  CommonTermsQuery (FieldName "user")
                  (QueryString "bitemyapp")
                  (CutoffFrequency 0.0001)
                  Or Or Nothing Nothing Nothing Nothing
      let search = mkSearch (Just query) Nothing
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

  describe "sorting" $ do
    it "returns documents in the right order" $ do
      _ <- insertData
      _ <- insertOther
      let sortSpec = DefaultSortSpec $ mkSort (FieldName "age") Ascending
      let search = Search Nothing
                   (Just IdentityFilter) (Just [sortSpec]) Nothing Nothing
                   False 0 10
      reply <- searchByIndex testServer testIndex search
      let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
      let myTweet = fmap (hitSource . head . hits . searchHits) result
      myTweet `shouldBe` Right otherTweet

  describe "filtering API" $ do
    it "returns document for composed boolmatch and identity" $ do
      _ <- insertData
      let queryFilter = BoolFilter (MustMatch (Term "user" "bitemyapp") False)
                        <&&> IdentityFilter
      let search = mkSearch Nothing (Just queryFilter)
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

    it "returns document for term filter" $ do
      _ <- insertData
      let termFilter = TermFilter (Term "user" "bitemyapp") False
      let search = mkSearch Nothing (Just termFilter)
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

    it "returns document for existential filter" $ do
      _ <- insertData
      let search = mkSearch Nothing (Just (ExistsFilter (FieldName "user")))
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

    it "returns document for geo boundingbox filter" $ do
      _ <- insertData
      let box = GeoBoundingBox (LatLon 40.73 (-74.1)) (LatLon 40.10 (-71.12))
      let bbConstraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False GeoFilterMemory
      let geoFilter = GeoBoundingBoxFilter bbConstraint
      let search = mkSearch Nothing (Just geoFilter)
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

    it "doesn't return document for nonsensical boundingbox filter" $ do
      _ <- insertData
      let box          = GeoBoundingBox (LatLon 0.73 (-4.1)) (LatLon 0.10 (-1.12))
      let bbConstraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False GeoFilterMemory
      let geoFilter    = GeoBoundingBoxFilter bbConstraint
      let search       = mkSearch Nothing (Just geoFilter)
      searchExpectNoResults search

    it "returns document for geo distance filter" $ do
      _ <- insertData
      let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
      let distance = Distance 10.0 Miles
      let optimizeBbox = OptimizeGeoFilterType GeoFilterMemory
      let geoFilter = GeoDistanceFilter geoPoint distance SloppyArc optimizeBbox False
      let search = mkSearch Nothing (Just geoFilter)
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

    it "returns document for geo distance range filter" $ do
      _ <- insertData
      let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
      let distanceRange = DistanceRange (Distance 0.0 Miles) (Distance 10.0 Miles)
      let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
      let search = mkSearch Nothing (Just geoFilter)
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

    it "doesn't return document for wild geo distance range filter" $ do
      _ <- insertData
      let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
      let distanceRange = DistanceRange (Distance 100.0 Miles) (Distance 1000.0 Miles)
      let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
      let search = mkSearch Nothing (Just geoFilter)
      searchExpectNoResults search

    it "returns document for geo polygon filter" $ do
      _ <- insertData
      let points = [LatLon 40.0 (-70.00),
                    LatLon 40.0 (-72.00),
                    LatLon 41.0 (-70.00),
                    LatLon 41.0 (-72.00)]
      let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
      let search = mkSearch Nothing (Just geoFilter)
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

    it "doesn't return document for bad geo polygon filter" $ do
      _ <- insertData
      let points = [LatLon 40.0 (-70.00),
                    LatLon 40.0 (-71.00),
                    LatLon 41.0 (-70.00),
                    LatLon 41.0 (-71.00)]
      let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
      let search = mkSearch Nothing (Just geoFilter)
      searchExpectNoResults search

    it "returns document for ids filter" $ do
      _ <- insertData
      let filter = IdsFilter (MappingName "tweet") [DocId "1"]
      let search = mkSearch Nothing (Just filter)
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

    it "returns document for Double range filter" $ do
      _ <- insertData
      let filter = RangeFilter (FieldName "age")
                   (RangeDoubleGtLt (GreaterThan 1000.0) (LessThan 100000.0))
                   RangeExecutionIndex False
      let search = mkSearch Nothing (Just filter)
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

    it "returns document for UTCTime date filter" $ do
      _ <- insertData
      let filter = RangeFilter (FieldName "postDate")
                   (RangeDateGtLt
                    (GreaterThanD (UTCTime
                                (ModifiedJulianDay 55000)
                                (secondsToDiffTime 9)))
                    (LessThanD (UTCTime
                                (ModifiedJulianDay 55000)
                                (secondsToDiffTime 11))))
                   RangeExecutionIndex False
      let search = mkSearch Nothing (Just filter)
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

    it "returns document for regexp filter" $ do
      _ <- insertData
      let filter = RegexpFilter (FieldName "user") (Regexp "bite.*app")
                   AllRegexpFlags (CacheName "test") False (CacheKey "key")
      let search = mkSearch Nothing (Just filter)
      myTweet <- searchTweet search
      myTweet `shouldBe` Right exampleTweet

    it "doesn't return document for non-matching regexp filter" $ do
      _ <- insertData
      let filter = RegexpFilter (FieldName "user")
                   (Regexp "boy") AllRegexpFlags
                   (CacheName "test") False (CacheKey "key")
      let search = mkSearch Nothing (Just filter)
      searchExpectNoResults search

  describe "Aggregation API" $ do
    it "returns term aggregation results" $ do
      _ <- insertData
      let terms = TermsAgg $ mkTermsAggregation "user"
      let search = mkAggregateSearch Nothing $ mkAggregations "users" terms
      searchExpectAggs search
      searchValidBucketAgg search "users" toTerms

    it "can give collection hint parameters to term aggregations" $ when' (atleast es13) $ do
      _ <- insertData
      let terms = TermsAgg $ (mkTermsAggregation "user") { termCollectMode = Just BreadthFirst }
      let search = mkAggregateSearch Nothing $ mkAggregations "users" terms
      searchExpectAggs search
      searchValidBucketAgg search "users" toTerms

    it "can give execution hint paramters to term aggregations" $ when' (atmost es11) $ do
      _ <- insertData
      searchTermsAggHint [Map, Ordinals]

    it "can give execution hint paramters to term aggregations" $ when' (is es12) $ do
      _ <- insertData
      searchTermsAggHint [GlobalOrdinals, GlobalOrdinalsHash, GlobalOrdinalsLowCardinality, Map, Ordinals]

    it "can give execution hint paramters to term aggregations" $ when' (atleast es12) $ do
      _ <- insertData
      searchTermsAggHint [GlobalOrdinals, GlobalOrdinalsHash, GlobalOrdinalsLowCardinality, Map]

    it "returns date histogram aggregation results" $ do
      _ <- insertData
      let histogram = DateHistogramAgg $ mkDateHistogram (FieldName "postDate") Minute
      let search = mkAggregateSearch Nothing (mkAggregations "byDate" histogram)
      searchExpectAggs search
      searchValidBucketAgg search "byDate" toDateHistogram

    it "returns date histogram using fractional date" $ do
      _ <- insertData
      let periods            = [Year, Quarter, Month, Week, Day, Hour, Minute, Second]
      let fractionals        = map (FractionalInterval 1.5) [Weeks, Days, Hours, Minutes, Seconds]
      let intervals          = periods ++ fractionals
      let histogram          = mkDateHistogram (FieldName "postDate")
      let search interval    = mkAggregateSearch Nothing $ mkAggregations "byDate" $ DateHistogramAgg (histogram interval)
      let expect interval    = searchExpectAggs (search interval)
      let valid interval     = searchValidBucketAgg (search interval) "byDate" toDateHistogram
      forM_ intervals expect
      forM_ intervals valid

  describe "Highlights API" $ do

    it "returns highlight from query when there should be one" $ do
      _ <- insertData
      _ <- insertOther
      let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
      let testHighlight = Highlights Nothing [FieldHighlight (FieldName "message") Nothing]

      let search = mkHighlightSearch (Just query) testHighlight
      myHighlight <- searchTweetHighlight search
      myHighlight `shouldBe` Right (Just (M.fromList [("message",["Use <em>haskell</em>!"])]))

    it "doesn't return highlight from a query when it shouldn't" $ do
      _ <- insertData
      _ <- insertOther
      let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
      let testHighlight = Highlights Nothing [FieldHighlight (FieldName "user") Nothing]

      let search = mkHighlightSearch (Just query) testHighlight
      myHighlight <- searchTweetHighlight search
      myHighlight `shouldBe` Right Nothing

  describe "ToJSON RegexpFlags" $ do
    it "generates the correct JSON for AllRegexpFlags" $
      toJSON AllRegexpFlags `shouldBe` String "ALL"

    it "generates the correct JSON for NoRegexpFlags" $
      toJSON NoRegexpFlags `shouldBe` String "NONE"

    it "generates the correct JSON for SomeRegexpFlags" $
      let flags = AnyString :| [ Automaton
                               , Complement
                               , Empty
                               , Intersection
                               , Interval ]
      in toJSON (SomeRegexpFlags flags) `shouldBe` String "ANYSTRING|AUTOMATON|COMPLEMENT|EMPTY|INTERSECTION|INTERVAL"

    prop "removes duplicates from flags" $ \(flags :: RegexpFlags) ->
      let String str = toJSON flags
          flagStrs   = T.splitOn "|" str
      in noDuplicates flagStrs

  describe "omitNulls" $ do
    it "checks that omitNulls drops list elements when it should" $
       let dropped = omitNulls $ [ "test1" .= (toJSON ([] :: [Int]))
                                 , "test2" .= (toJSON ("some value" :: Text))]
       in dropped `shouldBe` Object (HM.fromList [("test2", String "some value")])

    it "checks that omitNulls doesn't drop list elements when it shouldn't" $
       let notDropped = omitNulls $ [ "test1" .= (toJSON ([1] :: [Int]))
                                    , "test2" .= (toJSON ("some value" :: Text))]
       in notDropped `shouldBe` Object (HM.fromList [ ("test1", Array (V.fromList [Number 1.0]))
                                                 , ("test2", String "some value")])
    it "checks that omitNulls drops non list elements when it should" $
       let dropped = omitNulls $ [ "test1" .= (toJSON Null)
                                 , "test2" .= (toJSON ("some value" :: Text))]
       in dropped `shouldBe` Object (HM.fromList [("test2", String "some value")])
    it "checks that omitNulls doesn't drop non list elements when it shouldn't" $
       let notDropped = omitNulls $ [ "test1" .= (toJSON (1 :: Int))
                                    , "test2" .= (toJSON ("some value" :: Text))]
       in notDropped `shouldBe` Object (HM.fromList [ ("test1", Number 1.0)
                                                 , ("test2", String "some value")])


text:
Raphael Warnock Is Tapping Into a Long Legacy of Faith-Based Civil Rights Activism in his Quest for the Senate
And he's become the target of ugly Republican attacks in the process.
Digital Media FellowBio
Rich Von Biberstein/Icon SMI/ZUMA
The Reverend Raphael Warnock is at the center of a political vortex. If the Democrat wins the neck-and-neck runoff against Republican Sen. Kelly Loeffler on January 5, he'll make history as Georgia's first Black senator—and potentially flip the Senate to Democratic control.
And while runoff elections in Georgia are traditionally disasters for Democrats, as our colleague Ari Berman recently wrote, this time may be different. Warnock, who preaches at the former church of Martin Luther King Jr., carries the legacy of Black religious leaders who fought for civil rights. He is beloved by young progressive organizers. And there's record cash hauls and enthusiasm backing him, as well as his fellow Democrat and runoff candidate Jon Ossoff. Still, Warnock faces brutal and racist attacks from Republicans, who have unleashed old stereotypes and prejudices about the Black church to smear the candidate as a dangerous radical socialist.
In these last weeks before the big vote, Mother Jones reporter Becca Andrews, who wrote a profile of Warnock in October, went to the state to cover the final push. "His potential victory for the US Senate runoffs depends on the vote outside Atlanta," Andrews told Mother Jones Podcast host Jamilah King. "I wanted to a get a sense for how people feel about him in more rural Georgia."
Andrews recently followed the campaign, from Augusta to Albany to Athens, and found out just what is making it tick.
Warnock's faith has been a huge part of his run, and his events are filled with sermons and hymns. In a normal year, many of his campaign stops would have been in churches. And while he is still reaching voters at church services, they are outdoor drive-in affairs. "Warnock has come out of a tradition of believing we're first of all, servants of all," said the Reverend Christopher Johnson, a Methodist pastor in Augusta, Georgia, who Andrews spoke to on the road. "And we also believe in transcending—changing what is to what it ought to be."
While the fight for civil rights in the American South has long had close ties to religious groups, this year in Georgia there's a new element: Young progressive organizers have flocked to the state to get out the vote for a candidate who they believe shares their values. This is a particularly powerful dynamic in Albany, a town in southeast Georgia that was hit with a devastating coronavirus outbreak after a funeral in February turned into a super-spreader event. "[Warnock] talks about the importance of seeing people in their humanity and pre-existing conditions," Amna Farooqi, a 26-year-old community organizer there, told Andrews. "I feel like that messaging really resonates for a lot of people, especially considering that we were a hotspot in COVID."
Kiana Jackson, 23, is a lifelong resident of Albany and explained to Andrews why she is so excited about Warnock's candidacy: "When we have a Black man that is a progressive that's running to be a United States senator from a southern state, then we have to make sure that we're coming out and we're supporting them with everything we can."
"This Process Was Not Designed for People Who Look Like Me": Georgia's Electors Cast Their Historic Ballots for Joe Biden
Becca Andrews
Georgia Republicans Are Using Fake Claims of Fraud to Make It Harder to Vote
Runoff Elections in Georgia Are Disasters for Democrats. Here's Why This Time Is Different.
"We Shall Overcome": Raphael Warnock's (Not-So-Long-Shot) Bid to Be Georgia's First Black Senator

redpajama_set_name: RedPajamaCommonCrawl


text:
WATCH EDIFY
January 23, 2020 The Newspaper Serving LGBT Los Angeles
Honoring Gay Rights & Civil Rights Activist Bayard Rustin
New CA Bill to End the Epidemics of HIV, Hepatitis C and Other STDs in LGBTQ+ Community
LONG BEACH PRIDE Unveils New Logo, Drops "Lesbian & Gay" From Name
VIDEO: Grand Opening of Los Angeles LGBT Center's New Campus in South LA
GUEST COLUMN: Queer Mindfulness Meditation
City of WeHo Accepting Clothing Donations for LGBTQ+ Youth as Part of MLK Day of Service
Recent West Hollywood Property Listings
LGBTQ+ Ally Taylor Swift to Receive GLAAD's Vanguard Award
PLAY PREVIEW: A Queer Tribute to Frida Kahlo
Sobering New Year with the City of West Hollywood
Go out with a BOOM in West Hollywood this New Year's Eve.
WeHo's premier alcohol- and drug-free New Year's Eve event theme is "House of #BOOM!" It is said to bring hundreds of people together for a "fabulous celebration, featuring DJ Eddie X, dancing, food, a photo booth, face and body painting and an alcohol-free midnight toast." T here will be a costume contest for those wearing the most fabulous costume representing the theme of either Glamour; Lady Gaga vs. Madonna; Menage a Trio; Hot Mess; or Military.
#BOOM! will take place on the evening of Monday, December 31, 2018, starting at 8 p.m. and will run until 1 a.m. on Tuesday, January 1, 2019 at the West Hollywood Park Auditorium, located at 647 N. San Vicente Boulevard. #BOOM! is a free event. Attendees must be 18 or older to attend. Advance RSVP is encouraged, as #BOOM! has limited capacity. RSVP at www. boom2019.eventbrite.com.
"The holiday season can be a particularly challenging time for people with substance abuse or mental health issues," a press release stated. "#BOOM! is a safe space for anyone who wants to celebrate the New Year without the presence of alcohol or drugs. The event is spearheaded by the West Hollywood Project, an initiative funded by the Los Angeles County Department of Public Health, to reduce alcohol and drug-related harms in West Hollywood."
Substance use and addiction is often a significant problem in the LGBTQ+ community. People who identify as such are at a greater risk for substance use and mental health issues compared to those who identify as heterosexual. More than twice as many LGBTQ+ adults compared to heterosexual adults reported using drugs in the past year, according to a 2015 report. Those who identified as LGBTQ+ were also more likely to smoke cigarettes, drink alcohol and binge drink, and nearly twice as likely to have had an alcohol or drug problem in the past year.
Limited parking for #BOOM! will be available in the adjacent West Hollywood Park five-story parking structure, located at 625 N. San Vicente Boulevard. The PickUp, West Hollywood's Free Weekend Ride, will run along Santa Monica Boulevard in a sixmile loop with 15 stops in each direction between Robertson Boulevard and La Brea Avenue from 8 p.m. on Monday, December 31, 2018 until 3 a.m. on Tuesday, January 1, 2019. It arrives at stops approximately every 15 minutes to transport revelers safely to and from their destinations, while music mixes by DJ Derek Monteiro play on board.
The City of West Hollywood will also be running a special schedule of its free shuttle service, Cityline, departing its stop near Hollywood and Highland every hour from 7:30 p.m. to 10:30 p.m., with stops at La Brea Avenue, Fairfax Avenue, and N. San Vicente Boulevard. Return trips from the San Vicente stop will begin at 8 p.m. and run approximately every hour with the final departure to Hollywood and Highland at 1:15 a.m. on January 1, 2019.
#BOOM! is sponsored by the City of West Hollywood, the Institute for Public Strategies, the Los Angeles LGBT Center, the West Hollywood Project, the Tweakers Project, and APLA Health. Admission is free but capacity is limited. For more information, please visit www. boom2019.eventbrite.com.
By Amy Patton December 29, 2018
Tags: adiction, City of, lgbq, LGBT, LGBTQIA, new year, New Year's Eve, Party, sober, substance, weho, west hollywood in A&E, CALENDAR, LOCAL, NIGHTLIFE, WEST HOLLYWOOD
A&E, BEST OF GAY LA, FEATURED, FEATURES, LOCAL, LOS ANGELES
January 22, 2020 thepridela
InsightLA is a non-profit meditation center offering practices of mindfulness and compassion that are both secular and Buddhist in origin. ...
ARTS, CALENDAR, CALIFORNIA, HISTORY, LOCAL, LONG BEACH, NEWS, Q&A, THEATER
January 21, 2020 Amy Patton
On this day in history 67 years ago, gay civil rights organizer Bayard Rustin was arrested on a discriminatory, anti-gay...
CALENDAR, Holiday, LOCAL, WEST HOLLYWOOD
Today, in honor of Martin Luther King, Jr. Day of Service, the City of West Hollywood encourages citizens to volunteer...
BUSINESS, FINANCE, LISTINGS, LOCAL, WEST HOLLYWOOD
Thinking of buying a house in West Hollywood? With a population under 40,000 and an area of just under 2...
AIDS, CALIFORNIA, HEALTH, LOCAL, NEWS
Senator Scott Wiener (D-San Francisco) recently introduced a bill that would require state agencies to create and enforce a master...
A&E, FEATURED, FEATURES, LOCAL, LOS ANGELES, Q&A, SPORTS, WEST HOLLYWOOD
WeHo Dodgeball at the Sin City Classic LGBTQ+ Sports Festival
Hosted annually by The Greater Los Angeles Softball Association (GLASA), Sin City Classic brings together the United State's finest athletes for a...
A&E, ARTS, BEST OF GAY LA, BUSINESS, LOCAL, LONG BEACH, NEWS, NIGHTLIFE, PRIDE
Formerly known as the Long Beach Lesbian and Gay Pride, the new and improved Long Beach Pride – the organization...
AIDS, EDUCATION, HEALTH, LOCAL, LOS ANGELES, NEWS, SEX, TECHNOLOGY, TRANS HEALTH, VIDEO
'Center South' will provide HIV testing; access to PrEP and PEP services; housing case management; mental health services; a computer lab; and...
LOCAL, LOS ANGELES, Q&A, SPORTS, WEST HOLLYWOOD
Sin City Classic Sports Festival has Los Angeles Roots
For those who cannot wait every two years for an iteration of the Olympics, or every four years for Gay...
A&E, LOCAL, LOS ANGELES, NIGHTLIFE, WEST HOLLYWOOD
LA's Only Known Trans Nightlife Event Just Got A Little More Fierce
Heavenly Bodies is Los Angeles' only known monthly transgender nightlife event featuring live performances hosted by trans model and activist, Sasha Colby. Proceeds...
CRIME, LOCAL, NEWS, SANTA MONICA, SEX
Craigslist Man Extorts Angelinos, Santa Monicans with Gay Sex, Pleads Guilty
Tyler Buchanan faces 10 years in federal prison By Sam Catanzaro A man has pleaded guilty to using Craigslist, among...
CRIME, LEGAL, LOCAL, NEWS, OBITUARY, WEST HOLLYWOOD
Honoring Timothy Dean One Year Later
January 8, 2020 Amy Patton
An intimate gathering was held Tuesday, Jan. 7 at West Hollywood Park to honor the 1-year anniversary of 55-year-old fashion...
A&E, ARTS, BEST OF GAY LA, BUSINESS, FILM, LOCAL, LOS ANGELES, NEWS
January 7, 2020 thepridela
GLAAD announced Tuesday, Jan. 7 that Taylor Swift with receive the Vanguard Award at the 31st Annual GLAAD Media Awards. ...
CALIFORNIA, HEALTH, Life, LOCAL, NEWS, POLITICS, TRANS, TRANS HEALTH
Three CA Bills for the LGBTQ+ Community to Keep An Eye On
Three key pieces of California legislation that were tabled last year for various reasons will see the floor in 2020. ...
A&E, HOLLYWOOD, LOCAL, LOS ANGELES, NIGHTLIFE, TELEVISION
LGBT Icon, Talkshow Host Ellen DeGeneres Wins Award at 77th Annual Golden Globes
The 77th annual Golden Globes is looking a little bit more LGBTQ-inclusive, and talkshow host and LGBTQ+ icon Ellen DeGeneres'...
Subscribe to our newsletter to receive the latest news and events in Los Angeles and the surrounding areas!
If you'd like to subscribe to additional lists, please select any here! Yo! Venice Brentwood News Century City-Westwood News Westside Today Santa Monica Mirror Palisades News
by thepridela
© 2020 The Pride LA
Made with ❤ by ENKI Tech