diff --git a/modules/hudozka-api/src/lib/date.ts b/modules/hudozka-api/src/lib/date.ts new file mode 100644 index 00000000..2d5b7473 --- /dev/null +++ b/modules/hudozka-api/src/lib/date.ts @@ -0,0 +1,18 @@ +import moment from 'moment' +moment.locale('ru') + +/** + * + * Date -> DD.MM.YYYY + * + * @param date + */ +export const dateFormat = (date: string | Date) => moment(date).format('L') + +/** + * + * Date -> Unix time + * + * @param date + */ +export const timestamp = (date: Date) => date.getTime() diff --git a/modules/hudozka-api/src/lib/file.ts b/modules/hudozka-api/src/lib/file.ts new file mode 100644 index 00000000..504cc26d --- /dev/null +++ b/modules/hudozka-api/src/lib/file.ts @@ -0,0 +1,23 @@ +const getExt = (name: string) => /\.(\w+)$/.exec(name)[1] +const defaultExt = (ext: string) => ext.toUpperCase() +const exts = ['png', 'pdf', 'jpg', 'jpeg', 'gif', 'doc'] +const extd = ['ПНГ', 'ПДФ', 'ЖПГ', 'ЖПГ', 'ГИФ', 'ДОК'] +const units = ['байт', 'КБ', 'МБ', 'ГБ', 'ТБ', 'ПБ'] + +export function ext(filname: string): string { + const ext = getExt(filname) + const i = exts.indexOf(ext) + + return i > -1 + ? extd[i] + : defaultExt(ext) +} + +export function size(bytes: any, precision = 1): string { + if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return '' + + const number = Math.floor(Math.log(bytes) / Math.log(1024)) + const size = (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + + return `${size} ${units[number]}` +} diff --git a/modules/hudozka-api/src/lib/sort.js b/modules/hudozka-api/src/lib/sort.js new file mode 100644 index 00000000..3fa95c00 --- /dev/null +++ b/modules/hudozka-api/src/lib/sort.js @@ -0,0 +1,37 @@ +function prioritySort(priority, key, list) { + priority = priority.reverse() + + let weight = i => { + const p = key(i) + const index = priority.indexOf(p) + return index > -1 + ? index + 1 + : 0 + } + + return sort(list, weight) +} + +function sort(list, fn, reverse = false) { + return list + .map(i => [fn(i), i]) + .sort((a, b) => reverse + ? a[0] - b[0] + : b[0] - a[0]) + .map(i => i[1]) +} + +function sortBy(fn, reverse = false) { + return (i1, i2) => { + const t1 = fn(i1) + const t2 = fn(i2) + + return reverse + ? t1 - t2 + : t2 - t1 + } +} + +exports.prioritySort = prioritySort +exports.sortBy = sortBy +exports.sort = sort diff --git a/modules/hudozka-api/src/lib/store.ts b/modules/hudozka-api/src/lib/store.ts new file mode 100644 index 00000000..309848bd --- /dev/null +++ b/modules/hudozka-api/src/lib/store.ts @@ -0,0 +1,19 @@ +export async function find(collection, query, {sort, skip, limit}) { + const cursor = collection.find(query) + + if (sort) cursor.sort(sort) + if (skip) cursor.skip(skip) + if (limit) cursor.limit(limit) + + return cursor.toArray() +} + +export async function findOne(collection, query) { + return collection.findOne(query) +} + +export async function total(collection, query) { + return collection + .find(query) + .count() +} diff --git a/modules/hudozka-api/src/lib/string.js b/modules/hudozka-api/src/lib/string.js new file mode 100644 index 00000000..9a8a5493 --- /dev/null +++ b/modules/hudozka-api/src/lib/string.js @@ -0,0 +1,12 @@ +function titleCase(str) { + return str.charAt(0).toUpperCase() + str.slice(1) +} + +function trim(string) { + return string + .replace(/^\s+/, '') + .replace(/\s+$/, '') +} + +exports.titleCase = titleCase +exports.trim = trim diff --git a/modules/hudozka-api/src/lib/url.js b/modules/hudozka-api/src/lib/url.js new file mode 100644 index 00000000..d68c640d --- /dev/null +++ b/modules/hudozka-api/src/lib/url.js @@ -0,0 +1,40 @@ +function getPathWithNoTrailingSlash(path='') { + path = path.charAt(path.length - 1) === '/' + ? path.substr(0, path.length - 1) + : path + return path.length === 0 + ? '/' + : path +} + +function isEqualPath(a, b, ignoreTrailingSlash = false) { + return ignoreTrailingSlash + ? getPathWithNoTrailingSlash(a) === getPathWithNoTrailingSlash(b) + : a === b +} + +function isMatchPathPattern(pattern, path, ignoreTrailingSlash = false) { + const test = ignoreTrailingSlash + ? getPathWithNoTrailingSlash(path) + : path + return pattern.test(test) +} + +/** + * + * /article/* -> ^\/article\/[/\w\d-_]+\/?$ + * + * @param url + * @return {RegExp} + */ +function urlToPattern(url) { + const u = url.replace('/*', '[/\\w\\d-_]+') + const pattern = `^${u}/?$` + + return new RegExp(pattern) +} + +exports.getPathWithNoTrailingSlash = getPathWithNoTrailingSlash +exports.isMatchPathPattern = isMatchPathPattern +exports.urlToPattern = urlToPattern +exports.isEqualPath = isEqualPath diff --git a/modules/hudozka-api/src/models/course.js b/modules/hudozka-api/src/models/course.js new file mode 100644 index 00000000..35c33679 --- /dev/null +++ b/modules/hudozka-api/src/models/course.js @@ -0,0 +1,22 @@ +const courses = { + painting: ['Живопись', ''], + drawing: ['Рисунок', '/courses/drawing'], + sculpting: ['Скульптура', ''], + sculpture: ['Скульптура', ''], + ceramic: ['Керамика', '/courses/ceramics'], + ceramics: ['Керамика', '/courses/ceramics'], + composition: ['Композиция', ''], + 'easel-composition': ['Станковая композиция', ''], + dpi: ['ДПИ', ''], + 'art-talks': ['Беседа по искусству', ''], + history: ['История искусства', '/courses/art-history'], + cg: ['Комп. графика', '/courses/cg'], +} + +export function getCourse(id) { + const course = id in courses ? courses[id] : null + if (!course) return null + + const [title, url] = course + return {id, title, url} +} diff --git a/modules/hudozka-api/src/models/schedule.js b/modules/hudozka-api/src/models/schedule.js new file mode 100644 index 00000000..7acdde25 --- /dev/null +++ b/modules/hudozka-api/src/models/schedule.js @@ -0,0 +1,44 @@ +export const autumnMonth = 8; +export const springMonth = 0; + +export function scheduleDate(schedule){ + let {period, semester} = schedule; + + let i = semester === 'spring' ? 1 : 0; + let year = getPeriod(period)[i]; + + return new Date( + year, + i ? springMonth : autumnMonth, + 1 + ); +} + +/** + * + * f("2012-2015") -> [2012, 2015] + * f(["2012", "2015"]) -> [2012, 2015] + * f([2012, 2015]) -> [2012, 2015] + * + * @param i + * @returns {*} + */ +export function getPeriod(i){ + if (typeof i === 'string') return getPeriod(i.split('-')); + + return i.map(Number); +} + +/** + * + * f("2012-2015") -> "2012-2015" + * f(["2012", "2015"]) -> "2012-2015" + * f([2012, 2015]) -> "2012-2015" + * + * @param i + * @returns {*} + */ +export function getPeriodString(i) { + return getPeriod(i) + .join('-') +} diff --git a/modules/hudozka-api/src/server/articles.ts b/modules/hudozka-api/src/server/articles.ts new file mode 100644 index 00000000..2dbca9f5 --- /dev/null +++ b/modules/hudozka-api/src/server/articles.ts @@ -0,0 +1,71 @@ +import Article from '../core/Article' +import ImageArtifactType from '../core/ImageArtifactType' +import { sortBy } from '../utils/sort' +import { timestamp } from '../lib/date' +import { encodeImage } from '../factory/image' + +export const sortArticleByDate = sortBy( + x => timestamp(new Date(x.date)) +) + +export async function findArticlesNin(nin, skip, limit, sort) { + const query = { + _id: { $nin: nin } + } + + return Article.find(query, { + sort, + skip, + limit, + }) +} + +export async function findArticles(page: number, pageSize: number) { + const limit = pageSize + const skip = (page - 1) * pageSize + + const pinnedArticles = page === 1 + ? await Article.findPinned() + : [] + + const total = await Article.total() + const totalPages = Math.ceil(total / pageSize) + + const id = i => i._id + const pinnedIds = pinnedArticles.map(id) + const articles = await findArticlesNin(pinnedIds, skip, limit, { date: -1 }) + + const prevPage = page > 1 + ? page - 1 + : null + + const nextPage = page < totalPages + ? page + 1 + : null + + const items = [ + ...pinnedArticles.sort(sortArticleByDate), + ...articles + ] + .map(x => x.plain()) + .map(x => ({ + ...x, + preview: !x.preview ? null : ( + x.preview.getPicture(ImageArtifactType.MEDIUM) + ) + })) + + return { + items, + prevPage, + nextPage, + totalPages, + } +} + +export function encodeArticle(article) { + return { + ...article, + preview: encodeImage(article.preview), + } +} \ No newline at end of file diff --git a/modules/hudozka-api/src/server/collective.js b/modules/hudozka-api/src/server/collective.js new file mode 100644 index 00000000..98dfb699 --- /dev/null +++ b/modules/hudozka-api/src/server/collective.js @@ -0,0 +1,18 @@ +const { collection } = require('../db') +// import { collection } from '../core/db' + +export async function getCollective(query = {}) { + return await collection('collective') + .find(query) + .toArray(); +} + +/** + * Find a person by it's id + * + * @param id + * @returns {Promise|*} + */ +export async function getPerson(id) { + return await collection('collective').findOne({ id: id }); +} diff --git a/modules/hudozka-api/src/server/lib.ts b/modules/hudozka-api/src/server/lib.ts new file mode 100644 index 00000000..898a1080 --- /dev/null +++ b/modules/hudozka-api/src/server/lib.ts @@ -0,0 +1,14 @@ +/** + * + * {a: {}, b: {}, ...} -> Map (a: Class, b: Class) + * + * @param Class + * @param object + */ +export function mapOf(Class, object) { + return Object + .entries(object) + .reduce((map, i) => ( + map.set(i[0], new Class(i[1])) + ), new Map()) +} diff --git a/modules/hudozka-api/src/server/schedule.ts b/modules/hudozka-api/src/server/schedule.ts new file mode 100644 index 00000000..f7b07a1b --- /dev/null +++ b/modules/hudozka-api/src/server/schedule.ts @@ -0,0 +1,135 @@ +import { createMap } from '../utils/common' +import { getCollective } from './collective' +import { + scheduleDate, + getPeriod as schedulePeriod, + getPeriodString +} from '../models/schedule' +import { getCourse } from '../models/course' + +// import { last } from 'lodash' +// const sortBy = require('../lib/sort').sortBy + +const SCHEDULES = 'schedules' + +export async function findSchedule(collection, period, semester) { + const schedule = await collection(SCHEDULES).findOne({ + $or: [ + { period: period }, + { period: schedulePeriod(period) } + ], + semester: semester + }) + + return schedule ? migrate(schedule) : null +} + +// async function findAllScheduleParams() { +// const schedules = await collection(SCHEDULES) +// .find({}, { +// period: 1, +// semester: 1, +// _id: 0, +// }) +// .toArray() + +// return schedules +// .map(i => ({ +// ...i, +// period: schedulePeriod(i.period), +// })) +// .sort(sortBy( +// i => scheduleDate(i).getTime(), +// true, +// )) +// } + +// function text({ semester, period }) { +// const word = semester => [['spring', 'Весна'], ['autumn', 'Осень']] +// .find(i => i[0] === semester) +// [1] +// const periodIndex = semester === 'spring' ? 1 : 0 +// const year = period[periodIndex] + +// return `${word(semester)} ${year} года` +// } + +async function migrate(schedule) { + const versions = { + '1.0': migrate10to20, + '2.0': migrate20to30, + } + + const version = schedule.version + const update = versions[version] + return version in versions ? migrate(update(schedule)) : schedule +} + +function migrate10to20(schedule) { + const dayNames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] + const time = time => time + .map(i => i.split(' ')) + + const week = days => days.reduce((week, lesson, index) => { + if (lesson) { + const day = dayNames[index] + week[day] = [Object.assign(lesson, { + time: time(lesson.time) + })] + } + + return week + }, {}) + + return Object.assign(schedule, { + version: '2.0', + period: schedulePeriod(schedule.period), + groups: schedule.schedule + .map(group => ({ + name: group.group, + time: group.time, + week: week(group.week) + })) + }) +} + +async function migrate20to30(schedule) { + const days = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat']//, 'sun'] + + const persons = await getCollective() + const person = createMap(i => i.id, persons) + + const week = week => days + .map(day => week[day] || []) + .map(lessons => lessons.map(lesson)) + + const lesson = i => ({ + time: i.time, + course: getCourse(i.lesson), + teacher: person.has(i.teacher) ? teacher(i.teacher) : teacherDummy(i.teacher) + }) + + const teacher = id => ({ + id: id, + url: `/teacher/${id}`, + name: personName(id) + }) + + const teacherDummy = name => ({ + id: name, + url: null, + name: name + }) + + const personName = id => person.has(id) ? person.get(id).name : null + + return Object.assign(schedule, { + version: '3.0', + groups: schedule.groups + .map(group => ({ + name: group.name, + time: group.time, + week: week(group.week) + })) + }) +} diff --git a/modules/hudozka-api/src/utils/common.ts b/modules/hudozka-api/src/utils/common.ts new file mode 100644 index 00000000..a2ec4754 --- /dev/null +++ b/modules/hudozka-api/src/utils/common.ts @@ -0,0 +1,106 @@ +/** + * Generates list of numbers + * @param {Number} start starting value + * @param {Number} num amount of numbers in list + * @param {Number} step difference value + * @return {Array} list of numbers + */ +export function range(start, num, step) { + var list = []; + var val = start; + for (var i = 0; i < num; i++) { + list.push(val); + val += step; + } + return list; +} + +/** + * Return a closure that compares a Number argument with specified Number + * @param withNumber Number to compare with + * @returns {Function} (i) => {} + */ +export function numberEquals(withNumber) { + return i => i === withNumber; +} + +/** + * Return a closure that compares a second Number argument with specified Number + * @param withNumber Number to compare with + * @returns {Function} (i) => {} + */ +export function indexEquals(withNumber) { + return (_, i) => i === withNumber; +} + +/** + * Return a reduce function + * [['spring', 'Весена'], ['autumn', 'Осень']] -> 'Весна' or 'Осень' depend on byCondition result + * @param byCondition + * @param map + */ +export function select(byCondition, map = i => i) { + return (result, item) => byCondition(item) ? map(item) : map(result) +} + +/** + * Select an item in Array by specified selector function + * @param scope + * @param selector + * @returns {*} + */ +export function choise(scope, selector) { + return scope.reduce(selector, null); +} + +export function unique(fn: (value: T) => U): (list: T[]) => U[] { + return list => Array.from( + new Set(list.map(fn)) + ) +} + +export function branch(fn) { + let keys = unique(fn); + + return list => keys(list) + .reduce((dict: any, key: any) => { + dict[key] = list.filter( + i => fn(i) === key + ); + + return dict; + }, {}) + +} + +export function assign(dict, key, value) { + dict[key] = value; + return dict; +} + +export function map(v, fn) { + return value => fn(value) ? v : value; +} + +/** + * + * f(g, [x, y, z, ...]) => map of {g(x): x, g(y): y, g(z): z} + * + * @param list + * @param selector + * @returns {*} + */ +export function createMap(selector, list) { + return list.reduce((map, i) => map.set(selector(i), i), new Map()); +} + +/** + * + * f(g) => x ? g(x) : null + * + * @param {function} fn + * @returns {function(*=): null} + */ +export function nullSafe(fn) { + return i => i ? fn(i) : null; +} diff --git a/modules/hudozka-api/src/utils/date.js b/modules/hudozka-api/src/utils/date.js new file mode 100644 index 00000000..9bdac8af --- /dev/null +++ b/modules/hudozka-api/src/utils/date.js @@ -0,0 +1,21 @@ +import {range} from './common'; + +/** + * + * @param start + * @returns {*} + */ +export function getDates(start) { + const oneDay = (1000 * 60 * 60 * 24); + const startTime = start.getTime(); + const startDay = start.getDay(); + + return range(0, 6, 1) + .reduce((dates, weekDay) => { + let delta = 1 + weekDay - startDay; + let time = startTime + (oneDay * delta); + + return dates.concat([new Date(time)]); + }, []) + .map(i => i.getDate()); +} \ No newline at end of file diff --git a/modules/hudozka-api/src/utils/fn.js b/modules/hudozka-api/src/utils/fn.js new file mode 100644 index 00000000..caaf894a --- /dev/null +++ b/modules/hudozka-api/src/utils/fn.js @@ -0,0 +1,13 @@ +/** + * + * f([x, y, z, ...]) -> f(v) -> {v.x, v.y, v.z} + * + * @param fields + * @returns {function(*): *} + */ +export function select(fields=[]){ + return i => fields.reduce((out, key) => { + out[key] = i[key]; + return out; + }, {}); +} diff --git a/modules/hudozka-api/src/utils/net.js b/modules/hudozka-api/src/utils/net.js new file mode 100644 index 00000000..623cab68 --- /dev/null +++ b/modules/hudozka-api/src/utils/net.js @@ -0,0 +1,8 @@ +export function query(list) { + return list + .reduce( + (query, param) => query.concat([`${param[0]}=${param[1]}`]), + [] + ) + .join('&'); +} diff --git a/modules/hudozka-api/src/utils/populate.js b/modules/hudozka-api/src/utils/populate.js new file mode 100644 index 00000000..d5a50261 --- /dev/null +++ b/modules/hudozka-api/src/utils/populate.js @@ -0,0 +1,35 @@ +export function pop(data){ + var Populator = function(data){ + this.target = data; + } + + Populator.prototype = { + populate: function(p){ + var self = this; + if(typeof p == "function"){ + self.target = p(self.target); + }else if(p instanceof Array){ + p.forEach(function(i){ + self.target = self.populate(i).result(); + }); + + } + return self; + }, + result: function(){ + return this.target; + } + }; + + var fun = new Populator(data); + return fun; +} + +export function populate(fn, key){ + return (dict) => { + let id = dict[key]; + let value = fn(id); + dict[key] = value ? value : id; + return dict; + } +} \ No newline at end of file diff --git a/modules/hudozka-api/src/utils/sort.js b/modules/hudozka-api/src/utils/sort.js new file mode 100644 index 00000000..774b1adf --- /dev/null +++ b/modules/hudozka-api/src/utils/sort.js @@ -0,0 +1,26 @@ +export function sortByPattern(list, pattern, map) { + let weight = i => { + const index = pattern.indexOf(map(i)) + return index > -1 ? pattern.length - index : 0 + } + + return sortByWeight(list, weight) +} + +export function sortByWeight(list, fn) { + return list + .map(i => [fn(i), i]) + .sort((a, b) => b[0] - a[0]) + .map(i => i[1]) +} + +export function sortBy(fn) { + return (i1, i2) => { + let t1 = fn(i1) + let t2 = fn(i2) + + if (t1 < t2) return 1 + if (t1 > t2) return -1 + return 0 + } +} diff --git a/modules/hudozka-api/src/utils/string.js b/modules/hudozka-api/src/utils/string.js new file mode 100644 index 00000000..a5e7dace --- /dev/null +++ b/modules/hudozka-api/src/utils/string.js @@ -0,0 +1,22 @@ +import {map} from './common'; + +/** + * Return a function that accept one String parameter and returns result of replacing. + * + * @param searchValue + * @param replaceValue + */ +export const stringReplace = (searchValue, replaceValue) => + str => str.replace(searchValue, replaceValue); + +let toTrue = map(true, i => i === 'true'); +let toFalse = map(false, i => i === 'false'); +const boolProcessors = [toTrue, toFalse]; + +export function stringToBoolean(string){ + return boolProcessors + .reduce( + (value, fn) => fn(value), + string + ); +} \ No newline at end of file