feat(faqs): initial

This commit is contained in:
Gregor Kleen 2020-04-23 16:52:34 +02:00
parent 4e8aaba782
commit 7b5337723d
15 changed files with 316 additions and 14 deletions

View File

@ -914,11 +914,11 @@ th, td
dt, .dt
font-weight: 600
&.sec
font-style: italic
font-size: 0.9rem
font-weight: 600
color: var(--color-fontsec)
&.sec
font-style: italic
font-size: 0.9rem
font-weight: 600
color: var(--color-fontsec)
dd, .dd
margin-left: 12px
@ -926,6 +926,12 @@ th, td
dd + dt, .dd + dt, dd + .dt, .dd + .dt
margin-top: 17px
.deflist--no-grid
dt, .dt
font-weight: 600
dd, .dd
margin-left: 12px
.explanation
font-style: italic
font-size: 0.9rem
@ -1320,3 +1326,35 @@ a.breadcrumbs__home
&--success
border-left-color: var(--color-success)
.faq__question
font-size: 20px
font-weight: 600
margin: 0
.faq__answer
margin-left: 17px
:not(.show-hide--collapsed) > .faq__answer
margin-top: 7px
.faq__section
padding-bottom: 17px
&:last-child, &.show-hide--collapsed
border-bottom: none
padding-bottom: 0
& + section:not(.faq__section)
border-top: 1px solid #d3d3d3
padding-top: 30px
.faq__section + .faq__section
margin-top: 17px
.faq__question-link
opacity: 0.2
&:hover
opacity: 1

View File

@ -57,23 +57,44 @@ export class ShowHide {
this._element.classList.add(SHOW_HIDE_TOGGLE_RIGHT_CLASS);
}
this._checkHash();
window.addEventListener('hashchange', this._checkHash.bind(this));
// mark as initialized
this._element.classList.add(SHOW_HIDE_INITIALIZED_CLASS, SHOW_HIDE_TOGGLE_CLASS);
}
destroy() {
this._element.removeEventListener('click', this._clickHandler);
}
destroy() {}
_addClickListener() {
this._element.addEventListener('click', this._clickHandler);
this._element.addEventListener('click', this._clickHandler.bind(this));
}
_clickHandler = () => {
const newState = this._element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS);
_show() {
this._element.parentElement.classList.remove(SHOW_HIDE_COLLAPSED_CLASS);
}
_toggle() {
return this._element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS);
}
_clickHandler(event) {
if (event.target.closest('a') && event.target.closest('a') !== this._element)
return;
if (event.target.matches('a') && event.target !== this._element)
return;
const newState = this._toggle();
if (this._showHideId) {
this._storageManager.save(this._showHideId, newState);
}
}
_checkHash() {
if (this._element.id && '#' + this._element.id === location.hash) {
this._show();
}
}
}

View File

@ -19,7 +19,7 @@ $show-hide-toggle-size: 6px
border-right: 2px solid currentColor
border-top: 2px solid currentColor
transition: transform .2s ease
transform: translateY(-50%) rotate(-45deg)
transform: translateY(2px) translateY(-50%) rotate(-45deg)
@media (max-width: 768px)
left: auto
@ -33,7 +33,7 @@ $show-hide-toggle-size: 6px
.show-hide--collapsed
.show-hide__toggle::before
transform: translateY(-50%) rotate(135deg)
transform: translateY(-2px) translateY(-50%) rotate(135deg)
& > :not(.show-hide__toggle)
display: block

View File

@ -0,0 +1,3 @@
FAQNoCampusAccount: Ich habe keinen LMU Campus-Login; kann ich trotzdem Zugang zum System erhalten?
FAQForgottenPassword: Ich habe mein Passwort vergessen
FAQCampusCantLogin: Ich kann mich mit meinem LMU Campus-Login nicht anmelden

View File

@ -1250,6 +1250,7 @@ MenuAllocationUsers: Bewerber
MenuAllocationPriorities: Zentrale Dringlichkeiten
MenuAllocationCompute: Platzvergabe berechnen
MenuAllocationAccept: Platzvergabe akzeptieren
MenuFaq: FAQ
BreadcrumbSubmissionFile: Datei
BreadcrumbSubmissionUserInvite: Einladung zur Abgabe
@ -1320,6 +1321,7 @@ BreadcrumbAllocationPriorities: Zentrale Dringlichkeiten
BreadcrumbAllocationCompute: Platzvergabe berechnen
BreadcrumbAllocationAccept: Platzvergabe akzeptieren
BreadcrumbMessageHide: Verstecken
BreadcrumbFaq: FAQ
ExternalExamEdit coursen@CourseName examn@ExamName: Bearbeiten: #{coursen}, #{examn}
ExternalExamGrades coursen@CourseName examn@ExamName: Prüfungsleistungen: #{coursen}, #{examn}
@ -2478,4 +2480,7 @@ BearerTokenOverrideExpiration: Ablaufzeitpunkt überschreiben
BearerTokenExpires: Ablaufzeitpunkt
BearerTokenExpiresTip: Wird der Ablaufzeitpunkt überschrieben und kein Ablaufzeitpunkt angegeben, ist das Token für immer gültig.
BearerTokenOverrideStart: Startzeitpunkt
BearerTokenOverrideStartTip: Wird kein Startzeitpunkt angegeben, wird bei Verwendung des Tokens nur der Ablaufzeitpunkt überprüft.
BearerTokenOverrideStartTip: Wird kein Startzeitpunkt angegeben, wird bei Verwendung des Tokens nur der Ablaufzeitpunkt überprüft.
FaqTitle: Häufig gestellte Fragen
AdditionalFaqs: Weitere häufig gestellte Fragen

1
routes
View File

@ -64,6 +64,7 @@
/info/legal LegalR GET !free
/info/allocation InfoAllocationR GET !free
/info/glossary GlossaryR GET !free
/info/faq FaqR GET !free
/version VersionR GET !free
/help HelpR GET POST !free

View File

@ -2198,6 +2198,7 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb LegalR = i18nCrumb MsgMenuLegal $ Just InfoR
breadcrumb InfoAllocationR = i18nCrumb MsgBreadcrumbAllocationInfo $ Just InfoR
breadcrumb VersionR = i18nCrumb MsgMenuVersion $ Just InfoR
breadcrumb FaqR = i18nCrumb MsgBreadcrumbFaq $ Just InfoR
breadcrumb HelpR = i18nCrumb MsgMenuHelp Nothing
@ -2550,6 +2551,14 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
, navQuick' = mempty
, navForceActive = False
}
, return $ NavFooter NavLink
{ navLabel = MsgMenuFaq
, navRoute = FaqR
, navAccess' = return True
, navType = NavTypeLink { navModal = False }
, navQuick' = mempty
, navForceActive = False
}
, return $ NavFooter NavLink
{ navLabel = MsgMenuGlossary
, navRoute = GlossaryR
@ -3126,6 +3135,17 @@ pageActions InstanceR = return
]
pageActions HelpR = return
[ NavPageActionPrimary
{ navLink = NavLink
{ navLabel = MsgMenuFaq
, navRoute = FaqR
, navAccess' = return True
, navType = NavTypeLink { navModal = False }
, navQuick' = mempty
, navForceActive = False
}
, navChildren = []
}
, NavPageActionPrimary
{ navLink = NavLink
{ navLabel = MsgInfoLecturerTitle
, navRoute = InfoLecturerR

View File

@ -2,6 +2,7 @@ module Handler.Help where
import Import
import Handler.Utils
import Handler.Info (faqsWidget)
import Jobs
import qualified Data.Map as Map
@ -68,4 +69,7 @@ postHelpR = do
, formEncoding = formEnctype
, formAttrs = [ asyncSubmitAttr | isModal ]
}
mFaqs <- (>>= \(mWgt, truncated) -> (, truncated) <$> mWgt) <$> traverse (faqsWidget $ Just 5) (Just <$> mReferer)
$(widgetFile "help")

View File

@ -85,3 +85,69 @@ getGlossaryR =
where
entries = $(i18nWidgetFiles "glossary")
msgMap = $(glossaryTerms "glossary")
mkFaqItems "faq"
mkMessageFor "UniWorX" "FAQItem" "messages/faq" "de-de-formal"
faqsWidget :: ( MonadHandler m, HandlerSite m ~ UniWorX )
=> Maybe Natural -> Maybe (Route UniWorX) -> m (Maybe Widget, Bool)
faqsWidget mLimit route = do
faqs <- for route $ \route' -> filterM (showFAQ route') universeF
MsgRenderer mr <- getMsgRenderer
let
rItems' = sortOn (CI.mk . views _1 mr) $ do
(k, wgt) <- Map.toList items
msg <- maybeToList $ Map.lookup k faqItemMap
whenIsJust faqs $ \faqs' ->
guard $ msg `elem` faqs'
return (msg, wgt)
rItems <- case (,) <$> route <*> mLimit of
Nothing -> return rItems'
Just (route', limit) -> do
let wIndices = zip [0..] rItems'
wPrios <- forM wIndices $ \x@(_, (msg, _)) -> (, x) . Just <$> prioFAQ route' msg
let prioLimited = go Nothing [] $ sortOn (views _1 Down) wPrios
where
go _ acc [] = acc
go maxP acc ((p, x) : xs)
| maxP == Just p || length acc < fromIntegral limit
= go (Just p) (x : acc) xs
| otherwise
= acc
return . map (view _2) $ sortOn (view _1) prioLimited
let truncated = length rItems < length rItems'
return ( guardOn (not $ null rItems') $(widgetFile "faq")
, truncated
)
where
items = $(i18nWidgetFiles "faq")
faqLink :: FAQItem -> Widget
faqLink = toWidget <=< toTextUrl . (FaqR :#:)
getFaqR :: Handler Html
getFaqR =
siteLayoutMsg' MsgFaqTitle $ do
setTitleI MsgFaqTitle
fromMaybe mempty . view _1 =<< faqsWidget Nothing Nothing
showFAQ :: ( MonadHandler m, HandlerSite m ~ UniWorX )
=> Route UniWorX -> FAQItem -> m Bool
showFAQ _ FAQNoCampusAccount = is _Nothing <$> maybeAuthId
showFAQ (AuthR _) FAQCampusCantLogin = return True
showFAQ _ FAQCampusCantLogin = is _Nothing <$> maybeAuthId
showFAQ (AuthR _) FAQForgottenPassword = return True
showFAQ _ FAQForgottenPassword = is _Nothing <$> maybeAuthId
-- showFAQ _ _ = return False
prioFAQ :: Monad m
=> Route UniWorX -> FAQItem -> m Rational
prioFAQ _ FAQNoCampusAccount = return 1
prioFAQ _ FAQCampusCantLogin = return 1
prioFAQ _ FAQForgottenPassword = return 1

View File

@ -1,5 +1,6 @@
module Handler.Info.TH
( glossaryTerms
, mkFaqItems
) where
import Import
@ -21,3 +22,52 @@ glossaryTerms basename = do
where
unPathPiece :: Text -> String
unPathPiece = repack . mconcat . map (over _head Char.toUpper) . Text.splitOn "-"
mkFaqItems :: FilePath -> DecsQ
mkFaqItems basename = do
itemsAvailable <- i18nWidgetFilesAvailable' basename
let items = Map.mapWithKey (\k _ -> "FAQ" <> unPathPiece k) itemsAvailable
sequence
[ dataD (cxt []) dataName [] Nothing
[ normalC (mkName conName) []
| (_, conName) <- Map.toAscList items
]
[ derivClause (Just StockStrategy)
[ conT ''Eq
, conT ''Ord
, conT ''Read
, conT ''Show
, conT ''Enum
, conT ''Bounded
, conT ''Generic
, conT ''Typeable
]
, derivClause (Just AnyclassStrategy)
[ conT ''Universe
, conT ''Finite
]
]
, instanceD (cxt []) (conT ''PathPiece `appT` conT dataName)
[ funD 'toPathPiece
[ clause [conP (mkName con) []] (normalB . litE . stringL $ repack int) []
| (int, con) <- Map.toList items
]
, funD 'fromPathPiece
[ clause [varP $ mkName "t"]
( guardedB
[ (,) <$> normalG [e|$(varE $ mkName "t") == int|] <*> [e|Just $(conE $ mkName con)|]
| (int, con) <- Map.toList items
]) []
, clause [wildP] (normalB [e|Nothing|]) []
]
]
, sigD (mkName "faqItemMap") [t|Map Text $(conT dataName)|]
, funD (mkName "faqItemMap")
[ clause [] (normalB [e| Map.fromList $(listE . map (\(int, con) -> tupE [litE . stringL $ repack int, conE $ mkName con]) $ Map.toList items) |]) []
]
]
where
unPathPiece :: Text -> String
unPathPiece = repack . mconcat . map (over _head Char.toUpper) . Text.splitOn "-"
dataName = mkName "FAQItem"

10
templates/faq.hamlet Normal file
View File

@ -0,0 +1,10 @@
$newline never
$forall (rItem, wgt) <- rItems
<section .faq__section>
<h2 .faq__question uw-show-hide data-show-hide-collapsed .show-hide__toggle ##{toPathPiece rItem}>
_{rItem}
\ #
<a href=^{faqLink rItem}>
<i .faq__question-link .fa .fa-link>
<div .faq__answer>
^{wgt}

View File

@ -1,4 +1,11 @@
<section>
_{MsgHelpIntroduction}
$maybe (faqs, truncated) <- mFaqs
^{faqs}
$if truncated
<section .faq__section>
<h2 .faq__question>
<a href=@{FaqR}>
_{MsgAdditionalFaqs}
<section>
^{formWidget}

View File

@ -0,0 +1,30 @@
$newline never
<p>
Können sie sich mit <i>exakt identischen</i> (idealerweise #
copy&paste) Daten #
im <a href="https://www.portal.uni-muenchen.de">Campus-Portal</a> #
anmelden?<br />
Falls nicht ist davon auszugehen, dass Sie Ihre Anmeldedaten falsch #
eingeben oder <a href=^{faqLink FAQNoCampusAccount}>keinen #
Campus-Login besitzen</a>.
<p>
Beachten Sie dabei auch, dass Uni2work Leerzeichen sowohl im #
Passwort als auch bei der Kennung berücksichtigt.<br />
Beim Passwort ist zudem Groß- und Kleinschreibung relevant.
<p>
Uni2work bietet zwei Login-Formulare.<br />
Für die Anmeldung mit der LMU Campus-Kennung müssen Sie das Formular #
„Campus-Login“ verwenden.
<p>
Falls Sie sich #
im <a href="https://www.portal.uni-muenchen.de">Campus-Portal</a> #
anmelden können, aber nicht in Uni2work, wenden Sie sich bitte über #
das <a href=@{HelpR}>Hilfe-Formular</a>, oben rechts auf jeder #
Seite, an die Uni2work-Administration.

View File

@ -0,0 +1,12 @@
$newline never
<p>
Wenn Sie sich gewöhnlicherweise mit Ihrem LMU Campus-Login anmelden, #
wenden Sie sich bitte an #
den <a href="https://www.it-servicedesk.uni-muenchen.de">IT-Servicedesk</a> #
um Ihr Passwort zurücksetzen zu lassen.
<p>
Wenn Sie sich mit einer Uni2work-internen Kennung anmelden wenden #
Sie sich dafür bitte über das <a href=@{HelpR}>Hilfe-Formular</a>, #
oben rechts auf jeder Seite, an die Uni2work-Administration.

View File

@ -0,0 +1,35 @@
$newline never
<p>
Uni2work-Administratoren können Uni2work-interne Kennungen ausstellen.
<p>
Wenden Sie sich dafür über das <a href=@{HelpR}>Hilfe-Formular</a>, #
oben rechts auf jeder Seite, an die Uni2work-Administration.
<p>
Beschreiben Sie dabei bitte Ihre Situation und begründen Sie, warum #
Sie Zugriff auf das System benötigen.
<p>
Um einen Uni2work-internen Account zu erstellen werden folgende #
Informationen benötigt, senden Sie diese bitte direkt mit Ihrer #
Anfrage:
<dl .deflist--no-grid>
<dt>Akademischer Titel
<dt>Vorname(n)
<dt>Vollständiger Nachname
<dd>Der Nachname kann aus mehreren Wörtern bestehen
<dd>Adelstitel und Prädikate wie „von“ und „zu“ sind Bestandteil des Nachnames
<dt>Vollständiger Name
<dd>Der vollständige Name muss mindestens den kompletten Nachnamen enthalten
<dd>Der vollständige Name kann zudem beliebige Teile der Vornamen und des akademischen Titels enthalten
<dt>E-Mail Addresse zur Anzeige
<dd>Wird anderen Benutzern angezeigt, wenn man z.B. als Kursverwalter oder Korrektor eingetragen ist
<dt>Matrikelnummer
<dt>Geschlecht
<dd>„Unbekannt“, „Männlich“, „Weiblich“ oder „Keine Angabe“
<dd>Nach <a href="https://en.wikipedia.org/wiki/ISO/IEC_5218">ISO 5218</a>
<dt>E-Mail Addresse zum Versand
<dd>An diese Addresse werden Mitteilungen von Uni2work versandt
<dd>Die zuverlässige Zustellung muss gewährleistet sein, daher keine Emails von freien Mailanbietern wie GMail, Hotmail, GMX, etc.