This commit is contained in:
root 2020-07-19 13:26:57 +00:00
parent 1bb080f7f1
commit 0c881c919b
15 changed files with 260 additions and 83 deletions

View file

@ -9,6 +9,7 @@
"@types/html-to-text": "^5.1.1",
"@types/ioredis": "^4.17.0",
"@types/jest": "^26.0.4",
"@types/moment": "^2.13.0",
"@types/mongoose": "^5.7.28",
"@types/multer": "^1.4.3",
"@types/multer-s3": "^2.7.7",
@ -26,6 +27,7 @@
"html-to-text": "^5.1.1",
"ioredis": "^4.17.3",
"jest": "^26.1.0",
"moment": "^2.27.0",
"mongoose": "^5.9.20",
"multer": "^1.4.2",
"multer-s3": "^2.9.0",

View file

@ -17,9 +17,9 @@ export default class Utils {
bool = bool && (
explored !== undefined && explored !== null
)
if (i - 1 === levels && isStr) {
bool = bool && typeof explored === 'string' && explored.length > 0
}
if (isStr) {
bool = bool && typeof explored === 'string' && explored.length > 0
}
return bool
}

View file

@ -16,6 +16,12 @@ import twig from 'twig'
import EmailService from './EmailService'
import ErrorController from './controllers/ErrorController'
process.on('unhandledRejection', (err) => {
console.error(err)
console.log('unhandledRejection!')
process.exit()
})
dotenv.config({
path: __dirname + '/../.env'
})
@ -110,11 +116,7 @@ let main = async () => {
app.get('/500', ErrorController.internalError)
app.use((err: any, req: express.Request, res: express.Response, next: express.RequestHandler) => {
console.error(err.stack)
//res.status(res.statusCode).json({ success: false, error: err.stack })
ErrorController.handle(500, 'Erreur interne', 'Ouups je sais pas quoi dire', '💥', err.stack)(req, res)
})
app.use(ErrorController.handleServerError)
app.use(ErrorController.notFoundHandler())

View file

@ -30,11 +30,22 @@ export default class AdminOrganizationController {
let body: any = {
token: AdminOrganizationController.generateToken(),
createdAt: new Date(),
slug: slugify(req.body.adminName).toLowerCase(),
// start the slugs array
slugs: [slugify(req.body.adminName)],
...req.body,
...{
proposedVersion: {
name: req.body.adminName
name: req.body.adminName,
contacts: {
facebook: '',
twitter: '',
instagram: '',
website: '',
address: '',
person: '',
email: req.body.email,
phone: ''
}
// descriptionShort: '',
// descriptionLong: '',
// contacts: [],
@ -49,14 +60,25 @@ export default class AdminOrganizationController {
}
Organization.create(body).then(data => {
AdminOrganizationController.sendEmailTokenUniversal(data)
res.json({ success: true, data })
res.json({ success: true, data, body })
}).catch(err => res.status(400).json({ success: false, errors: err.errors }))
}
static update(req: express.Request, res: express.Response) {
Organization.findById(req.params.id)
.then(data => {
// generate extra slugs
let extra: any = {}
if (req.body.name !== undefined) {
extra.slug = slugify(req.body.name)
let slug = slugify(req.body.name)
// only add this slug if the proposed slug is not found in the list of current slug
let currentSlugs: string[] = []
if (data !== null && Array.isArray(data.get('slugs'))) {
currentSlugs = data.get('slugs')
}
if (currentSlugs.filter(s => s === slug).length === 0) {
extra.slugs = currentSlugs.concat([slug])
}
}
Organization.updateOne({ _id: req.params.id }, {
...extra,
@ -65,6 +87,8 @@ export default class AdminOrganizationController {
})
.then(data => res.json({ success: true, data }))
.catch(err => res.status(400).json({ success: false, errors: err.errors !== undefined ? err.errors : err }))
})
.catch(err => res.status(400).json({ success: false, errors: err }))
}
static destroy(req: express.Request, res: express.Response) {

View file

@ -34,15 +34,19 @@ export default class DelegateController {
})
}
const next = (tag: any) => {
const next = (tags: any) => {
// only update proposedVersion
let proposedVersion: any = req.body
proposedVersion.tag = tag
proposedVersion.tags = tags
// sanitize long description
if (Utils.isStrUsable(proposedVersion.descriptionLong)) {
proposedVersion.descriptionLong = proposedVersion.descriptionLong.replace(/\n/g, '')
proposedVersion.descriptionLong = sanitizeHtml(
proposedVersion.descriptionLong,
{ allowedTags: ['h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'li', 'b', 'i', 'abbr', 'strong', 'em', 'hr', 'br', 'div', 'pre'] }
)
}
// validate contact.address
// validate all fields to not overflow
@ -73,6 +77,19 @@ export default class DelegateController {
MediaService.delete(organization.proposedVersion.cover.key, 'coverDeleted')
}
// delete unused thumbnail
// if the thumbnail is reset we must delete the old one only if it is unused in the published version
if (
Utils.isStrUsable(organization, 'proposedVersion.thumbnail.key') &&
!Utils.isStrUsable(proposedVersion, 'thumbnail.key') &&
!(
Utils.isStrUsable(organization, 'publishedVersion.thumbnail.key') &&
organization.publishedVersion.thumbnail.key === organization.proposedVersion.thumbnail.key
)
) {
MediaService.delete(organization.proposedVersion.thumbnail.key, 'thumbnailDeleted')
}
// format schedule, pricing
if (!Array.isArray(proposedVersion.schedule)) {
proposedVersion.schedule = []
@ -93,22 +110,27 @@ export default class DelegateController {
}).catch(err => res.status(400).json({ success: false, errors: err.errors !== undefined ? err.errors : err }))
}
if (req.body.tag !== undefined && req.body.tag !== null && req.body.tag.length > 2) {
if (Utils.isUsable(req.body, 'tags') && Array.isArray(req.body.tags)) {
// skip the tag part if the tag didn't changed
if (
organization.proposedVersion.tag === undefined ||
organization.proposedVersion.tag === null ||
req.body.tag !== organization.proposedVersion.tag._id
organization.proposedVersion.tags === undefined ||
organization.proposedVersion.tags === null ||
req.body.tags !== organization.proposedVersion.tags
) {
// if the tag is defined, search the tag id
Tag.findById(req.body.tag).then(tag => {
next(tag)
}).catch(err => {
console.log(err)
res.status(400).json({ success: false, errors: err, _note: 'The tag id provided is invalid' })
let promises: Promise<string>[] = []
req.body.tags.forEach((id: string) => {
promises.push(new Promise((resolve, reject) => {
Tag.findById(id).then((tag: any) => resolve(tag.get('_id'))).catch((err: any) => reject(err))
}))
})
Promise.all(promises).then(() => {
next(req.body.tags)
}).catch((err) => {
return res.status(400).json({ success: false, errors: err, _note: 'One of the tag id provided is invalid' })
})
} else {
next(organization.tag)
next(organization.tags)
}
} else {
next(null)

View file

@ -13,6 +13,12 @@ export default class ErrorController {
}
}
static handleServerError(err: any, req: express.Request, res: express.Response, next: any) {
console.error(err.stack)
//res.status(res.statusCode).json({ success: false, error: err.stack })
return ErrorController.handle(500, 'Erreur interne', 'Ouups je sais pas quoi dire', '💥', err.stack)(req, res)
}
static notFoundHandler() {
return ErrorController.handle(404, 'Page introuvable', 'Mais où peut donc se trouver cette page ?', '🔍 🕵️')
}

View file

@ -9,6 +9,8 @@ import { IconService, IconInterface } from '../IconService'
import IORedis from 'ioredis'
import Tag from '../models/Tag'
import ErrorController from './ErrorController'
import Utils from '../Utils'
import mongoose from 'mongoose'
export default class PublicController {
@ -19,45 +21,67 @@ export default class PublicController {
// data: await client.get('hello')
// })
Tag.find().then(tags => {
Organization.find().then(organizations => {
let isProposed = Utils.isStrUsable(req.query, 'only')
if (isProposed && !mongoose.Types.ObjectId.isValid(req.query.only)) {
return ErrorController.handleServerError({ stack: 'Invalid object id in only query param'}, req, res, [])
}
Organization.find(isProposed ? { _id: req.query.only } : {}).then(organizations => {
if (!isProposed) {
organizations = organizations.filter(o => o.get('publishedAt') !== undefined && o.get('publishedAt') !== null)
}
res.render('home.twig', {
isProposed,
tags,
tagsJSON: JSON.stringify(tags),
organizationsJSON: JSON.stringify(organizations
.filter(o => o.get('publishedAt') !== undefined && o.get('publishedAt') !== null)
.map(o => {
const version = o.get('publishedVersion')
const version = isProposed ? o.get('proposedVersion'): o.get('publishedVersion')
return {
_id: o._id,
name: version.name,
description: version.descriptionShort,
thumbnail: version.thumbnail.location,
tag: version.tag === null ? 'tag_not_found' : version.tag._id,
slug: o.get('slug')
tags: version.tags === null ? 'tags_not_found' : version.tags,
slugs: o.get('slugs'),
isProposed
}
})
)
})
}).catch(err => () => {
console.log(err)
return ErrorController.handleServerError(err, req, res, [])
})
}).catch(err => () => {
console.log(err)
return ErrorController.handleServerError(err, req, res, [])
})
}
static async organization(req: express.Request, res: express.Response) {
Organization.find({ slug: req.params.slug }).then(data => {
Organization.find({ slugs: { '$in': req.params.slug } }).then(data => {
if (data.length === 0) {
return ErrorController.notFoundHandler()(req, res)
} else {
const org = data[0]
let version = org.get('publishedVersion')
let lastPublished = org.get('publishedAt')
let isProposed = false
if (req.query.version === 'proposed') {
isProposed = true
lastPublished = org.get('updatedAt')
version = org.get('proposedVersion')
} else if (org.get('publishedAt') === undefined || org.get('publishedAt') === null) {
} else if (lastPublished === undefined || lastPublished === null) {
return ErrorController.notFoundHandler()(req, res)
}
if (version.contacts !== null && version.contacts !== undefined) {
if (typeof version.contacts.address === 'string') {
// if (lastPublished !== null) {
// lastPublished = lastPublished
// }
if (Utils.isUsable(version.contacts)) {
if (Utils.isStrUsable(version.contacts, 'address')) {
version.contacts.address = version.contacts.address.split('\n')
}
if (typeof version.contacts.phone === 'string') {
if (Utils.isStrUsable(version.contacts, 'phone')) {
let phone = version.contacts.phone
if (phone.indexOf('+33') === 0) {
phone = '0' + phone.substr(3)
@ -83,10 +107,16 @@ export default class PublicController {
}
res.render('organization.twig', {
layout: 'standalone',
data: version
data: version,
lastPublished: lastPublished.toLocaleDateString('fr-FR') + ' ' + lastPublished.toLocaleTimeString('fr-FR', { timezone: '+2' }).substr(0, 5),
isProposed,
id: org.get('_id')
})
}
}).catch(_ => res.status(404).render('not-found.twig'))
}).catch(err => () => {
console.log(err)
return ErrorController.handleServerError(err, req, res, [])
})
}
static async about(req: express.Request, res: express.Response) {

View file

@ -74,14 +74,14 @@ const OrganizationVersion = {
priceLabel: { type: String, required: true },
description: { type: String }
}],
tag: [Tag]
tags: [{ type: String }]
}
const Organization = new Schema({
adminName: { type: String, required: true },
email: { type: String, required: true },
token: { type: String, required: true },
slug: [{ type: String }], // aliases system
slugs: [{ type: String }], // aliases system
validationState: {
type: AllowedString,
required: true,

View file

@ -181,6 +181,8 @@
margin-bottom: 1em;
box-shadow: 0 0 8px 0px rgba(0,0,0,0.1);
transition: all 0.2s;
height: 12em;
overflow: hidden;
}
.card:hover {
@ -202,7 +204,7 @@
}
.card-content {
/* width: 100%; */
width: 100%;
padding: 1.5em;
display: flex;
flex-direction: column;
@ -238,12 +240,14 @@
color: #34495E;
margin: 0;
line-height: 1.6em;
position: relative;
}
.card-link {
color: #0029FF;
opacity: 0.75;
text-align: right;
position: absolute;
right: .5em;
bottom: 0;
margin-bottom: -.5em;
}
@media (max-width: 1350px) {

View file

@ -10,11 +10,6 @@ let navContent = document.getElementById('nav-content')
let mosaic = document.getElementById('mosaic')
let mosaicHeader = document.getElementById('mosaic-header')
organizations = organizations.map(org => {
org.tag = tags.filter(t => t._id === org.tag)[0]
return org
})
navEnabler.onclick = async () => {
if (!navOpened) {
// open the menu
@ -105,28 +100,36 @@ function renderCard(organization) {
titleContainer.appendChild(title)
let icon = createEl('card-icon')
if (Array.isArray(organization.tags) && organization.tags.length > 0) {
let tag = tags.filter(tag => organization.tags[0] === tag._id)[0]
icon.innerHTML = `<svg
aria-hidden="true"
focusable="false"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 ${organization.tag.icon.width} ${organization.tag.icon.height}">
<path fill="currentColor" d="${organization.tag.icon.path}"></path>
viewBox="0 0 ${tag.icon.width} ${tag.icon.height}">
<path fill="currentColor" d="${tag.icon.path}"></path>
</svg>`
}
titleContainer.appendChild(icon)
upperContent.appendChild(titleContainer)
let description = createEl('card-description')
description.textContent = organization.description
upperContent.appendChild(description)
let link = createEl('card-link')
let aTag = createEl(0, 'a')
aTag.href = "/association/" + organization.slug
//let link = createEl('card-link')
let aTag = createEl('card-link', 'a')
aTag.href = "/association/" + organization.slugs[organization.slugs.length - 1]
if (organization.isProposed) {
aTag.href += "?version=proposed"
}
aTag.textContent = "En savoir plus"
link.appendChild(aTag)
description.appendChild(aTag)
upperContent.appendChild(description)
//link.appendChild(aTag)
content.appendChild(upperContent)
content.appendChild(link)
//content.appendChild(link)
card.appendChild(content)
card.onclick = () => {
@ -152,7 +155,7 @@ function enableTag(node) {
if (!all) {
tagId = node.attributes['data-tag-id'].value
}
let data = organizations.filter(orga => orga.tag._id === tagId || all)
let data = organizations.filter(orga => orga.tags.filter(id => id === tagId).length > 0 || all)
let cards = renderMosaic(data)
if (currentCardContainer !== null) {
mosaic.removeChild(currentCardContainer)

View file

@ -23,6 +23,35 @@ a:hover {
color: #2980b9;
}
/* sticky footer */
html, body {
height: 100%;
}
body {
display: flex;
flex-direction: column;
}
.up-footer {
flex: 1 0 auto;
}
.sticky-footer {
flex-shrink: 0;
}
/* Porposed alert */
.proposed-alert {
position: fixed;
z-index: 9999999;
left: 1em;
bottom: 2em;
padding: 1em;
font-weight: bold;
border-radius: 3px;
text-transform: uppercase;
border: 1px solid #c0392b;
background-color: #e74c3c;
color: white;
}
@media (max-width: 1600px) {
.container {

View file

@ -538,13 +538,26 @@ section {
********************************************************************************/
.mentions {
display: flex;
justify-content: center;
width: 100%;
flex-direction: column;
text-align: center;
color: #d35400;
margin-top: 2em;
margin-bottom: .5em;
}
.mentions div {
margin-bottom: .5em;
}
.footer {
width: 100%;
display: grid;
grid-template-columns: 50% 25% 25%;
grid-template-rows: 1fr;
height: 1em;
margin-top: 2em;
}
.footer div:nth-child(1) {
@ -559,6 +572,9 @@ section {
background-color: rgb(253, 110, 11, .83);
}
/**
RESPONSIVE
**/
@media (max-width: 1200px) {
.schedule-category-days-container {

View file

@ -21,6 +21,7 @@ Github: https://github.com/lefuturiste
{% block head %}{% endblock %}
</head>
<body>
<div class="up-footer">
{% if layout is not defined %}
<div class="header">
<div class="container header-container">
@ -54,6 +55,17 @@ Github: https://github.com/lefuturiste
{% else %}
{% block content %}{% endblock %}
{% endif %}
{% if isProposed is defined and isProposed == true %}
<div class="proposed-alert">
! Version proposé !
{# <div class="proposed-alert-content">
</div> #}
</div>
{% endif %}
</div>
<div class="sticky-footer">
{% block footer %}{% endblock %}
</div>
{% block scripts %}{% endblock %}
</body>

View file

@ -6,7 +6,7 @@
{% block content %}
<div class="header">
<div class="container header-container">
<a href="/" class="return">
<a href="{% if isProposed is defined and isProposed == true %}/?only={{ id }}{% else %}/{% endif %}" class="return">
<div class="return-icon">
<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path fill="currentColor" d="M256 504C119 504 8 393 8 256S119 8 256 8s248 111 248 248-111 248-248 248zM142.1 273l135.5 135.5c9.4 9.4 24.6 9.4 33.9 0l17-17c9.4-9.4 9.4-24.6 0-33.9L226.9 256l101.6-101.6c9.4-9.4 9.4-24.6 0-33.9l-17-17c-9.4-9.4-24.6-9.4-33.9 0L142.1 239c-9.4 9.4-9.4 24.6 0 34z"></path>
@ -66,17 +66,21 @@
</div>
{% endif %}
{% if data.descriptionLong|length > 0 %}
{# {% if data.descriptionLong|length > 0 %} #}
<section>
<div class="section-title">
<h2>Présentation</h2>
<div class="section-divider"></div>
</div>
<div class="description">
{% if data.descriptionLong is not null and data.descriptionLong|length > 7 %}
{{ data.descriptionLong|raw }}
{% else %}
{{ data.descriptionShort }}
{% endif %}
</div>
</section>
{% endif %}
{# {% endif %} #}
{% if data.schedule|length > 0 %}
<section>
@ -274,6 +278,17 @@
</section>
{% endif %}
</div>
{% endblock %}
{% block footer %}
<div class="mentions">
<div class="advice">
Contenu rédigée par l'une des associations participant à associations.espacecondorcet.org
</div>
<div class="last-update">
Dernière mise à jour le {{ lastPublished }}
</div>
</div>
<div class="footer">
<div></div>
<div></div>

View file

@ -667,6 +667,13 @@
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.2.tgz#857a118d8634c84bba7ae14088e4508490cd5da5"
integrity sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q==
"@types/moment@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@types/moment/-/moment-2.13.0.tgz#604ebd189bc3bc34a1548689404e61a2a4aac896"
integrity sha1-YE69GJvDvDShVIaJQE5hoqSqyJY=
dependencies:
moment "*"
"@types/mongodb@*":
version "3.5.25"
resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.5.25.tgz#ab187db04d79f8e3f15af236327dc9139d9d4736"
@ -3471,6 +3478,11 @@ mkdirp@^0.5.1:
dependencies:
minimist "^1.2.5"
moment@*, moment@^2.27.0:
version "2.27.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d"
integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==
mongodb@3.5.9:
version "3.5.9"
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.5.9.tgz#799b72be8110b7e71a882bb7ce0d84d05429f772"