Commit cc76d943 authored by Rosanny Sihombing's avatar Rosanny Sihombing
Browse files

Merge branch 'MLAB-524' into 'testing'

Mlab 524

See merge request !41
parents 12d3fa2d a032f1f3
Pipeline #4547 failed with stage
in 13 seconds
pages-testing: pages-testing:
stage: deploy stage: deploy
script: script:
- cat $configfiledev > ./config/config.js
- npm install - npm install
- npm run clean
- npm run build
- rm -R built/views
- cp -R views built
- cat $configfiledev > ./built/config/config.js
- "pm2 delete --silent project || :" - "pm2 delete --silent project || :"
- pm2 start ./app.js --name=project - pm2 start ./built/app.js --name=project
- pm2 save - pm2 save
tags: tags:
- testing - testing
......
const express = require('express') import express from 'express'
const path = require('path') import path from 'path'
const passport = require('passport') //import passport from 'passport'
const morgan = require('morgan') import morgan from 'morgan'
const cookieParser = require('cookie-parser') import cookieParser from 'cookie-parser'
const bodyParser = require('body-parser') import bodyParser from 'body-parser'
const session = require('express-session') //import session from 'express-session'
const flash = require('express-flash') //import flash from 'express-flash'
const fileUpload = require('express-fileupload') //import fileUpload from 'express-fileupload'
const helmet = require('helmet') import helmet from 'helmet'
const compression = require('compression') import compression from 'compression'
var env = process.env.NODE_ENV || 'testing' var env = process.env.NODE_ENV || 'testing'
const config = require('./config/config')[env] const config = require('./config/config')[env]
const lang = 'DE';
var app = express() var app = express()
app.set('port', config.app.port) app.set('port', config.app.port)
app.set('views', __dirname + '/views') app.set('views', __dirname + '/views')
app.set('view engine', 'pug') app.set('view engine', 'pug')
...@@ -26,7 +26,7 @@ app.use(cookieParser()) ...@@ -26,7 +26,7 @@ app.use(cookieParser())
app.use(bodyParser.json()) app.use(bodyParser.json())
app.use(bodyParser.urlencoded({extended: false})) app.use(bodyParser.urlencoded({extended: false}))
app.use(express.static(path.join(__dirname, 'public'))) app.use(express.static(path.join(__dirname, 'public')))
app.use(session( /*app.use(session(
{ {
resave: true, resave: true,
saveUninitialized: true, saveUninitialized: true,
...@@ -35,20 +35,20 @@ app.use(session( ...@@ -35,20 +35,20 @@ app.use(session(
} }
)) ))
app.use(passport.initialize()) app.use(passport.initialize())
app.use(passport.session()) app.use(passport.session()) */
app.use(flash()) /*app.use(flash())
app.use((req, res, next) => { app.use((req, res, next) => {
res.locals.errors = req.flash("error") res.locals.errors = req.flash("error")
res.locals.successes = req.flash("success") res.locals.successes = req.flash("success")
next() next()
}) }) */
// enable files upload // enable files upload
app.use(fileUpload({ /*app.use(fileUpload({
createParentPath: true, createParentPath: true,
limits: { limits: {
fileSize: 1000000 // 1 MB max. file size fileSize: 1000000 // 1 MB max. file size
} }
})) })) */
// caching disabled for every route // caching disabled for every route
// NOTE: Works in Firefox and Opera. Does not work in Edge // NOTE: Works in Firefox and Opera. Does not work in Edge
app.use(function(req, res, next) { app.use(function(req, res, next) {
...@@ -56,17 +56,17 @@ app.use(function(req, res, next) { ...@@ -56,17 +56,17 @@ app.use(function(req, res, next) {
next() next()
}) })
require('./routes/project')(app, config, passport) require('./routes/project')(app, lang)
// Handle 404 // Handle 404
app.use(function (req, res, next) { app.use(function (req:any, res:any) {
res.status(404).render('./DE/404') res.status(404).render(lang+'/404')
}) })
// Handle 500 - any server error // Handle 500 - any server error
app.use(function (err, req, res, next) { app.use(function (err:any, req:any, res:any, next:any) {
console.error(err.stack) console.error(err.stack)
res.status(500).render('./DE/500', { res.status(500).render(lang+'/500', {
error: err error: err
}) })
}) })
......
...@@ -2,17 +2,7 @@ module.exports = { ...@@ -2,17 +2,7 @@ module.exports = {
development: { development: {
app: { app: {
name: 'Project Page Manager', name: 'Project Page Manager',
port: process.env.PORT || 8888, port: process.env.PORT || 8888
sessionSecret: 'thisisasecret-thisisasecret-thisisasecret'
},
passport: {
strategy: 'saml',
saml: {
path: process.env.SAML_PATH || '/saml/SSO',
entryPoint: process.env.SAML_ENTRY_POINT || 'saml entry URL',
issuer: 'saml issuer URL',
logoutUrl: 'saml Logout URL'
}
}, },
database: { database: {
user: 'usernamedb', // DB username user: 'usernamedb', // DB username
...@@ -22,15 +12,6 @@ module.exports = { ...@@ -22,15 +12,6 @@ module.exports = {
host_project: 'localhost', // local host_project: 'localhost', // local
dbProject: 'projectdb' // Project DB dbProject: 'projectdb' // Project DB
}, },
mailer: {
host: 'mailhost', // hostname
secureConnection: false, // TLS requires secureConnection to be false
port: 587, // port for secure SMTP
authUser: 'usernamemail',
authPass: 'passwordmail',
tlsCiphers: 'SSLv3',
from: 'email_from',
},
gitlab: { gitlab: {
token_readWriteProjects: 'putyourtokenhere' token_readWriteProjects: 'putyourtokenhere'
} }
...@@ -38,17 +19,7 @@ module.exports = { ...@@ -38,17 +19,7 @@ module.exports = {
testing: { testing: {
app: { app: {
name: 'Project Page Manager', name: 'Project Page Manager',
port: process.env.PORT || 8888, port: process.env.PORT || 8888
sessionSecret: 'thisisasecret-thisisasecret-thisisasecret'
},
passport: {
strategy: 'saml',
saml: {
path: process.env.SAML_PATH || '/saml/SSO',
entryPoint: process.env.SAML_ENTRY_POINT || 'saml entry URL',
issuer: 'saml issuer URL',
logoutUrl: 'saml Logout URL'
}
}, },
database: { database: {
user: 'usernamedb', // DB username user: 'usernamedb', // DB username
...@@ -58,15 +29,6 @@ module.exports = { ...@@ -58,15 +29,6 @@ module.exports = {
host_project: 'localhost', // local host_project: 'localhost', // local
dbProject: 'projectdb' // Project DB dbProject: 'projectdb' // Project DB
}, },
mailer: {
host: 'mailhost', // hostname
secureConnection: false, // TLS requires secureConnection to be false
port: 587, // port for secure SMTP
authUser: 'usernamemail',
authPass: 'passwordmail',
tlsCiphers: 'SSLv3',
from: 'email_from',
},
gitlab: { gitlab: {
token_readWriteProjects: 'putyourtokenhere' token_readWriteProjects: 'putyourtokenhere'
} }
......
const mysql = require('mysql') import mysql from 'mysql2'
var env = process.env.NODE_ENV || 'testing'; var env = process.env.NODE_ENV || 'development';
const config = require('./config')[env] const config = require('./config')[env]
// ==== USER ACOOUNT DB CONNECTION ==== // ==== USER ACOOUNT DB CONNECTION ====
...@@ -18,13 +18,6 @@ userConnection.connect(function(err) { ...@@ -18,13 +18,6 @@ userConnection.connect(function(err) {
}) })
userConnection.query('USE '+config.database.dbUser) userConnection.query('USE '+config.database.dbUser)
// user db connection test
userConnection.query('SELECT 1 + 5 AS solution', function (err, rows, fields) {
if (err) throw err
console.log('Solution = ', rows[0].solution)
})
//userConnection.end()
// ==== PROJECT DB CONNECTION ==== // ==== PROJECT DB CONNECTION ====
var projectConnection = mysql.createConnection({ var projectConnection = mysql.createConnection({
host: config.database.host_project, host: config.database.host_project,
...@@ -39,16 +32,9 @@ projectConnection.connect(function(err) { ...@@ -39,16 +32,9 @@ projectConnection.connect(function(err) {
}) })
projectConnection.query('USE '+config.database.dbProject) projectConnection.query('USE '+config.database.dbProject)
// projectdb connection test
projectConnection.query('SELECT 10 + 5 AS project', function (err, rows, fields) {
if (err) throw err
console.log('Project = ', rows[0].project)
})
//projectConnection.end()
var connection = { var connection = {
user: userConnection, user: userConnection,
project: projectConnection project: projectConnection
} }
module.exports = connection export = connection
\ No newline at end of file \ No newline at end of file
const nodemailer = require('nodemailer')
var env = process.env.NODE_ENV || 'testing';
const config = require('./config')[env]
var smtpTransport = nodemailer.createTransport({
host: config.mailer.host,
secureConnection: config.mailer.secureConnection,
port: config.mailer.port,
auth: {
user: config.mailer.authUser,
pass: config.mailer.authPass
},
tls: {
ciphers: config.mailer.tlsCiphers
}
});
var mailOptions = {
to: "",
from: config.mailer.from,
subject: "",
text: ""
};
var mailer = {
transport: smtpTransport,
options: mailOptions
}
module.exports = mailer
\ No newline at end of file
const axios = require('axios') import axios from 'axios'
var gitlab = { var gitlab = {
getProjects: async function(perPage, idAfter) { getProjects: async function(perPage:number, idAfter:number) {
try { try {
let projects = await axios({ let projects = await axios({
method: 'get', method: 'get',
...@@ -30,7 +30,7 @@ var gitlab = { ...@@ -30,7 +30,7 @@ var gitlab = {
data: err} data: err}
} }
}, },
getLatestPipelineStatus: async function(projectId) { getLatestPipelineStatus: async function(projectId:number) {
return axios({ return axios({
method: 'get', method: 'get',
url: 'https://transfer.hft-stuttgart.de/gitlab/api/v4/projects/'+projectId+'/pipelines' url: 'https://transfer.hft-stuttgart.de/gitlab/api/v4/projects/'+projectId+'/pipelines'
...@@ -40,4 +40,4 @@ var gitlab = { ...@@ -40,4 +40,4 @@ var gitlab = {
} }
} }
module.exports = gitlab export = gitlab
\ No newline at end of file \ No newline at end of file
var helpers = { var helpers = {
stringToArray: function (input){ stringToArray: function (input:string){
if(input != null){ if(input != null){
return input.split(','); return input.split(',');
}else{ }else{
...@@ -8,4 +8,4 @@ var helpers = { ...@@ -8,4 +8,4 @@ var helpers = {
} }
}; };
module.exports = helpers; export = helpers;
\ No newline at end of file \ No newline at end of file
const dbconn = require('../config/dbconn');
var methods = {
// test method
currentDate: function() {
console.log('Current Date is: ' + new Date().toISOString().slice(0, 10));
},
// ===================== user db =====================
getUserIdByEmail: function(email, callback) {
var userId
dbconn.user.query('SELECT id FROM user WHERE email = "' +email+'"', function (err, rows, fields) {
if (err) {
throw err;
}
else {
if ( rows.length > 0) {
userId = rows[0].id;
}
}
callback(userId, err);
});
},
/*
getUserProjectRole: function(userId, callback) {
dbconn.user.query('SELECT project_id, role_id FROM user_project_role WHERE user_id = "' +userId+'"', function (err, rows, fields) {
if (err) throw err;
callback(rows, err);
});
},
*/
addUserProjectRole: function(data, callback) {
dbconn.user.query('INSERT INTO user_project_role SET ?', data, function (err, results, fields){
if (err) throw err;
callback(err);
})
},
// ======================= project db =======================
getAllProjects: function(callback) {
dbconn.project.query('CALL getAllprojects', function (err, rows, fields){
if (err) throw err;
callback(rows[0], err);
})
},
getAllMailinglists: function(callback) {
dbconn.project.query('CALL getAllLists', function (err, rows, fields){
if (err) throw err;
callback(rows[0], err);
})
},
getProjectOverviewById: function(projectId, callback) {
dbconn.project.query('CALL GetProjectInformationByProjectID(' + projectId+ ')', function (err, rows, fields){
if (err) throw err;
callback(rows[0], err);
})
},
getProjectImagesById: function(projectId, callback) {
dbconn.project.query('CALL getImagesByProjectID(' + projectId+ ')', function (err, rows, fields){
if (err) throw err;
callback(rows[0], err);
})
},
addProjectOverview: function(data, callback) {
dbconn.project.query('INSERT INTO project_overview SET ?', data, function (err, results, fields){
if (err) {
console.error(err);
}
callback(results, err);
})
}
};
module.exports = methods;
\ No newline at end of file
const dbconn = require('../config/dbconn');
var methods = {
getAllMailinglists: async function() {
try {
let rows:any = await dbconn.project.promise().query('CALL getAllLists')
if (rows[0][0]) {
return rows[0][0]
} else { return null }
} catch (err) {
console.error(err)
}
return null
},
getProjectOverviewById: async function(projectId:number) {
try {
let rows:any = await dbconn.project.promise().query('CALL GetProjectInformationByProjectID(' + projectId+ ')')
if (rows[0][0]) {
return rows[0][0]
} else { return null }
} catch (err) {
console.error(err)
}
return null
},
getProjectImagesById: async function(projectId:number) {
try {
let rows:any = await dbconn.project.promise().query('CALL getImagesByProjectID(' + projectId+ ')')
if (rows[0][0]) {
return rows[0][0]
} else { return null }
} catch (err) {
console.error(err)
}
return null
}
};
export = methods;
\ No newline at end of file
This diff is collapsed.
...@@ -14,7 +14,9 @@ ...@@ -14,7 +14,9 @@
"url": "https://transfer.hft-stuttgart.de/gitlab/m4lab_tv1/project-page.git" "url": "https://transfer.hft-stuttgart.de/gitlab/m4lab_tv1/project-page.git"
}, },
"scripts": { "scripts": {
"start": "nodemon app.js", "start": "nodemon app.ts",
"build": "tsc --build",
"clean": "tsc --build --clean",
"test": "" "test": ""
}, },
"dependencies": { "dependencies": {
...@@ -23,17 +25,10 @@ ...@@ -23,17 +25,10 @@
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"cookie-parser": "1.4.3", "cookie-parser": "1.4.3",
"errorhandler": "1.4.3",
"express": "^4.17.1", "express": "^4.17.1",
"express-fileupload": "^1.1.7-alpha.2", "helmet": "^4.6.0",
"express-flash": "0.0.2",
"express-session": "^1.17.1",
"fs": "0.0.1-security",
"helmet": "^3.23.3",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"mysql": "^2.18.1", "mysql2": "^2.2.5",
"passport": "0.3.2",
"passport-saml": "^2.0.6",
"pug": "^3.0.2" "pug": "^3.0.2"
}, },
"engines": { "engines": {
...@@ -41,6 +36,17 @@ ...@@ -41,6 +36,17 @@
}, },
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"nodemon": "^2.0.9" "@types/async": "^3.2.6",
"@types/compression": "^1.7.0",
"@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.12",
"@types/express-fileupload": "^1.1.6",
"@types/express-flash": "^0.0.2",
"@types/express-session": "^1.17.3",
"@types/morgan": "^1.9.2",
"@types/passport": "^1.0.6",
"nodemon": "^2.0.9",
"ts-node": "^10.0.0",
"typescript": "^4.3.5"
} }
} }
//const SamlStrategy = require('passport-saml').Strategy //const SamlStrategy = require('passport-saml').Strategy
const methods = require('../functions/methods') import methods from '../functions/methods'
const gitlab = require('../functions/gitlab') import gitlab from '../functions/gitlab'
// pwd encryption import helpers from '../functions/helpers'
//const bcrypt = require('bcryptjs');
//const saltRounds = 10;
//const salt = 64; // salt length
// forgot pwd
const async = require('async')
//const crypto = require('crypto')
//const mailer = require('./mailer')
const helpers = require('../functions/helpers')
const pictSizeLimit = 1000000 // 1 MB
module.exports = function (app) { module.exports = function (app:any, lang:string) {
// ======== APP ROUTES - PROJECT ==================== // ======== APP ROUTES - PROJECT ====================
var lang = 'DE'
app.get('/mailinglists', function (req, res) { app.get('/', function (req:any, res:any) {
async.waterfall([ res.render(lang+'/project/project-simplified')
function(done) {
methods.getAllMailinglists(function(mailinglistOverview, err) {
if (!err) {
done(err, mailinglistOverview)
}
}) })
},
// create JSON object of mailinglists for front-end app.get('/mailinglists', async function (req:any, res:any) {
function(mailinglistOverview, done) { let mailList = await methods.getAllMailinglists()
var allMailingLists = [] // JSON object if (mailList) {
for (let i = 0; i < mailinglistOverview.length; i++) { let allMailingLists = [] // JSON object
for (let i = 0; i < mailList.length; i++) {
// add data to JSON object // add data to JSON object
allMailingLists.push({ allMailingLists.push({
id: mailinglistOverview[i].id, id: mailList[i].id,
name: mailinglistOverview[i].name, name: mailList[i].name,
src: mailinglistOverview[i].src, src: mailList[i].src,
projectstatus: mailinglistOverview[i].projectstatus, projectstatus: mailList[i].projectstatus,
project_title: mailinglistOverview[i].project_title, project_title: mailList[i].project_title,
keywords: mailinglistOverview[i].keywords keywords: mailList[i].keywords
}); });
} }
res.render(lang+'/project/mailinglists', { res.render(lang+'/project/mailinglists', {
isUserAuthenticated: req.isAuthenticated(), //isUserAuthenticated: req.isAuthenticated(),
user: req.user, //user: req.user,
mailinglists: allMailingLists mailinglists: allMailingLists
}); });
} } else {
]) res.render(lang+'/project/mailinglists', {
}) //isUserAuthenticated: req.isAuthenticated(),
//user: req.user,
app.get('/', function (req, res) { mailinglists: null
res.render(lang+'/project/project-simplified', {
isUserAuthenticated: req.isAuthenticated(),
user: req.user
});
})
app.get('/addprojectoverview', function (req, res) {
if (req.isAuthenticated()) {
res.render(lang+'/project/addProjectOverview')
}
else {
res.redirect('/login')
}
})
app.post('/addprojectoverview', function (req, res) {
if (req.isAuthenticated()) {
var wiki = 0
if (req.body.wiki)
wiki = 1
var projectLogo = req.files.logo
var projectPicture = req.files.src
var projectLogoPath, projectPicturePath
if (projectLogo) {
// raise error if size limit is exceeded
if (projectLogo.size === pictSizeLimit) {
req.flash('error', 'Projektlogo exceeds 1 MB');
res.redirect('/addprojectoverview');
}
else {
// TEST PATH FOR DEVELOPMENT (LOCALHOST)
projectLogoPath = './folder-in-server-to-save-projektlogo/'+req.body.pname+'/'+projectLogo.name
// PATH FOR TEST/LIVE SERVER
// var projectLogoPath = to-be-defined
}
}
if (projectPicture) {
// raise error if size limit is exceeded
if (projectPicture.size === pictSizeLimit) {
req.flash('error', 'Projektbild exceeds 1 MB');
res.redirect('/addprojectoverview');
}
else {
// TEST PATH FOR DEVELOPMENT (LOCALHOST)
projectPicturePath = './folder-in-server-to-save-projektbild/'+req.body.pname+'/'+projectPicture.name
// PATH FOR TEST/LIVE SERVER
// var projectPicturePath = to-be-defined
}
}
var projectTerm = req.body.termForm + " - " + req.body.termTo
var projectOverviewData = {
pname: req.body.pname,
title: req.body.title,
onelinesummary: req.body.summary,
category: req.body.category,
logo: projectLogoPath,
gitlab: req.body.gitlabURL,
wiki: wiki,
overview: req.body.overview,
question: req.body.question,
approach: req.body.approach,
result: req.body.result,
keywords: req.body.keywords,
announcement: req.body.announcement,
term: projectTerm,
further_details: req.body.furtherDetails,
website: req.body.website,
src: projectPicturePath,
caption: req.body.caption,
contact_lastname: req.body.contactName,
contact_email: req.body.contactEmail,
leader_lastname: req.body.leaderName,
leader_email: req.body.leaderEmail
}
// save pictures
if (projectLogo) {
projectLogo.mv(projectLogoPath, function(err) {
if (err) {
console.error(err)
res.status(500).render(lang+'/500', {
error: err
})
}
});
}
if (projectPicture) {
projectPicture.mv(projectPicturePath, function(err) {
if (err) {
console.error(err)
res.status(500).render(lang+'/500', {
error: err
})
}
});
}
/* RS: Temporary solution while Project DB is still in early phase.
When User DB and Project DB are integrated and quite stabil, this operation should be done in 1 transaction.
*/
var userId // todo: make this global variable?
async.waterfall([
// get userId by email from userdb
function(done) {
methods.getUserIdByEmail(req.user.email, function(id, err) {
if (!err) {
userId = id
done(err)
}
})
},
// add project overview
function(done) {
methods.addProjectOverview(projectOverviewData, function(data, err){
if (err) {
res.status(500).render(lang+'/500', {
error: err
})
}
else {
done(err, data.insertId)
}
})
},
// assign the created overview to logged-in user
function(projectOverviewId, done) {
var userProjectRoleData = {
project_id: projectOverviewId,
user_id: userId,
role_id: 3 // OVERVIEW_CREATOR
}
methods.addUserProjectRole(userProjectRoleData, function(userProjects, err) {
if (err) {
//req.flash('error', "Failed")
req.flash('error', "Fehlgeschlagen")
res.redirect('/addProjectOverview');
}
else {
req.flash('success', 'Your project has been created.')
res.redirect('/project');
}
}) })
} }
])
}
}) })
app.post('/updateprojectoverview', function (req, res) { app.get('/projectoverview', async function(req:any, res:any){
// only their own project let projectOverview = await methods.getProjectOverviewById(req.query.projectID)
}) if (projectOverview.length > 0) {
let partnerWebsites = helpers.stringToArray(projectOverview[0].partner_website)
app.get('/projectoverview', function(req, res){ let partnerNames = helpers.stringToArray(projectOverview[0].partner_name)
async.waterfall([ let awardSites = helpers.stringToArray(projectOverview[0].award_website)
function(done) { let awardNames = helpers.stringToArray(projectOverview[0].award_name)
methods.getProjectOverviewById(req.query.projectID, function(projectOverview, err) { let sponsorWebsites = helpers.stringToArray(projectOverview[0].sponsor_website)
if (!err) { let sponsorImgs = helpers.stringToArray(projectOverview[0].sponsor_img)
done(err, projectOverview) let sponsorNames = helpers.stringToArray(projectOverview[0].sponsor_name)
}
})
},
function(projectOverview,done){
methods.getProjectImagesById(req.query.projectID, function(projectImages, err) {
if (!err) {
done(err, projectImages, projectOverview)
}
})
},
// render projectOverview page
function(projectImages, projectOverview, done) {
console.log(projectImages)
partnerWebsites = helpers.stringToArray(projectOverview[0].partner_website)
partnerNames = helpers.stringToArray(projectOverview[0].partner_name)
awardSites = helpers.stringToArray(projectOverview[0].award_website)
awardNames = helpers.stringToArray(projectOverview[0].award_name)
sponsorWebsites = helpers.stringToArray(projectOverview[0].sponsor_website)
sponsorImgs = helpers.stringToArray(projectOverview[0].sponsor_img)
sponsorNames = helpers.stringToArray(projectOverview[0].sponsor_name)
let projectImages = await methods.getProjectImagesById(req.query.projectID)
res.render(lang+'/project/projectOverview', { res.render(lang+'/project/projectOverview', {
isUserAuthenticated: req.isAuthenticated(), //isUserAuthenticated: req.isAuthenticated(),
user: req.user, //user: req.user,
projectOV: projectOverview, projectOV: projectOverview,
projectImgs: projectImages, projectImgs: projectImages,
partnerWS: partnerWebsites, partnerWS: partnerWebsites,
...@@ -246,12 +66,13 @@ module.exports = function (app) { ...@@ -246,12 +66,13 @@ module.exports = function (app) {
sponsorIMG: sponsorImgs, sponsorIMG: sponsorImgs,
sponsorN: sponsorNames sponsorN: sponsorNames
}); });
} else {
res.redirect('/')
} }
])
}) })
// Projektdaten // Projektdaten
app.get('/projektdaten', async function(req, res){ app.get('/projektdaten', async function(req:any, res:any){
let projectArr = [] let projectArr = []
let isProject = true let isProject = true
let firstId = 0 let firstId = 0
...@@ -293,7 +114,7 @@ module.exports = function (app) { ...@@ -293,7 +114,7 @@ module.exports = function (app) {
}) })
// Projektinformationen // Projektinformationen
app.get('/projektinformationen', async function(req, res){ app.get('/projektinformationen', async function(req:any, res:any){
let pagesArr = [] let pagesArr = []
let isProject = true let isProject = true
let firstId = 0 let firstId = 0
......
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"rootDir": "./",
"outDir": "./built",
"esModuleInterop": true,
"strict": true,
"allowJs": true
}
}
\ No newline at end of file
doctype html
html(lang="de")
head
title= "Add Project Overview"
meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no")
link(rel="stylesheet", type="text/css", href="https://transfer.hft-stuttgart.de/css/bootstrap.min.css")
link(rel="stylesheet", type="text/css", href="https://transfer.hft-stuttgart.de/css/m4lab.css")
link(rel="stylesheet", href="https://use.fontawesome.com/releases/v5.8.2/css/all.css", integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay", crossorigin="anonymous")
// jQuery UI - Datepicker
link(rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css")
style.
.warning {
color: red;
font-size: 11px;
}
body
div(class="container-fluid")
div(class="row")
div(class="col-md-6 offset-md-2")
h4(class="mb-3 font-weight-bold") Neues Projekt
div(class="col-md-6 offset-md-3")
if errors
for error, i in errors
div.alert.alert-danger.alert-dismissible.fade.show #{ error }
a(class="close", href="#", data-dismiss="alert", aria-label="close") &times;
form(method="POST" encType="multipart/form-data")
div(class='form-row')
div(class='form-group col-md-12')
input#inputPname(name="title" class="form-control" type="text" placeholder="Projekttitel*" required)
div(class="form-group col-md-12")
input#inputTitle(name="pname" class="form-control" type="text" placeholder="Akronym*" required)
div(class="form-group col-md-12")
input#inputSummary(name="summary" class="form-control" type="text" placeholder="Kurzbeschreibung")
div(class='form-group col-md-12')
select#inputCategory(name="category", class="form-control")
option(value="") - Projektkategorie -
option(value="Experten-Gruppe") Experten-Gruppe
option(value="Student-Projekt") Student-Projekt
option(value="Lehr Projekt") Lehr Projekt
option(value="Transfer-projekt") Transfer-projekt
div(class="form-group col-md-12")
div(class='form-group row')
label(for="projectLogo" class="col-sm-3 col-form-label") Projektlogo (max. 1 MB)
div(class="col-md-9")
input#inputLogo(name="logo" class="form-control" type="file")
div(class="form-group col-md-12")
div(class="input-group mb-3")
input#inputGitlabURL(name="gitlabURL" type="text" class="form-control" placeholder="M4_LAB GitLab Project URL, z.B. https://transfer.hft-stuttgart.de/gitlab/username/projectname")
div(class="input-group-prepend")
div(class="input-group-text")
input#inputWiki(name="wiki" type="checkbox")
| &nbsp; Wiki
h5(class="mb-3 font-weight-bold") Inhalte
div(class='form-row')
div(class='form-group col-md-12')
textarea#inputOverview(name="overview" class="form-control" type="text" rows="5" placeholder="Projektüberblick")
div(class="form-group col-md-12")
textarea#inputQuestion(name="question" class="form-control" type="text" rows="5" placeholder="Fragestellung")
div(class='form-group col-md-12')
textarea#inputApproach(name="approach" class="form-control" type="text" rows="5" placeholder="Vorgehensweise")
div(class="form-group col-md-12")
textarea#inputResult(name="result" class="form-control" type="text" rows="5" placeholder="Ergebnis und Nutzung")
div(class="form-group col-md-12")
input#inputKeywords(name="keywords" class="form-control" type="text" placeholder="keywords")
h5(class="mb-3 font-weight-bold") Projektinformationen
div(class='form-row')
div(class='form-group col-md-12')
input#inputAnnouncement(name="announcement" class="form-control" type="text" rows="5" placeholder="Ausschreibung")
div(class="form-group col-md-12")
div(class='form-group row')
label(for="projectLogo" class="col-sm-2 col-form-label") Laufzeit
div(class="col-md-5")
input#inputTermFrom(name="termForm" class="form-control" type="text" placeholder="von (dd.mm.yyyy)")
div(class="col-md-5")
input#inputTermTo(name="termTo" class="form-control" type="text" placeholder="bis (dd.mm.yyyy)")
div(class='form-group col-md-12')
textarea#inputFurtherDetails(name="furtherDetails" class="form-control" type="text" rows="5" placeholder="Weitere Informationen (bspw. Links zu Berichten)")
div(class="form-group col-md-12")
input#inputWebsite(name="website" class="form-control" type="text" placeholder="Projekt-Website")
h5(class="mb-3 font-weight-bold") Bilder
div(class='form-row')
div(class="form-group col-md-12")
div(class='form-group row')
label(for="projectPicture" class="col-sm-3 col-form-label") Projektbild (max. 1 MB)
div(class="col-md-9")
input#inputSrc(name="src" class="form-control" type="file")
div(class="form-group col-md-12")
input#inputCaption(name="caption" class="form-control" type="text" placeholder="Bildunterschrift/Bildquelle")
h5(class="mb-3 font-weight-bold") Kontakt
div(class='form-row')
div(class="form-group col-md-2")
<p class="font-weight-normal">Ansprechperson</p>
div(class="form-group col-md-5")
input#inputContactName(name="contactName" class="form-control" type="text" placeholder="Anrede, Titel, Vorname, Nachname")
div(class="form-group col-md-5")
input#inputContactEmail(name="contactEmail" class="form-control" type="email" placeholder="E-Mail-Adresse")
div(class="form-group col-md-2")
<p class="font-weight-normal">Projektleitung</p>
div(class="form-group col-md-5")
input#inputLeaderName(name="leaderName" class="form-control" type="text" placeholder="Anrede, Titel, Vorname, Nachname")
div(class="form-group col-md-5")
input#inputLeaderEmail(name="leaderEmail" class="form-control" type="email" placeholder="E-Mail-Adresse")
p <em><small>* Pflichtfeld</small></em>
input#submitBtn(type="submit", class="btn btn-outline-dark btn-block", value="Projekt Anlegen")
// jQuery
script(src="https://code.jquery.com/jquery-3.3.1.min.js")
script(src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js", integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1", crossorigin="anonymous")
// jQuery UI - Datepicker
script(src="https://code.jquery.com/ui/1.12.1/jquery-ui.js")
script(src="/js/jquery-ui/i18n/datepicker-de.js")
//script(src="i18n/datepicker-de.js")
// Bootstrap
script(src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous")
// Header
script(src="/js/headfoot.js")
script.
$( function() {
$.datepicker.setDefaults( $.datepicker.regional["de"] );
$("#inputTermFrom").datepicker();
$("#inputTermTo").datepicker();
});
\ No newline at end of file
...@@ -20,6 +20,9 @@ html(lang="de") ...@@ -20,6 +20,9 @@ html(lang="de")
div(class="col-md-12 margin_bottom_30") div(class="col-md-12 margin_bottom_30")
h2(class="text-center color_708090") <strong>Aktive Mailinglisten</strong> h2(class="text-center color_708090") <strong>Aktive Mailinglisten</strong>
div(class="table-responsive table-borderless") div(class="table-responsive table-borderless")
if !mailinglists
p There is no active mailing list at the moment
else
table(class="table table-striped table-bordered table-hover") table(class="table table-striped table-bordered table-hover")
thead() thead()
tr() tr()
......
doctype html
html(lang="de")
head
title= "Manage Project Overview"
meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no")
link(rel="stylesheet", type="text/css", href="https://transfer.hft-stuttgart.de/css/bootstrap.min.css")
link(rel="stylesheet", type="text/css", href="https://transfer.hft-stuttgart.de/css/m4lab.css")
link(rel="stylesheet", href="https://use.fontawesome.com/releases/v5.8.2/css/all.css", integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay", crossorigin="anonymous")
// jQuery UI - Datepicker
link(rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css")
style.
.warning {
color: red;
font-size: 11px;
}
body
div(class="container-fluid")
div(class="row")
div(class="col-md-6 offset-md-2")
h4(class="mb-3 font-weight-bold") Manage Project
div(class="col-md-6 offset-md-3")
if errors
for error, i in errors
div.alert.alert-danger.alert-dismissible.fade.show #{ error }
a(class="close", href="#", data-dismiss="alert", aria-label="close") &times;
form(method="POST" encType="multipart/form-data")
div(class='form-row')
div(class='form-group col-md-12')
input#inputPname(name="title" class="form-control" type="text" placeholder="Projekttitel*" value=overview.title required)
div(class="form-group col-md-12")
input#inputTitle(name="pname" class="form-control" type="text" placeholder="Akronym*" value=overview.pname required)
div(class="form-group col-md-12")
input#inputSummary(name="summary" class="form-control" type="text" placeholder="Kurzbeschreibung")
div(class='form-group col-md-12')
select#inputCategory(name="category", class="form-control")
option(value="") - Projektkategorie -
option(value="Experten-Gruppe") Experten-Gruppe
option(value="Student-Projekt") Student-Projekt
option(value="Lehr Projekt") Lehr Projekt
option(value="Transfer-projekt") Transfer-projekt
div(class="form-group col-md-12")
div(class='form-group row')
label(for="projectLogo" class="col-sm-3 col-form-label") Projektlogo (max. 1 MB)
div(class="col-md-9")
input#inputLogo(name="logo" class="form-control" type="file")
div(class="form-group col-md-12")
div(class="input-group mb-3")
input#inputGitlabURL(name="gitlabURL" type="text" class="form-control" placeholder="M4_LAB GitLab Project URL, z.B. https://transfer.hft-stuttgart.de/gitlab/username/projectname")
div(class="input-group-prepend")
div(class="input-group-text")
input#inputWiki(name="wiki" type="checkbox")
| &nbsp; Wiki
h5(class="mb-3 font-weight-bold") Inhalte
div(class='form-row')
div(class='form-group col-md-12')
textarea#inputOverview(name="overview" class="form-control" type="text" rows="5" placeholder="Projektüberblick")
div(class="form-group col-md-12")
textarea#inputQuestion(name="question" class="form-control" type="text" rows="5" placeholder="Fragestellung")
div(class='form-group col-md-12')
textarea#inputApproach(name="approach" class="form-control" type="text" rows="5" placeholder="Vorgehensweise")
div(class="form-group col-md-12")
textarea#inputResult(name="result" class="form-control" type="text" rows="5" placeholder="Ergebnis und Nutzung")
div(class="form-group col-md-12")
input#inputKeywords(name="keywords" class="form-control" type="text" placeholder="keywords" value=overview.keywords)
h5(class="mb-3 font-weight-bold") Projektinformationen
div(class='form-row')
div(class='form-group col-md-12')
input#inputAnnouncement(name="announcement" class="form-control" type="text" rows="5" placeholder="Ausschreibung")
div(class="form-group col-md-12")
div(class='form-group row')
label(for="projectLogo" class="col-sm-2 col-form-label") Laufzeit
div(class="col-md-5")
input#inputTermFrom(name="termForm" class="form-control" type="text" placeholder="von (dd.mm.yyyy)")
div(class="col-md-5")
input#inputTermTo(name="termTo" class="form-control" type="text" placeholder="bis (dd.mm.yyyy)")
div(class='form-group col-md-12')
textarea#inputFurtherDetails(name="furtherDetails" class="form-control" type="text" rows="5" placeholder="Weitere Informationen (bspw. Links zu Berichten)")
div(class="form-group col-md-12")
input#inputWebsite(name="website" class="form-control" type="text" placeholder="Projekt-Website")
h5(class="mb-3 font-weight-bold") Bilder
div(class='form-row')
div(class="form-group col-md-12")
div(class='form-group row')
label(for="projectPicture" class="col-sm-3 col-form-label") Projektbild (max. 1 MB)
div(class="col-md-9")
input#inputSrc(name="src" class="form-control" type="file")
div(class="form-group col-md-12")
input#inputCaption(name="caption" class="form-control" type="text" placeholder="Bildunterschrift/Bildquelle")
h5(class="mb-3 font-weight-bold") Kontakt
div(class='form-row')
div(class="form-group col-md-2")
<p class="font-weight-normal">Ansprechperson</p>
div(class="form-group col-md-5")
input#inputContactName(name="contactName" class="form-control" type="text" placeholder="Anrede, Titel, Vorname, Nachname")
div(class="form-group col-md-5")
input#inputContactEmail(name="contactEmail" class="form-control" type="email" placeholder="E-Mail-Adresse" value=overview.contact_email)
div(class="form-group col-md-2")
<p class="font-weight-normal">Projektleitung</p>
div(class="form-group col-md-5")
input#inputLeaderName(name="leaderName" class="form-control" type="text" placeholder="Anrede, Titel, Vorname, Nachname")
div(class="form-group col-md-5")
input#inputLeaderEmail(name="leaderEmail" class="form-control" type="email" placeholder="E-Mail-Adresse" value=overview.leader_email)
p <em><small>* Pflichtfeld</small></em>
input#submitBtn(type="submit", class="btn btn-outline-dark btn-block", value="Submit")
// jQuery
script(src="https://code.jquery.com/jquery-3.3.1.min.js")
script(src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js", integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1", crossorigin="anonymous")
// jQuery UI - Datepicker
script(src="https://code.jquery.com/ui/1.12.1/jquery-ui.js")
script(src="/js/jquery-ui/i18n/datepicker-de.js")
//script(src="i18n/datepicker-de.js")
// Bootstrap
script(src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous")
// Header
script(src="/js/headfootLogout.js")
script.
$( function() {
$.datepicker.setDefaults( $.datepicker.regional["de"] );
$("#inputTermFrom").datepicker();
$("#inputTermTo").datepicker();
});
\ No newline at end of file
...@@ -8,43 +8,12 @@ html(lang="de") ...@@ -8,43 +8,12 @@ html(lang="de")
link(rel="stylesheet", type="text/css", href="/css/bootstrap.min.css") link(rel="stylesheet", type="text/css", href="/css/bootstrap.min.css")
link(rel="stylesheet", type="text/css", href="/css/m4lab.css") link(rel="stylesheet", type="text/css", href="/css/m4lab.css")
link(rel="stylesheet", href="https://use.fontawesome.com/releases/v5.8.2/css/all.css", integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay", crossorigin="anonymous") link(rel="stylesheet", href="https://use.fontawesome.com/releases/v5.8.2/css/all.css", integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay", crossorigin="anonymous")
style.
.help .card-title > a:before {
float: right !important;
content: "-";
padding-right: 5px;
}
.help .card-title > a.collapsed:before {
float: right !important;
content: "+";
}
.help h3 > a {
color: #708090;
text-decoration: none;
display: block;
}
.help a {
display: inline;
}
.help .card > .card-header {
color: #fff;
}
.card-title {
margin-bottom: 0.5rem;
margin-top: 0.5rem;
}
#infoicon {
color: #708090;
}
.heading {
color: #708090;
}
body body
include project.html include project.html
// jQuery // jQuery
script(src="https://code.jquery.com/jquery-3.3.1.min.js") script(src="https://code.jquery.com/jquery-3.3.1.min.js")
script(src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js", integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1", crossorigin="anonymous") script(src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js", integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1", crossorigin="anonymous")
// Bootstrap // Bootstrap
......
...@@ -69,11 +69,6 @@ ...@@ -69,11 +69,6 @@
</div> </div>
</div> </div>
<div class="container"> <div class="container">
<!-- text: Hilfestellung zu Gitlab / short help about Gitlab --> <!-- text: Hilfestellung zu Gitlab / short help about Gitlab -->
<hr /> <hr />
......
doctype html
html(lang="de")
head
title= "Project List"
meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no")
link(rel="stylesheet", type="text/css", href="https://transfer.hft-stuttgart.de/css/bootstrap.min.css")
link(rel="stylesheet", type="text/css", href="https://transfer.hft-stuttgart.de/css/m4lab.css")
link(rel="stylesheet", href="https://use.fontawesome.com/releases/v5.8.2/css/all.css", integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay", crossorigin="anonymous")
body
div(class="container-fluid")
if isUserAuthenticated
p Auf dieser Seite sehen Sie die Liste der über dieses Portal veröffentlichten Projekte.
a(href="/addprojectoverview" class="btn btn-primary" role="button" aria-pressed="true") Projekt anlegen
else
p Auf dieser Seite sehen Sie die Liste der über dieses Portal veröffentlichten Projekte.
p Möchten Sie ein neues Projekt anlegen, dann klicken Sie bitte auf #[a(href="/addprojectoverview") Anmelden und Projekt anlegen]
if successes
for success in successes
div.alert.alert-success.alert-dismissible #{ success }
a(class="close", href="#", data-dismiss="alert", aria-label="close") &times;
// Active projects
h3(class="mb-3 font-weight-bold") Aktive Projekte
table(class="table table-striped")
thead
tr
th Logo
th Akronym
th Title
th Kernziel
th Kategorie
th Ansprechpartner
th Projektinhalte
tbody
for item in active
tr
//td #{item.status}
td
img(src=item.logo, width="40", height="40")
td #{item.akronym}
td #{item.title}
td #{item.summary}
td #{item.category}
td #[a(class="nav-link", href="mailto:"+ item.cp) #{item.cp}]
td #[a(class="nav-link", href="https://m4lab.hft-stuttgart.de/projectoverview?projectID="+item.id) Zur Projektübersicht]
if item.gitlab
a(class="nav-link", href="https://transfer.hft-stuttgart.de/gitlab/"+item.gitlab+"/tree/master") Projektdateien
a(class="nav-link", href="https://transfer.hft-stuttgart.de/gitlab/"+item.gitlab+"/wikis/home") Projektwiki
else
a(class="nav-link", href="#") Projektdateien
a(class="nav-link", href="#") Projektwiki
br
// Non-active projects
h3(class="mb-3 font-weight-bold") Abgeschlossene Projekte
table(class="table table-striped")
thead
tr
th Logo
th Akronym
th Title
th Kernziel
th Kategorie
th Ansprechpartner
th Projektinhalte
tbody
for item in nonActive
tr
//td #{item.status}
td
img(src=item.logo, width="40", height="40")
td #{item.akronym}
td #{item.title}
td #{item.summary}
td #{item.category}
td #[a(class="nav-link", href="mailto:"+ item.cp) #{item.cp}]
td #[a(class="nav-link", href="https://m4lab.hft-stuttgart.de/projectoverview?projectID="+item.id) Zur Projektübersicht]
if item.gitlab
a(class="nav-link", href="https://transfer.hft-stuttgart.de/gitlab/"+item.gitlab+"/tree/master") Projektdateien
a(class="nav-link", href="https://transfer.hft-stuttgart.de/gitlab/"+item.gitlab+"/wikis/home") Projektwiki
else
a(class="nav-link", href="#") Projektdateien
a(class="nav-link", href="#") Projektwiki
// jQuery
script(src="https://code.jquery.com/jquery-3.3.1.min.js")
script(src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js", integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1", crossorigin="anonymous")
// Bootstrap
script(src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous")
// Header
script(src="/js/headfoot.js")
\ No newline at end of file
doctype html
html(lang="de")
head
title= "Project List"
meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no")
link(rel="stylesheet", type="text/css", href="https://transfer.hft-stuttgart.de/css/bootstrap/bootstrap.css")
link(rel="stylesheet", href="https://use.fontawesome.com/releases/v5.8.2/css/all.css", integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay", crossorigin="anonymous")
style.
.help .card-title > a:before {
float: right !important;
content: "-";
padding-right: 5px;
}
.help .card-title > a.collapsed:before {
float: right !important;
content: "+";
}
.help h3 > a {
color: #8a348b;
text-decoration: none;
display: block;
}
.help a {
display: inline;
}
.help .card > .card-header {
color: #fff;
}
.card-title {
margin-bottom: 0.5rem;
margin-top: 0.5rem;
}
#infoicon {
color: #8a348b;
}
body
include project.html
// jQuery
script(src="https://code.jquery.com/jquery-3.3.1.min.js")
script(src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js", integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1", crossorigin="anonymous")
// Bootstrap
script(src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous")
// Header
if isUserAuthenticated
script(src="/js/headfootLogout.js")
else
script(src="/js/headfoot.js")
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment