Merge branch 'activitypub' into 'master'
Activitypub See merge request tcit/eventos!12
|
@ -9,6 +9,16 @@ use Mix.Config
|
|||
config :eventos,
|
||||
ecto_repos: [Eventos.Repo]
|
||||
|
||||
config :eventos, :instance,
|
||||
name: "Localhost",
|
||||
version: "1.0.0-dev",
|
||||
registrations_open: true
|
||||
|
||||
config :mime, :types, %{
|
||||
"application/activity+json" => ["activity-json"],
|
||||
"application/jrd+json" => ["jrd-json"]
|
||||
}
|
||||
|
||||
# Configures the endpoint
|
||||
config :eventos, EventosWeb.Endpoint,
|
||||
url: [host: "localhost"],
|
||||
|
|
|
@ -7,7 +7,7 @@ use Mix.Config
|
|||
# watchers to your application. For example, we use it
|
||||
# with brunch.io to recompile .js and .css sources.
|
||||
config :eventos, EventosWeb.Endpoint,
|
||||
http: [port: 4000],
|
||||
http: [port: 4001],
|
||||
debug_errors: true,
|
||||
code_reloader: true,
|
||||
check_origin: false,
|
||||
|
|
17
js/.babelrc
|
@ -1,18 +1,5 @@
|
|||
{
|
||||
"presets": [
|
||||
["env", {
|
||||
"modules": false,
|
||||
"targets": {
|
||||
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
|
||||
}
|
||||
}],
|
||||
"stage-2"
|
||||
],
|
||||
"plugins": ["transform-runtime"],
|
||||
"env": {
|
||||
"test": {
|
||||
"presets": ["env", "stage-2"],
|
||||
"plugins": ["istanbul"]
|
||||
}
|
||||
}
|
||||
"@vue/app"
|
||||
]
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
3
js/.env
Normal file
|
@ -0,0 +1,3 @@
|
|||
API_HOST=localhost
|
||||
API_ORIGIN=http://localhost:4001
|
||||
API_PATH=/api/v1
|
3
js/.env.dist
Normal file
|
@ -0,0 +1,3 @@
|
|||
API_HOST=event.tcit.fr
|
||||
API_ORIGIN=https://event.tcit.fr
|
||||
API_PATH=/api/v1
|
|
@ -1,2 +0,0 @@
|
|||
build/*.js
|
||||
config/*.js
|
|
@ -1,22 +1,7 @@
|
|||
// http://eslint.org/docs/user-guide/configuring
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
sourceType: 'module'
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/recommended' // or 'plugin:vue/base'
|
||||
],
|
||||
|
||||
// add your custom rules here
|
||||
'rules': {
|
||||
// don't require .vue extension when importing
|
||||
'no-console': [0],
|
||||
},
|
||||
'extends': [
|
||||
'plugin:vue/essential',
|
||||
'@vue/airbnb'
|
||||
]
|
||||
}
|
20
js/.gitignore
vendored
|
@ -1,16 +1,26 @@
|
|||
.DS_Store
|
||||
node_modules/
|
||||
dist/
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
/tests/e2e/reports/
|
||||
selenium-debug.log
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
test/unit/coverage
|
||||
test/e2e/reports
|
||||
selenium-debug.log
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
||||
|
||||
.env
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
// https://github.com/michael-ciniawsky/postcss-load-config
|
||||
|
||||
module.exports = {
|
||||
"plugins": {
|
||||
// to edit target browsers: use "browserslist" field in package.json
|
||||
"autoprefixer": {}
|
||||
plugins: {
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
30
js/README.md
|
@ -1,30 +0,0 @@
|
|||
# libre-event
|
||||
|
||||
> A Vue.js project
|
||||
|
||||
## Build Setup
|
||||
|
||||
``` bash
|
||||
# install dependencies
|
||||
npm install
|
||||
|
||||
# serve with hot reload at localhost:8080
|
||||
npm run dev
|
||||
|
||||
# build for production with minification
|
||||
npm run build
|
||||
|
||||
# build for production and view the bundle analyzer report
|
||||
npm run build --report
|
||||
|
||||
# run unit tests
|
||||
npm run unit
|
||||
|
||||
# run e2e tests
|
||||
npm run e2e
|
||||
|
||||
# run all tests
|
||||
npm test
|
||||
```
|
||||
|
||||
For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
|
|
@ -1,40 +0,0 @@
|
|||
require('./check-versions')()
|
||||
|
||||
process.env.NODE_ENV = 'production'
|
||||
|
||||
var ora = require('ora')
|
||||
var rm = require('rimraf')
|
||||
var path = require('path')
|
||||
var chalk = require('chalk')
|
||||
var webpack = require('webpack')
|
||||
var config = require('../config')
|
||||
var webpackConfig = require('./webpack.prod.conf')
|
||||
|
||||
var spinner = ora('building for production...')
|
||||
spinner.start()
|
||||
|
||||
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
|
||||
if (err) throw err
|
||||
webpack(webpackConfig, function (err, stats) {
|
||||
spinner.stop()
|
||||
if (err) throw err
|
||||
process.stdout.write(stats.toString({
|
||||
colors: true,
|
||||
modules: false,
|
||||
children: false,
|
||||
chunks: false,
|
||||
chunkModules: false
|
||||
}) + '\n\n')
|
||||
|
||||
if (stats.hasErrors()) {
|
||||
console.log(chalk.red(' Build failed with errors.\n'))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(' Build complete.\n'))
|
||||
console.log(chalk.yellow(
|
||||
' Tip: built files are meant to be served over an HTTP server.\n' +
|
||||
' Opening index.html over file:// won\'t work.\n'
|
||||
))
|
||||
})
|
||||
})
|
|
@ -1,48 +0,0 @@
|
|||
var chalk = require('chalk')
|
||||
var semver = require('semver')
|
||||
var packageConfig = require('../package.json')
|
||||
var shell = require('shelljs')
|
||||
function exec (cmd) {
|
||||
return require('child_process').execSync(cmd).toString().trim()
|
||||
}
|
||||
|
||||
var versionRequirements = [
|
||||
{
|
||||
name: 'node',
|
||||
currentVersion: semver.clean(process.version),
|
||||
versionRequirement: packageConfig.engines.node
|
||||
}
|
||||
]
|
||||
|
||||
if (shell.which('npm')) {
|
||||
versionRequirements.push({
|
||||
name: 'npm',
|
||||
currentVersion: exec('npm --version'),
|
||||
versionRequirement: packageConfig.engines.npm
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
var warnings = []
|
||||
for (var i = 0; i < versionRequirements.length; i++) {
|
||||
var mod = versionRequirements[i]
|
||||
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
|
||||
warnings.push(mod.name + ': ' +
|
||||
chalk.red(mod.currentVersion) + ' should be ' +
|
||||
chalk.green(mod.versionRequirement)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length) {
|
||||
console.log('')
|
||||
console.log(chalk.yellow('To use this template, you must update following to modules:'))
|
||||
console.log()
|
||||
for (var i = 0; i < warnings.length; i++) {
|
||||
var warning = warnings[i]
|
||||
console.log(' ' + warning)
|
||||
}
|
||||
console.log()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
/* eslint-disable */
|
||||
require('eventsource-polyfill')
|
||||
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
|
||||
|
||||
hotClient.subscribe(function (event) {
|
||||
if (event.action === 'reload') {
|
||||
window.location.reload()
|
||||
}
|
||||
})
|
|
@ -1,92 +0,0 @@
|
|||
require('./check-versions')()
|
||||
|
||||
var config = require('../config')
|
||||
if (!process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
|
||||
}
|
||||
|
||||
var opn = require('opn')
|
||||
var path = require('path')
|
||||
var express = require('express')
|
||||
var webpack = require('webpack')
|
||||
var proxyMiddleware = require('http-proxy-middleware')
|
||||
var webpackConfig = (process.env.NODE_ENV === 'testing' || process.env.NODE_ENV === 'production')
|
||||
? require('./webpack.prod.conf')
|
||||
: require('./webpack.dev.conf')
|
||||
|
||||
// default port where dev server listens for incoming traffic
|
||||
var port = process.env.PORT || config.dev.port
|
||||
// automatically open browser, if not set will be false
|
||||
var autoOpenBrowser = !!config.dev.autoOpenBrowser
|
||||
// Define HTTP proxies to your custom API backend
|
||||
// https://github.com/chimurai/http-proxy-middleware
|
||||
var proxyTable = config.dev.proxyTable
|
||||
|
||||
var app = express()
|
||||
var compiler = webpack(webpackConfig)
|
||||
|
||||
var devMiddleware = require('webpack-dev-middleware')(compiler, {
|
||||
publicPath: webpackConfig.output.publicPath,
|
||||
quiet: true
|
||||
})
|
||||
|
||||
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
|
||||
log: false,
|
||||
heartbeat: 2000
|
||||
})
|
||||
// force page reload when html-webpack-plugin template changes
|
||||
compiler.plugin('compilation', function (compilation) {
|
||||
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
|
||||
hotMiddleware.publish({ action: 'reload' })
|
||||
cb()
|
||||
})
|
||||
})
|
||||
|
||||
// proxy api requests
|
||||
Object.keys(proxyTable).forEach(function (context) {
|
||||
var options = proxyTable[context]
|
||||
if (typeof options === 'string') {
|
||||
options = { target: options }
|
||||
}
|
||||
app.use(proxyMiddleware(options.filter || context, options))
|
||||
})
|
||||
|
||||
// handle fallback for HTML5 history API
|
||||
app.use(require('connect-history-api-fallback')())
|
||||
|
||||
// serve webpack bundle output
|
||||
app.use(devMiddleware)
|
||||
|
||||
// enable hot-reload and state-preserving
|
||||
// compilation error display
|
||||
app.use(hotMiddleware)
|
||||
|
||||
// serve pure static assets
|
||||
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
|
||||
app.use(staticPath, express.static('./static'))
|
||||
|
||||
var uri = 'http://localhost:' + port
|
||||
|
||||
var _resolve
|
||||
var readyPromise = new Promise(resolve => {
|
||||
_resolve = resolve
|
||||
})
|
||||
|
||||
console.log('> Starting dev server...')
|
||||
devMiddleware.waitUntilValid(() => {
|
||||
console.log('> Listening at ' + uri + '\n')
|
||||
// when env is testing, don't need open it
|
||||
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
|
||||
opn(uri)
|
||||
}
|
||||
_resolve()
|
||||
})
|
||||
|
||||
var server = app.listen(port)
|
||||
|
||||
module.exports = {
|
||||
ready: readyPromise,
|
||||
close: () => {
|
||||
server.close()
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
var path = require('path')
|
||||
var config = require('../config')
|
||||
var ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
|
||||
exports.assetsPath = function (_path) {
|
||||
var assetsSubDirectory = process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsSubDirectory
|
||||
: config.dev.assetsSubDirectory
|
||||
return path.posix.join(assetsSubDirectory, _path)
|
||||
}
|
||||
|
||||
exports.cssLoaders = function (options) {
|
||||
options = options || {}
|
||||
|
||||
var cssLoader = {
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
minimize: process.env.NODE_ENV === 'production',
|
||||
sourceMap: options.sourceMap
|
||||
}
|
||||
}
|
||||
|
||||
// generate loader string to be used with extract text plugin
|
||||
function generateLoaders (loader, loaderOptions) {
|
||||
var loaders = [cssLoader]
|
||||
if (loader) {
|
||||
loaders.push({
|
||||
loader: loader + '-loader',
|
||||
options: Object.assign({}, loaderOptions, {
|
||||
sourceMap: options.sourceMap
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Extract CSS when that option is specified
|
||||
// (which is the case during production build)
|
||||
if (options.extract) {
|
||||
return ExtractTextPlugin.extract({
|
||||
use: loaders,
|
||||
fallback: 'vue-style-loader'
|
||||
})
|
||||
} else {
|
||||
return ['vue-style-loader'].concat(loaders)
|
||||
}
|
||||
}
|
||||
|
||||
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
|
||||
return {
|
||||
css: generateLoaders(),
|
||||
postcss: generateLoaders(),
|
||||
less: generateLoaders('less'),
|
||||
sass: generateLoaders('sass', { indentedSyntax: true }),
|
||||
scss: generateLoaders('sass'),
|
||||
stylus: generateLoaders('stylus'),
|
||||
styl: generateLoaders('stylus')
|
||||
}
|
||||
}
|
||||
|
||||
// Generate loaders for standalone style files (outside of .vue)
|
||||
exports.styleLoaders = function (options) {
|
||||
var output = []
|
||||
var loaders = exports.cssLoaders(options)
|
||||
for (var extension in loaders) {
|
||||
var loader = loaders[extension]
|
||||
output.push({
|
||||
test: new RegExp('\\.' + extension + '$'),
|
||||
use: loader
|
||||
})
|
||||
}
|
||||
return output
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
var utils = require('./utils')
|
||||
var config = require('../config')
|
||||
var isProduction = process.env.NODE_ENV === 'production'
|
||||
|
||||
module.exports = {
|
||||
loaders: utils.cssLoaders({
|
||||
sourceMap: isProduction
|
||||
? config.build.productionSourceMap
|
||||
: config.dev.cssSourceMap,
|
||||
extract: isProduction
|
||||
}),
|
||||
transformToRequire: {
|
||||
video: 'src',
|
||||
source: 'src',
|
||||
img: 'src',
|
||||
image: 'xlink:href'
|
||||
}
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
var path = require('path')
|
||||
var utils = require('./utils')
|
||||
var config = require('../config')
|
||||
var vueLoaderConfig = require('./vue-loader.conf')
|
||||
|
||||
function resolve (dir) {
|
||||
return path.join(__dirname, '..', dir)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
app: './src/main.js'
|
||||
},
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
filename: '[name].js',
|
||||
publicPath: process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsPublicPath
|
||||
: config.dev.assetsPublicPath
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.vue', '.json'],
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.esm.js',
|
||||
'@': resolve('src'),
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|vue)$/,
|
||||
loader: 'eslint-loader',
|
||||
enforce: 'pre',
|
||||
include: [resolve('src'), resolve('test')],
|
||||
options: {
|
||||
formatter: require('eslint-friendly-formatter')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: vueLoaderConfig
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
include: [resolve('src'), resolve('test')]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('img/[name].[hash:7].[ext]')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('media/[name].[hash:7].[ext]')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
var utils = require('./utils')
|
||||
var webpack = require('webpack')
|
||||
var config = require('../config')
|
||||
var merge = require('webpack-merge')
|
||||
var baseWebpackConfig = require('./webpack.base.conf')
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
|
||||
|
||||
// add hot-reload related code to entry chunks
|
||||
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
|
||||
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
|
||||
})
|
||||
|
||||
module.exports = merge(baseWebpackConfig, {
|
||||
module: {
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
|
||||
},
|
||||
// cheap-module-eval-source-map is faster for development
|
||||
devtool: '#cheap-module-eval-source-map',
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': config.dev.env
|
||||
}),
|
||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
// https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'index.html',
|
||||
template: 'index.html',
|
||||
inject: true
|
||||
}),
|
||||
new FriendlyErrorsPlugin()
|
||||
]
|
||||
})
|
|
@ -1,126 +0,0 @@
|
|||
var path = require('path')
|
||||
var utils = require('./utils')
|
||||
var webpack = require('webpack')
|
||||
var config = require('../config')
|
||||
var merge = require('webpack-merge')
|
||||
var baseWebpackConfig = require('./webpack.base.conf')
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
var ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
|
||||
|
||||
var env = process.env.NODE_ENV === 'testing'
|
||||
? require('../config/test.env')
|
||||
: config.build.env
|
||||
|
||||
var webpackConfig = merge(baseWebpackConfig, {
|
||||
module: {
|
||||
rules: utils.styleLoaders({
|
||||
sourceMap: config.build.productionSourceMap,
|
||||
extract: true
|
||||
})
|
||||
},
|
||||
devtool: config.build.productionSourceMap ? '#source-map' : false,
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
filename: utils.assetsPath('js/[name].[chunkhash].js'),
|
||||
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
|
||||
},
|
||||
plugins: [
|
||||
// http://vuejs.github.io/vue-loader/en/workflow/production.html
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': env
|
||||
}),
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
warnings: false
|
||||
},
|
||||
sourceMap: true
|
||||
}),
|
||||
// extract css into its own file
|
||||
new ExtractTextPlugin({
|
||||
filename: utils.assetsPath('css/[name].[contenthash].css')
|
||||
}),
|
||||
// Compress extracted CSS. We are using this plugin so that possible
|
||||
// duplicated CSS from different components can be deduped.
|
||||
new OptimizeCSSPlugin({
|
||||
cssProcessorOptions: {
|
||||
safe: true
|
||||
}
|
||||
}),
|
||||
// generate dist index.html with correct asset hash for caching.
|
||||
// you can customize output by editing /index.html
|
||||
// see https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: process.env.NODE_ENV === 'testing'
|
||||
? 'index.html'
|
||||
: config.build.index,
|
||||
template: 'index.html',
|
||||
inject: true,
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeAttributeQuotes: true
|
||||
// more options:
|
||||
// https://github.com/kangax/html-minifier#options-quick-reference
|
||||
},
|
||||
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
|
||||
chunksSortMode: 'dependency'
|
||||
}),
|
||||
// keep module.id stable when vender modules does not change
|
||||
new webpack.HashedModuleIdsPlugin(),
|
||||
// split vendor js into its own file
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'vendor',
|
||||
minChunks: function (module, count) {
|
||||
// any required modules inside node_modules are extracted to vendor
|
||||
return (
|
||||
module.resource &&
|
||||
/\.js$/.test(module.resource) &&
|
||||
module.resource.indexOf(
|
||||
path.join(__dirname, '../node_modules')
|
||||
) === 0
|
||||
)
|
||||
}
|
||||
}),
|
||||
// extract webpack runtime and module manifest to its own file in order to
|
||||
// prevent vendor hash from being updated whenever app bundle is updated
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'manifest',
|
||||
chunks: ['vendor']
|
||||
}),
|
||||
// copy custom static assets
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: path.resolve(__dirname, '../static'),
|
||||
to: config.build.assetsSubDirectory,
|
||||
ignore: ['.*']
|
||||
}
|
||||
])
|
||||
]
|
||||
})
|
||||
|
||||
if (config.build.productionGzip) {
|
||||
var CompressionWebpackPlugin = require('compression-webpack-plugin')
|
||||
|
||||
webpackConfig.plugins.push(
|
||||
new CompressionWebpackPlugin({
|
||||
asset: '[path].gz[query]',
|
||||
algorithm: 'gzip',
|
||||
test: new RegExp(
|
||||
'\\.(' +
|
||||
config.build.productionGzipExtensions.join('|') +
|
||||
')$'
|
||||
),
|
||||
threshold: 10240,
|
||||
minRatio: 0.8
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (config.build.bundleAnalyzerReport) {
|
||||
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
|
||||
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
|
||||
}
|
||||
|
||||
module.exports = webpackConfig
|
|
@ -1,31 +0,0 @@
|
|||
// This is the webpack config used for unit tests.
|
||||
|
||||
var utils = require('./utils')
|
||||
var webpack = require('webpack')
|
||||
var merge = require('webpack-merge')
|
||||
var baseConfig = require('./webpack.base.conf')
|
||||
|
||||
var webpackConfig = merge(baseConfig, {
|
||||
// use inline sourcemap for karma-sourcemap-loader
|
||||
module: {
|
||||
rules: utils.styleLoaders()
|
||||
},
|
||||
devtool: '#inline-source-map',
|
||||
resolveLoader: {
|
||||
alias: {
|
||||
// necessary to to make lang="scss" work in test when using vue-loader's ?inject option
|
||||
// see discussion at https://github.com/vuejs/vue-loader/issues/724
|
||||
'scss-loader': 'sass-loader'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': require('../config/test.env')
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
// no need for app entry during tests
|
||||
delete webpackConfig.entry
|
||||
|
||||
module.exports = webpackConfig
|
|
@ -1,6 +0,0 @@
|
|||
var merge = require('webpack-merge')
|
||||
var prodEnv = require('./prod.env')
|
||||
|
||||
module.exports = merge(prodEnv, {
|
||||
NODE_ENV: '"development"'
|
||||
})
|
|
@ -1,38 +0,0 @@
|
|||
// see http://vuejs-templates.github.io/webpack for documentation.
|
||||
var path = require('path')
|
||||
|
||||
module.exports = {
|
||||
build: {
|
||||
env: require('./prod.env'),
|
||||
index: path.resolve(__dirname, '../../lib/eventos_web/templates/page/index.html.eex'),
|
||||
assetsRoot: path.resolve(__dirname, '../../priv/static'),
|
||||
assetsSubDirectory: '',
|
||||
assetsPublicPath: '/',
|
||||
productionSourceMap: true,
|
||||
// Gzip off by default as many popular static hosts such as
|
||||
// Surge or Netlify already gzip all static assets for you.
|
||||
// Before setting to `true`, make sure to:
|
||||
// npm install --save-dev compression-webpack-plugin
|
||||
productionGzip: false,
|
||||
productionGzipExtensions: ['js', 'css'],
|
||||
// Run the build command with an extra argument to
|
||||
// View the bundle analyzer report after build finishes:
|
||||
// `npm run build --report`
|
||||
// Set to `true` or `false` to always turn it on or off
|
||||
bundleAnalyzerReport: process.env.npm_config_report
|
||||
},
|
||||
dev: {
|
||||
env: require('./dev.env'),
|
||||
port: 8080,
|
||||
autoOpenBrowser: true,
|
||||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
proxyTable: {},
|
||||
// CSS Sourcemaps off by default because relative paths are "buggy"
|
||||
// with this option, according to the CSS-Loader README
|
||||
// (https://github.com/webpack/css-loader#sourcemaps)
|
||||
// In our experience, they generally work as expected,
|
||||
// just be aware of this issue when enabling this option.
|
||||
cssSourceMap: false
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
var merge = require('webpack-merge')
|
||||
var devEnv = require('./dev.env')
|
||||
|
||||
module.exports = merge(devEnv, {
|
||||
NODE_ENV: '"testing"'
|
||||
})
|
|
@ -1,16 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons' rel="stylesheet">
|
||||
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBF37pw38j0giICt73TCAPNogc07Upe_Q4&libraries=places"></script>
|
||||
<meta charset="utf-8">
|
||||
<title>Eventos</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to activate your JS doug.
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
12935
js/package-lock.json
generated
112
js/package.json
|
@ -1,99 +1,41 @@
|
|||
{
|
||||
"name": "libre-event",
|
||||
"version": "1.0.0",
|
||||
"description": "A Vue.js project",
|
||||
"author": "Thomas Citharel <tcit@tcit.fr>",
|
||||
"name": "eventos",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "node build/dev-server.js",
|
||||
"start": "node build/dev-server.js",
|
||||
"build": "node build/build.js",
|
||||
"unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
|
||||
"e2e": "node test/e2e/runner.js",
|
||||
"test": "npm run unit && npm run e2e",
|
||||
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"test:e2e": "vue-cli-service test:e2e"
|
||||
},
|
||||
"dependencies": {
|
||||
"jwt-decode": "^2.2.0",
|
||||
"moment": "^2.20.1",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"moment": "^2.22.1",
|
||||
"ngeohash": "^0.6.0",
|
||||
"vue": "^2.5.13",
|
||||
"register-service-worker": "^1.0.0",
|
||||
"vue": "^2.5.16",
|
||||
"vue-markdown": "^2.2.4",
|
||||
"vue-router": "^3.0.1",
|
||||
"vue2-google-maps": "^0.8.4",
|
||||
"vuetify": "^1.0.0-beta.2",
|
||||
"vuetify-google-autocomplete": "^1.1.0",
|
||||
"vuex": "^2.5.0",
|
||||
"vuex-i18n": "1.8.0"
|
||||
"vuetify": "^1.0.18",
|
||||
"vuetify-google-autocomplete": "^2.0.0-Alpha.9",
|
||||
"vuex": "^3.0.1",
|
||||
"vuex-i18n": "^1.10.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^7.2.4",
|
||||
"avoriaz": "^6.3.0",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-loader": "^7.1.1",
|
||||
"babel-plugin-transform-runtime": "^6.22.0",
|
||||
"babel-preset-env": "^1.3.2",
|
||||
"babel-preset-stage-2": "^6.22.0",
|
||||
"babel-register": "^6.22.0",
|
||||
"dotenv-webpack": "^1.5.5",
|
||||
"@vue/cli-plugin-babel": "^3.0.0-beta.10",
|
||||
"@vue/cli-plugin-e2e-nightwatch": "^3.0.0-beta.10",
|
||||
"@vue/cli-plugin-eslint": "^3.0.0-beta.10",
|
||||
"@vue/cli-plugin-pwa": "^3.0.0-beta.10",
|
||||
"@vue/cli-plugin-unit-mocha": "^3.0.0-beta.10",
|
||||
"@vue/cli-service": "^3.0.0-beta.10",
|
||||
"@vue/eslint-config-airbnb": "^3.0.0-beta.10",
|
||||
"@vue/test-utils": "^1.0.0-beta.10",
|
||||
"chai": "^4.1.2",
|
||||
"chalk": "^2.3.0",
|
||||
"chromedriver": "^2.34.1",
|
||||
"connect-history-api-fallback": "^1.5.0",
|
||||
"copy-webpack-plugin": "^4.3.1",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-spawn": "^5.0.1",
|
||||
"css-loader": "^0.28.8",
|
||||
"cssnano": "^3.10.0",
|
||||
"eslint": "^4.15.0",
|
||||
"eslint-friendly-formatter": "^3.0.0",
|
||||
"eslint-import-resolver-webpack": "^0.8.4",
|
||||
"eslint-loader": "^1.7.1",
|
||||
"eslint-plugin-html": "^3.2.2",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-vue": "^3.14.0",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"express": "^4.16.2",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"file-loader": "^1.1.6",
|
||||
"friendly-errors-webpack-plugin": "^1.1.3",
|
||||
"html-webpack-plugin": "^2.28.0",
|
||||
"http-proxy-middleware": "^0.17.3",
|
||||
"inject-loader": "^3.0.0",
|
||||
"karma": "^1.4.1",
|
||||
"karma-coverage": "^1.1.1",
|
||||
"karma-mocha": "^1.3.0",
|
||||
"karma-phantomjs-launcher": "^1.0.2",
|
||||
"karma-phantomjs-shim": "^1.5.0",
|
||||
"karma-sinon-chai": "^1.3.3",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-spec-reporter": "0.0.31",
|
||||
"karma-webpack": "^2.0.9",
|
||||
"mocha": "^4.1.0",
|
||||
"nightwatch": "^0.9.19",
|
||||
"opn": "^5.1.0",
|
||||
"optimize-css-assets-webpack-plugin": "^3.2.0",
|
||||
"ora": "^1.2.0",
|
||||
"phantomjs-prebuilt": "^2.1.16",
|
||||
"portfinder": "^1.0.13",
|
||||
"rimraf": "^2.6.2",
|
||||
"selenium-server": "^3.8.1",
|
||||
"semver": "^5.3.0",
|
||||
"shelljs": "^0.7.6",
|
||||
"sinon": "^4.1.4",
|
||||
"sinon-chai": "^2.14.0",
|
||||
"url-loader": "^0.6.2",
|
||||
"vue-loader": "^13.7.0",
|
||||
"vue-style-loader": "^3.0.3",
|
||||
"vue-template-compiler": "^2.5.13",
|
||||
"webpack": "^3.10.0",
|
||||
"webpack-dev-middleware": "^1.12.2",
|
||||
"webpack-dev-server": "^2.10.1",
|
||||
"webpack-hot-middleware": "^2.21.0",
|
||||
"webpack-merge": "^4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
"node-sass": "^4.7.2",
|
||||
"sass-loader": "^6.0.6",
|
||||
"vue-template-compiler": "^2.5.13"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
|
BIN
js/public/favicon.ico
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
js/public/img/icons/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
js/public/img/icons/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
js/public/img/icons/apple-touch-icon-120x120.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
js/public/img/icons/apple-touch-icon-152x152.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
js/public/img/icons/apple-touch-icon-180x180.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
js/public/img/icons/apple-touch-icon-60x60.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
js/public/img/icons/apple-touch-icon-76x76.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
js/public/img/icons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
js/public/img/icons/favicon-16x16.png
Normal file
After Width: | Height: | Size: 799 B |
BIN
js/public/img/icons/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
js/public/img/icons/msapplication-icon-144x144.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
js/public/img/icons/mstile-150x150.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
149
js/public/img/icons/safari-pinned-tab.svg
Normal file
|
@ -0,0 +1,149 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="16.000000pt" height="16.000000pt" viewBox="0 0 16.000000 16.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,16.000000) scale(0.000320,-0.000320)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M18 46618 c45 -75 122 -207 122 -211 0 -2 25 -45 55 -95 30 -50 55
|
||||
-96 55 -102 0 -5 5 -10 10 -10 6 0 10 -4 10 -9 0 -5 73 -135 161 -288 89 -153
|
||||
173 -298 187 -323 14 -25 32 -57 41 -72 88 -149 187 -324 189 -335 2 -7 8 -13
|
||||
13 -13 5 0 9 -4 9 -10 0 -5 46 -89 103 -187 175 -302 490 -846 507 -876 8 -16
|
||||
20 -36 25 -45 28 -46 290 -498 339 -585 13 -23 74 -129 136 -236 61 -107 123
|
||||
-215 137 -240 14 -25 29 -50 33 -56 5 -5 23 -37 40 -70 18 -33 38 -67 44 -75
|
||||
11 -16 21 -33 63 -109 14 -25 29 -50 33 -56 4 -5 21 -35 38 -65 55 -100 261
|
||||
-455 269 -465 4 -5 14 -21 20 -35 15 -29 41 -75 103 -180 24 -41 52 -88 60
|
||||
-105 9 -16 57 -100 107 -185 112 -193 362 -626 380 -660 8 -14 23 -38 33 -55
|
||||
11 -16 23 -37 27 -45 4 -8 26 -46 48 -85 23 -38 53 -90 67 -115 46 -81 64
|
||||
-113 178 -310 62 -107 121 -210 132 -227 37 -67 56 -99 85 -148 16 -27 32 -57
|
||||
36 -65 4 -8 15 -27 25 -42 9 -15 53 -89 96 -165 44 -76 177 -307 296 -513 120
|
||||
-206 268 -463 330 -570 131 -227 117 -203 200 -348 36 -62 73 -125 82 -140 10
|
||||
-15 21 -34 25 -42 4 -8 20 -37 36 -65 17 -27 38 -65 48 -82 49 -85 64 -111 87
|
||||
-153 13 -25 28 -49 32 -55 4 -5 78 -134 165 -285 87 -151 166 -288 176 -305
|
||||
10 -16 26 -43 35 -59 9 -17 125 -217 257 -445 132 -229 253 -441 270 -471 17
|
||||
-30 45 -79 64 -108 18 -29 33 -54 33 -57 0 -2 20 -37 44 -77 24 -40 123 -212
|
||||
221 -383 97 -170 190 -330 205 -355 16 -25 39 -65 53 -90 13 -25 81 -144 152
|
||||
-265 70 -121 137 -238 150 -260 12 -22 37 -65 55 -95 18 -30 43 -73 55 -95 12
|
||||
-22 48 -85 80 -140 77 -132 163 -280 190 -330 13 -22 71 -123 130 -225 59
|
||||
-102 116 -199 126 -217 10 -17 29 -50 43 -72 15 -22 26 -43 26 -45 0 -2 27
|
||||
-50 60 -106 33 -56 60 -103 60 -105 0 -2 55 -98 90 -155 8 -14 182 -316 239
|
||||
-414 13 -22 45 -79 72 -124 27 -46 49 -86 49 -89 0 -2 14 -24 30 -48 16 -24
|
||||
30 -46 30 -49 0 -5 74 -135 100 -176 5 -8 24 -42 43 -75 50 -88 58 -101 262
|
||||
-455 104 -179 199 -345 213 -370 14 -25 28 -49 32 -55 4 -5 17 -26 28 -45 10
|
||||
-19 62 -109 114 -200 114 -197 133 -230 170 -295 16 -27 33 -57 38 -65 17 -28
|
||||
96 -165 103 -180 4 -8 16 -28 26 -45 10 -16 77 -131 148 -255 72 -124 181
|
||||
-313 243 -420 62 -107 121 -209 131 -227 35 -62 323 -560 392 -678 38 -66 83
|
||||
-145 100 -175 16 -30 33 -59 37 -65 4 -5 17 -27 29 -47 34 -61 56 -100 90
|
||||
-156 17 -29 31 -55 31 -57 0 -2 17 -32 39 -67 21 -35 134 -229 251 -433 117
|
||||
-203 235 -407 261 -451 27 -45 49 -85 49 -88 0 -4 8 -19 19 -34 15 -21 200
|
||||
-341 309 -533 10 -19 33 -58 51 -87 17 -29 31 -54 31 -56 0 -2 25 -44 55 -94
|
||||
30 -50 55 -95 55 -98 0 -4 6 -15 14 -23 7 -9 27 -41 43 -71 17 -30 170 -297
|
||||
342 -594 171 -296 311 -542 311 -547 0 -5 5 -9 10 -9 6 0 10 -4 10 -10 0 -5
|
||||
22 -47 49 -92 27 -46 58 -99 68 -118 24 -43 81 -140 93 -160 5 -8 66 -114 135
|
||||
-235 69 -121 130 -227 135 -235 12 -21 259 -447 283 -490 10 -19 28 -47 38
|
||||
-62 11 -14 19 -29 19 -32 0 -3 37 -69 83 -148 99 -170 305 -526 337 -583 13
|
||||
-22 31 -53 41 -70 11 -16 22 -37 26 -45 7 -14 82 -146 103 -180 14 -24 181
|
||||
-311 205 -355 13 -22 46 -80 75 -130 29 -49 64 -110 78 -135 14 -25 51 -88 82
|
||||
-140 31 -52 59 -102 63 -110 4 -8 18 -33 31 -55 205 -353 284 -489 309 -535
|
||||
17 -30 45 -78 62 -106 18 -28 36 -60 39 -72 4 -12 12 -22 17 -22 5 0 9 -4 9
|
||||
-10 0 -5 109 -197 241 -427 133 -230 250 -431 259 -448 51 -90 222 -385 280
|
||||
-485 37 -63 78 -135 92 -160 14 -25 67 -117 118 -205 51 -88 101 -175 111
|
||||
-193 34 -58 55 -95 149 -257 51 -88 101 -173 110 -190 9 -16 76 -131 147 -255
|
||||
72 -124 140 -241 151 -260 61 -108 281 -489 355 -615 38 -66 77 -133 87 -150
|
||||
35 -63 91 -161 100 -175 14 -23 99 -169 128 -220 54 -97 135 -235 142 -245 4
|
||||
-5 20 -32 35 -60 26 -48 238 -416 276 -480 10 -16 26 -46 37 -65 30 -53 382
|
||||
-661 403 -695 10 -16 22 -37 26 -45 4 -8 26 -48 50 -88 24 -41 43 -75 43 -77
|
||||
0 -2 22 -40 50 -85 27 -45 50 -84 50 -86 0 -3 38 -69 83 -147 84 -142 302
|
||||
-520 340 -587 10 -19 34 -60 52 -90 18 -30 44 -75 57 -100 14 -25 45 -79 70
|
||||
-120 25 -41 56 -96 70 -121 14 -25 77 -133 138 -240 62 -107 122 -210 132
|
||||
-229 25 -43 310 -535 337 -581 11 -19 26 -45 34 -59 17 -32 238 -414 266 -460
|
||||
11 -19 24 -41 28 -49 3 -7 75 -133 160 -278 84 -146 153 -269 153 -274 0 -5 5
|
||||
-9 10 -9 6 0 10 -4 10 -10 0 -5 82 -150 181 -322 182 -314 201 -346 240 -415
|
||||
12 -21 80 -139 152 -263 71 -124 141 -245 155 -270 14 -25 28 -49 32 -55 6 -8
|
||||
145 -248 220 -380 37 -66 209 -362 229 -395 11 -19 24 -42 28 -49 4 -8 67
|
||||
-118 140 -243 73 -125 133 -230 133 -233 0 -2 15 -28 33 -57 19 -29 47 -78 64
|
||||
-108 17 -30 53 -93 79 -139 53 -90 82 -141 157 -272 82 -142 115 -199 381
|
||||
-659 142 -245 268 -463 281 -485 12 -22 71 -125 132 -230 60 -104 172 -298
|
||||
248 -430 76 -132 146 -253 156 -270 11 -16 22 -36 26 -44 3 -8 30 -54 60 -103
|
||||
29 -49 53 -91 53 -93 0 -3 18 -34 40 -70 22 -36 40 -67 40 -69 0 -2 37 -66 81
|
||||
-142 45 -77 98 -168 119 -204 20 -36 47 -81 58 -100 12 -19 27 -47 33 -62 6
|
||||
-16 15 -28 20 -28 5 0 9 -4 9 -9 0 -6 63 -118 140 -251 77 -133 140 -243 140
|
||||
-245 0 -2 18 -33 41 -70 22 -37 49 -83 60 -101 10 -19 29 -51 40 -71 25 -45
|
||||
109 -189 126 -218 7 -11 17 -29 22 -40 6 -11 22 -38 35 -60 14 -22 37 -62 52
|
||||
-90 14 -27 35 -62 45 -77 11 -14 19 -29 19 -32 0 -3 18 -35 40 -71 22 -36 40
|
||||
-67 40 -69 0 -2 19 -35 42 -72 23 -38 55 -94 72 -124 26 -47 139 -244 171
|
||||
-298 6 -9 21 -36 34 -60 28 -48 37 -51 51 -19 6 12 19 36 29 52 10 17 27 46
|
||||
38 65 11 19 104 181 208 360 103 179 199 345 213 370 14 25 42 74 64 109 21
|
||||
34 38 65 38 67 0 2 18 33 40 69 22 36 40 67 40 69 0 3 177 310 199 346 16 26
|
||||
136 234 140 244 2 5 25 44 52 88 27 44 49 81 49 84 0 2 18 34 40 70 22 36 40
|
||||
67 40 69 0 2 20 36 43 77 35 58 169 289 297 513 9 17 50 86 90 155 40 69 86
|
||||
150 103 180 16 30 35 62 41 70 6 8 16 24 22 35 35 64 72 129 167 293 59 100
|
||||
116 199 127 220 11 20 30 53 41 72 43 72 1070 1850 1121 1940 14 25 65 113
|
||||
113 195 48 83 96 166 107 185 10 19 28 50 38 68 11 18 73 124 137 235 64 111
|
||||
175 303 246 427 71 124 173 299 225 390 52 91 116 202 143 248 27 45 49 85 49
|
||||
89 0 4 6 14 14 22 7 9 28 43 46 76 26 47 251 436 378 655 11 19 29 51 40 70
|
||||
11 19 101 176 201 348 99 172 181 317 181 323 0 5 5 9 10 9 6 0 10 5 10 11 0
|
||||
6 8 23 18 37 11 15 32 52 49 82 16 30 130 228 253 440 122 212 234 405 248
|
||||
430 13 25 39 70 57 100 39 65 69 117 130 225 25 44 50 87 55 95 12 19 78 134
|
||||
220 380 61 107 129 224 150 260 161 277 222 382 246 425 15 28 47 83 71 123
|
||||
24 41 43 78 43 83 0 5 4 9 8 9 4 0 13 12 19 28 7 15 23 45 36 67 66 110 277
|
||||
478 277 483 0 3 6 13 14 21 7 9 27 41 43 71 17 30 45 80 63 110 34 57 375 649
|
||||
394 685 6 11 16 27 22 35 6 8 26 42 44 75 18 33 41 74 51 90 10 17 24 41 32
|
||||
55 54 97 72 128 88 152 11 14 19 28 19 30 0 3 79 141 175 308 96 167 175 305
|
||||
175 308 0 3 6 13 14 21 7 9 26 39 41 66 33 60 276 483 338 587 24 40 46 80 50
|
||||
88 4 8 13 24 20 35 14 23 95 163 125 215 11 19 52 91 92 160 40 69 80 139 90
|
||||
155 9 17 103 179 207 360 105 182 200 346 211 365 103 181 463 802 489 845 7
|
||||
11 15 27 19 35 4 8 29 51 55 95 64 110 828 1433 848 1470 9 17 24 41 33 55 9
|
||||
14 29 48 45 77 15 28 52 93 82 145 30 51 62 107 71 123 17 30 231 398 400 690
|
||||
51 88 103 179 115 202 12 23 26 48 32 55 6 7 24 38 40 68 17 30 61 107 98 170
|
||||
37 63 84 144 103 180 19 36 41 72 48 81 8 8 14 18 14 21 0 4 27 51 59 106 32
|
||||
55 72 124 89 154 16 29 71 125 122 213 51 88 104 180 118 205 13 25 28 50 32
|
||||
55 4 6 17 26 28 45 11 19 45 80 77 135 31 55 66 116 77 135 11 19 88 152 171
|
||||
295 401 694 620 1072 650 1125 11 19 87 152 170 295 83 143 158 273 166 288 9
|
||||
16 21 36 26 45 6 9 31 52 55 96 25 43 54 94 66 115 11 20 95 164 186 321 91
|
||||
157 173 299 182 315 9 17 26 46 37 65 12 19 66 114 121 210 56 96 108 186 117
|
||||
200 8 14 24 40 34 59 24 45 383 664 412 713 5 9 17 29 26 45 15 28 120 210
|
||||
241 419 36 61 68 117 72 125 4 8 12 23 19 34 35 57 245 420 262 453 11 20 35
|
||||
61 53 90 17 29 32 54 32 56 0 3 28 51 62 108 33 57 70 119 80 138 10 19 23 42
|
||||
28 50 5 8 32 53 59 100 27 47 149 258 271 470 122 212 234 405 248 430 30 53
|
||||
62 108 80 135 6 11 15 27 19 35 4 8 85 150 181 315 96 165 187 323 202 350 31
|
||||
56 116 202 130 225 5 8 25 42 43 75 19 33 92 159 162 280 149 257 157 271 202
|
||||
350 19 33 38 67 43 75 9 14 228 392 275 475 12 22 55 96 95 165 40 69 80 139
|
||||
90 155 24 42 202 350 221 383 9 15 27 47 41 72 14 25 75 131 136 236 61 106
|
||||
121 210 134 232 99 172 271 470 279 482 5 8 23 40 40 70 18 30 81 141 142 245
|
||||
60 105 121 210 135 235 14 25 71 124 127 220 56 96 143 247 194 335 51 88 96
|
||||
167 102 175 14 24 180 311 204 355 23 43 340 590 356 615 5 8 50 87 101 175
|
||||
171 301 517 898 582 1008 25 43 46 81 46 83 0 2 12 23 27 47 14 23 40 67 56
|
||||
97 16 30 35 62 42 70 7 8 15 22 18 30 4 8 20 38 37 65 16 28 33 57 37 65 6 12
|
||||
111 196 143 250 5 8 55 95 112 193 57 98 113 195 126 215 12 20 27 46 32 57 6
|
||||
11 14 27 20 35 5 8 76 130 156 270 80 140 165 287 187 325 23 39 52 90 66 115
|
||||
13 25 30 52 37 61 8 8 14 18 14 21 0 4 41 77 92 165 50 87 175 302 276 478
|
||||
101 176 208 360 236 408 28 49 67 117 86 152 19 35 41 70 48 77 6 6 12 15 12
|
||||
19 0 7 124 224 167 291 12 21 23 40 23 42 0 2 21 40 46 83 26 43 55 92 64 109
|
||||
54 95 327 568 354 614 19 30 45 75 59 100 71 128 82 145 89 148 4 2 8 8 8 13
|
||||
0 5 42 82 94 172 311 538 496 858 518 897 14 25 40 70 58 100 18 30 42 71 53
|
||||
90 10 19 79 139 152 265 73 127 142 246 153 265 10 19 43 76 72 125 29 50 63
|
||||
108 75 130 65 116 80 140 87 143 4 2 8 8 8 12 0 8 114 212 140 250 6 8 14 24
|
||||
20 35 5 11 54 97 108 190 l100 170 -9611 3 c-5286 1 -9614 -1 -9618 -5 -5 -6
|
||||
-419 -719 -619 -1068 -89 -155 -267 -463 -323 -560 -38 -66 -81 -140 -95 -165
|
||||
-31 -56 -263 -457 -526 -910 -110 -190 -224 -388 -254 -440 -29 -52 -61 -109
|
||||
-71 -125 -23 -39 -243 -420 -268 -465 -11 -19 -204 -352 -428 -740 -224 -388
|
||||
-477 -826 -563 -975 -85 -148 -185 -322 -222 -385 -37 -63 -120 -207 -185
|
||||
-320 -65 -113 -177 -306 -248 -430 -72 -124 -172 -297 -222 -385 -51 -88 -142
|
||||
-245 -202 -350 -131 -226 -247 -427 -408 -705 -65 -113 -249 -432 -410 -710
|
||||
-160 -278 -388 -673 -506 -877 -118 -205 -216 -373 -219 -373 -3 0 -52 82
|
||||
-109 183 -58 100 -144 250 -192 332 -95 164 -402 696 -647 1120 -85 149 -228
|
||||
396 -317 550 -212 365 -982 1700 -1008 1745 -10 19 -43 76 -72 125 -29 50 -64
|
||||
110 -77 135 -14 25 -63 110 -110 190 -47 80 -96 165 -110 190 -14 25 -99 171
|
||||
-188 325 -89 154 -174 300 -188 325 -13 25 -64 113 -112 195 -48 83 -140 242
|
||||
-205 355 -65 113 -183 317 -263 454 -79 137 -152 264 -163 282 -50 89 -335
|
||||
583 -354 614 -12 19 -34 58 -50 85 -15 28 -129 226 -253 440 -124 215 -235
|
||||
408 -247 430 -12 22 -69 121 -127 220 -58 99 -226 389 -373 645 -148 256 -324
|
||||
561 -392 678 -67 117 -134 232 -147 255 -13 23 -33 59 -46 80 l-22 37 -9615 0
|
||||
-9615 0 20 -32z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 10 KiB |
17
js/public/index.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>eventos</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but eventos doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
20
js/public/manifest.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "eventos",
|
||||
"short_name": "eventos",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/img/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/img/icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": "/index.html",
|
||||
"display": "standalone",
|
||||
"background_color": "#000000",
|
||||
"theme_color": "#4DBA87"
|
||||
}
|
|
@ -61,7 +61,7 @@
|
|||
<v-icon>add</v-icon>
|
||||
</v-btn>
|
||||
<v-footer class="indigo" app>
|
||||
<span class="white--text">© Thomas Citharel {{ new Date().getFullYear() }} - Made with <a href="https://api-platform.com/">API Platform</a> & <a href="https://vuejs.org/">VueJS</a> & <a href="https://www.vuetifyjs.com/">Vuetify</a> with some love and some weeks</span>
|
||||
<span class="white--text">© Thomas Citharel {{ new Date().getFullYear() }} - Made with Elixir, Phoenix & <a href="https://vuejs.org/">VueJS</a> & <a href="https://www.vuetifyjs.com/">Vuetify</a> with some love and some weeks</span>
|
||||
</v-footer>
|
||||
<v-snackbar
|
||||
:timeout="error.timeout"
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export const API_HOST = 'http://0.0.0.0:4000';
|
||||
export const API_PATH = '/api';
|
||||
export const API_HOST = process.env.API_HOST;
|
||||
export const API_ORIGIN = process.env.API_ORIGIN;
|
||||
export const API_PATH = process.env.API_PATH;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { API_HOST, API_PATH } from './_entrypoint';
|
||||
import { API_ORIGIN, API_PATH } from './_entrypoint';
|
||||
|
||||
const jsonLdMimeType = 'application/json';
|
||||
|
||||
|
@ -19,7 +19,7 @@ export default function eventFetch(url, store, optionsarg = {}) {
|
|||
options.headers.set('Authorization', `Bearer ${localStorage.getItem('token')}`);
|
||||
}
|
||||
|
||||
const link = url.includes(API_PATH) ? API_HOST + url : API_HOST + API_PATH + url;
|
||||
const link = url.includes(API_PATH) ? API_ORIGIN + url : API_ORIGIN + API_PATH + url;
|
||||
|
||||
return fetch(link, options).then((response) => {
|
||||
if (response.ok) return response;
|
||||
|
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
@ -1,10 +1,10 @@
|
|||
import { API_HOST, API_PATH } from '../api/_entrypoint';
|
||||
import { API_ORIGIN, API_PATH } from '../api/_entrypoint';
|
||||
|
||||
// URL and endpoint constants
|
||||
const LOGIN_URL = `${API_HOST}${API_PATH}/login`;
|
||||
const SIGNUP_URL = `${API_HOST}${API_PATH}/users/`;
|
||||
const CHECK_AUTH = `${API_HOST}${API_PATH}/user/`;
|
||||
const REFRESH_TOKEN = `${API_HOST}${API_PATH}/token/refresh`;
|
||||
const LOGIN_URL = `${API_ORIGIN}${API_PATH}/login`;
|
||||
const SIGNUP_URL = `${API_ORIGIN}${API_PATH}/users/`;
|
||||
const CHECK_AUTH = `${API_ORIGIN}${API_PATH}/user/`;
|
||||
const REFRESH_TOKEN = `${API_ORIGIN}${API_PATH}/token/refresh`;
|
||||
|
||||
export default {
|
||||
|
||||
|
|
|
@ -4,13 +4,14 @@
|
|||
<v-flex xs12 sm6 offset-sm3>
|
||||
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
|
||||
<v-card v-if="!loading">
|
||||
<v-card-media :src="actor.banner" height="400px">
|
||||
<v-layout column class="media">
|
||||
<v-card-title>
|
||||
<v-btn icon @click="$router.go(-1)">
|
||||
<v-icon>chevron_left</v-icon>
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon class="mr-3" v-if="$store.state.user && $store.state.user.account.id === account.id">
|
||||
<v-btn icon class="mr-3" v-if="$store.state.user && $store.state.user.actor.id === actor.id">
|
||||
<v-icon>edit</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon>
|
||||
|
@ -22,7 +23,7 @@
|
|||
<v-avatar size="125px">
|
||||
<img v-if="!account.avatar_url"
|
||||
class="img-circle elevation-7 mb-1"
|
||||
src="http://lorempixel.com/125/125/"
|
||||
src="https://picsum.photos/125/125/"
|
||||
>
|
||||
<img v-else
|
||||
class="img-circle elevation-7 mb-1"
|
||||
|
@ -33,13 +34,14 @@
|
|||
<v-container fluid grid-list-lg>
|
||||
<v-layout row>
|
||||
<v-flex xs7>
|
||||
<div class="headline">{{ account.display_name }}</div>
|
||||
<div><span class="subheading">@{{ account.username }}</span><span v-if="account.server">@{{ account.server.address }}</span></div>
|
||||
<v-card-text v-if="account.description" v-html="account.description"></v-card-text>
|
||||
<div class="headline">{{ actor.display_name }}</div>
|
||||
<div><span class="subheading">@{{ actor.username }}<span v-if="actor.domain">@{{ actor.domain }}</span></span></div>
|
||||
<v-card-text v-if="actor.description" v-html="actor.description"></v-card-text>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-layout>
|
||||
</v-card-media>
|
||||
<v-list three-line>
|
||||
<v-list-tile>
|
||||
<v-list-tile-action>
|
||||
|
@ -74,15 +76,15 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
</v-list>
|
||||
<v-container fluid grid-list-md v-if="account.participatingEvents && account.participatingEvents.length > 0">
|
||||
<v-container fluid grid-list-md v-if="actor.participatingEvents && actor.participatingEvents.length > 0">
|
||||
<v-subheader>Participated at</v-subheader>
|
||||
<v-layout row wrap>
|
||||
<v-flex v-for="event in account.participatingEvents" :key="event.id">
|
||||
<v-flex v-for="event in actor.participatingEvents" :key="event.id">
|
||||
<v-card>
|
||||
<v-card-media
|
||||
class="black--text"
|
||||
height="200px"
|
||||
src="http://lorempixel.com/400/200/"
|
||||
src="https://picsum.photos/400/200/"
|
||||
>
|
||||
<v-container fill-height fluid>
|
||||
<v-layout fill-height>
|
||||
|
@ -115,15 +117,15 @@
|
|||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
<v-container fluid grid-list-md v-if="account.organizingEvents && account.organizingEvents.length > 0">
|
||||
<v-container fluid grid-list-md v-if="actor.organizingEvents && actor.organizingEvents.length > 0">
|
||||
<v-subheader>Organized events</v-subheader>
|
||||
<v-layout row wrap>
|
||||
<v-flex v-for="event in account.organizingEvents" :key="event.id">
|
||||
<v-flex v-for="event in actor.organizingEvents" :key="event.id">
|
||||
<v-card>
|
||||
<v-card-media
|
||||
class="black--text"
|
||||
height="200px"
|
||||
src="http://lorempixel.com/400/200/"
|
||||
src="https://picsum.photos/400/200/"
|
||||
>
|
||||
<v-container fill-height fluid>
|
||||
<v-layout fill-height>
|
||||
|
@ -169,12 +171,17 @@ export default {
|
|||
name: 'Account',
|
||||
data() {
|
||||
return {
|
||||
account: null,
|
||||
actor: null,
|
||||
loading: true,
|
||||
}
|
||||
},
|
||||
props: ['id'],
|
||||
mounted() {
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchData();
|
||||
},
|
||||
watch: {
|
||||
|
@ -183,12 +190,12 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
fetchData() {
|
||||
eventFetch(`/accounts/${this.id}`, this.$store)
|
||||
eventFetch(`/actors/${this.name}`, this.$store)
|
||||
.then(response => response.json())
|
||||
.then((response) => {
|
||||
this.account = response.data;
|
||||
this.actor = response.data;
|
||||
this.loading = false;
|
||||
console.log(this.account);
|
||||
console.log(this.actor);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
<template>
|
||||
<v-container fluid grid-list-md>
|
||||
<v-container fluid grid-list-sm>
|
||||
<h3>Create a new event</h3>
|
||||
<v-form>
|
||||
<v-stepper v-model="e1" vertical>
|
||||
<v-stepper-step step="1" :complete="e1 > 1">Basic Informations
|
||||
<v-stepper v-model="e1">
|
||||
<v-stepper-header>
|
||||
<v-stepper-step step="1" :complete="e1 > 1" editable>Basic Informations
|
||||
<small>Title and description</small>
|
||||
</v-stepper-step>
|
||||
<v-divider></v-divider>
|
||||
<v-stepper-step step="2" :complete="e1 > 2" editable>Date and place</v-stepper-step>
|
||||
<v-divider></v-divider>
|
||||
<v-stepper-step step="3" :complete="e1 > 3">Extra informations</v-stepper-step>
|
||||
</v-stepper-header>
|
||||
<v-stepper-items>
|
||||
<v-stepper-content step="1">
|
||||
<v-layout row wrap>
|
||||
<v-flex xs12>
|
||||
|
@ -59,7 +66,6 @@
|
|||
</v-layout>
|
||||
<v-btn color="primary" @click.native="e1 = 2">Next</v-btn>
|
||||
</v-stepper-content>
|
||||
<v-stepper-step step="2" :complete="e1 > 2">Date and place</v-stepper-step>
|
||||
<v-stepper-content step="2">
|
||||
Event starts at:
|
||||
<v-text-field type="datetime-local" v-model="event.begins_on"></v-text-field>
|
||||
|
@ -171,12 +177,12 @@
|
|||
placeholder="Start typing"
|
||||
label="Location"
|
||||
enable-geolocation
|
||||
types="geocode"
|
||||
v-on:placechanged="getAddressData"
|
||||
>
|
||||
</vuetify-google-autocomplete>
|
||||
<v-btn color="primary" @click.native="e1 = 3">Next</v-btn>
|
||||
</v-stepper-content>
|
||||
<v-stepper-step step="3" :complete="e1 > 3">Extra informations</v-stepper-step>
|
||||
<v-stepper-content step="3">
|
||||
<v-text-field
|
||||
label="Number of seats"
|
||||
|
@ -189,6 +195,7 @@
|
|||
v-model="event.price"
|
||||
></v-text-field>
|
||||
</v-stepper-content>
|
||||
</v-stepper-items>
|
||||
</v-stepper>
|
||||
</v-form>
|
||||
<v-btn color="primary" @click="create">Create event</v-btn>
|
||||
|
@ -197,7 +204,6 @@
|
|||
|
||||
<script>
|
||||
// import Location from '@/components/Location';
|
||||
import VuetifyGoogleAutocomplete from 'vuetify-google-autocomplete';
|
||||
import eventFetch from '@/api/eventFetch';
|
||||
import VueMarkdown from 'vue-markdown';
|
||||
|
||||
|
@ -208,7 +214,6 @@
|
|||
components: {
|
||||
/* Location,*/
|
||||
VueMarkdown,
|
||||
VuetifyGoogleAutocomplete
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -241,7 +246,7 @@
|
|||
participants: [],
|
||||
},
|
||||
categories: [],
|
||||
tags: [{ name: 'test' }, { name: 'montag' }],
|
||||
tags: [],
|
||||
tagsToSend: [],
|
||||
tagsFetched: [],
|
||||
};
|
||||
|
@ -260,13 +265,13 @@
|
|||
this.event.seats = parseInt(this.event.seats, 10);
|
||||
this.tagsToSend.forEach((tag) => {
|
||||
this.event.tags.push({
|
||||
name: tag,
|
||||
title: tag,
|
||||
// '@type': 'Tag',
|
||||
});
|
||||
});
|
||||
this.event.category_id = this.event.category.id;
|
||||
this.event.organizer_account_id = this.$store.state.user.account.id;
|
||||
this.event.participants = [this.$store.state.user.account.id];
|
||||
this.event.organizer_actor_id = this.$store.state.user.actor.id;
|
||||
this.event.participants = [this.$store.state.user.actor.id];
|
||||
this.event.price = parseFloat(this.event.price);
|
||||
|
||||
if (this.id === undefined) {
|
||||
|
@ -284,6 +289,7 @@
|
|||
this.$router.push({name: 'Event', params: {id: data.id}});
|
||||
});
|
||||
}
|
||||
this.event.tags = [];
|
||||
},
|
||||
fetchCategories() {
|
||||
eventFetch('/categories', this.$store)
|
||||
|
@ -313,7 +319,7 @@
|
|||
});
|
||||
},
|
||||
getAddressData: function (addressData) {
|
||||
console.log(addressData);
|
||||
if (addressData !== null) {
|
||||
this.event.address = {
|
||||
geom: {
|
||||
data: {
|
||||
|
@ -328,6 +334,7 @@
|
|||
postalCode: addressData.postal_code,
|
||||
streetAddress: `${addressData.street_number} ${addressData.route}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -70,7 +70,6 @@
|
|||
<vuetify-google-autocomplete
|
||||
id="map"
|
||||
append-icon="search"
|
||||
classname="form-control"
|
||||
placeholder="Start typing"
|
||||
label="Location"
|
||||
enable-geolocation
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<v-icon>chevron_left</v-icon>
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon class="mr-3" v-if="event.organizer.id === $store.state.user.account.id" :to="{ name: 'EditEvent', params: {id: event.id}}">
|
||||
<v-btn icon class="mr-3" v-if="event.organizer.id === $store.state.user.actor.id" :to="{ name: 'EditEvent', params: {id: event.id}}">
|
||||
<v-icon>edit</v-icon>
|
||||
</v-btn>
|
||||
<v-menu bottom left>
|
||||
|
@ -22,60 +22,69 @@
|
|||
<v-list-tile @click="downloadIcsEvent()">
|
||||
<v-list-tile-title>Download</v-list-tile-title>
|
||||
</v-list-tile>
|
||||
<v-list-tile @click="deleteEvent()" v-if="$store.state.user.account.id === event.organizer.id">
|
||||
<v-list-tile @click="deleteEvent()" v-if="$store.state.user.actor.id === event.organizer.id">
|
||||
<v-list-tile-title>Delete</v-list-tile-title>
|
||||
</v-list-tile>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-card-title>
|
||||
<v-container grid-list-md text-xs-center>
|
||||
<v-card-media
|
||||
src="https://picsum.photos/600/400/"
|
||||
height="200px"
|
||||
>
|
||||
|
||||
</v-card-media>
|
||||
<v-layout row wrap>
|
||||
<v-flex xs6>
|
||||
<v-spacer></v-spacer>
|
||||
<div class="text-xs-center">
|
||||
<v-card-title class="pl-5 pt-5">
|
||||
<div class="display-1 pl-5 pt-5">{{ event.title }}</div>
|
||||
</v-card-title>
|
||||
<span class="subheading grey--text">{{ event.begins_on | formatDay }}</span>
|
||||
<h1 class="display-2">{{ event.title }}</h1>
|
||||
<div>
|
||||
<router-link :to="{name: 'Account', params: { name: event.organizer.username } }">
|
||||
<v-avatar size="25px">
|
||||
<img class="img-circle elevation-7 mb-1"
|
||||
:src="event.organizer.avatar"
|
||||
>
|
||||
</v-avatar>
|
||||
</router-link>
|
||||
<span v-if="event.organizer">Organisé par {{ event.organizer.display_name }}</span>
|
||||
</div>
|
||||
<p>
|
||||
<vue-markdown :source="event.description" />
|
||||
</p>
|
||||
<!--<p><router-link :to="{ name: 'Account', params: {id: event.organizer.id} }"><span class="grey--text">{{ event.organizer.username }}</span></router-link> organises {{ event.title }} <span v-if="event.address.addressLocality">in {{ event.address.addressLocality }}</span> on the {{ event.startDate | formatDate }}.</p>
|
||||
<v-card-text v-if="event.description"><vue-markdown :source="event.description"></vue-markdown></v-card-text>-->
|
||||
</div>
|
||||
<v-container fluid grid-list-md>
|
||||
<v-subheader>Membres</v-subheader>
|
||||
<v-layout row>
|
||||
<v-flex xs2>
|
||||
<router-link :to="{name: 'Account', params: {'id': event.organizer.id}}">
|
||||
<v-avatar size="75px">
|
||||
<img v-if="!event.organizer.avatar_url"
|
||||
class="img-circle elevation-7 mb-1"
|
||||
src="http://lorempixel.com/125/125/"
|
||||
>
|
||||
<img v-else
|
||||
class="img-circle elevation-7 mb-1"
|
||||
:src="event.organizer.avatar_url"
|
||||
>
|
||||
</v-avatar>
|
||||
</router-link>
|
||||
Organisateur <span>{{ event.organizer.username }}</span>
|
||||
</v-flex>
|
||||
<v-flex xs2 v-for="account in event.participants" :key="account.id">
|
||||
<router-link :to="{name: 'Account', params: {'id': account.id}}">
|
||||
<v-avatar size="75px">
|
||||
<img v-if="!account.avatar_url"
|
||||
class="img-circle elevation-7 mb-1"
|
||||
src="http://lorempixel.com/125/125/"
|
||||
>
|
||||
<img v-else
|
||||
class="img-circle elevation-7 mb-1"
|
||||
:src="account.avatar_url"
|
||||
>
|
||||
</v-avatar>
|
||||
</router-link>
|
||||
<span>{{ account.username }}</span>
|
||||
<v-flex xs6>
|
||||
<v-card-actions>
|
||||
<v-btn color="success" v-if="!event.participants.map(participant => participant.id).includes($store.state.user.actor.id)" @click="joinEvent" class="btn btn-primary"><v-icon>check</v-icon> Join</v-btn>
|
||||
<v-btn v-if="event.participants.map(participant => participant.id).includes($store.state.user.actor.id)" @click="leaveEvent" class="btn btn-primary">Leave</v-btn>
|
||||
</v-card-actions>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
<v-card-actions>
|
||||
<button v-if="!event.participants.map(participant => participant.id).includes($store.state.user.account.id)" @click="joinEvent" class="btn btn-primary">Join</button>
|
||||
<button v-if="event.participants.map(participant => participant.id).includes($store.state.user.account.id)" @click="leaveEvent" class="btn btn-primary">Leave</button>
|
||||
<button @click="deleteEvent" class="btn btn-danger">Delete</button>
|
||||
</v-card-actions>
|
||||
<v-container fluid grid-list-md>
|
||||
<v-subheader>Membres</v-subheader>
|
||||
<v-layout row>
|
||||
<v-flex xs2 v-for="actor in event.participants" :key="actor.uuid">
|
||||
<router-link :to="{name: 'Account', params: { name: actor.username }}">
|
||||
<v-avatar size="75px">
|
||||
<img v-if="!actor.avatar"
|
||||
class="img-circle elevation-7 mb-1"
|
||||
src="https://picsum.photos/125/125/"
|
||||
>
|
||||
<img v-else
|
||||
class="img-circle elevation-7 mb-1"
|
||||
:src="actor.avatar"
|
||||
>
|
||||
</v-avatar>
|
||||
</router-link>
|
||||
<span>{{ actor.username }}</span>
|
||||
</v-flex>
|
||||
<span v-if="event.participants.length === 0">No participants yet.</span>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-layout>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
|
@ -97,8 +106,10 @@
|
|||
loading: true,
|
||||
error: false,
|
||||
event: {
|
||||
id: this.id,
|
||||
name: '',
|
||||
slug: '',
|
||||
title: '',
|
||||
uuid: this.uuid,
|
||||
description: '',
|
||||
organizer: {
|
||||
id: null,
|
||||
|
@ -111,11 +122,11 @@
|
|||
methods: {
|
||||
deleteEvent() {
|
||||
const router = this.$router;
|
||||
eventFetch(`/events/${this.id}`, this.$store, { method: 'DELETE' })
|
||||
eventFetch(`/events/${this.uuid}`, this.$store, { method: 'DELETE' })
|
||||
.then(() => router.push({'name': 'EventList'}));
|
||||
},
|
||||
fetchData() {
|
||||
eventFetch(`/events/${this.id}`, this.$store)
|
||||
eventFetch(`/events/${this.uuid}`, this.$store)
|
||||
.then(response => response.json())
|
||||
.then((data) => {
|
||||
this.loading = false;
|
||||
|
@ -129,21 +140,21 @@
|
|||
});
|
||||
},
|
||||
joinEvent() {
|
||||
eventFetch(`/events/${this.id}/join`, this.$store)
|
||||
eventFetch(`/events/${this.uuid}/join`, this.$store)
|
||||
.then(response => response.json())
|
||||
.then((data) => {
|
||||
console.log(data);
|
||||
});
|
||||
},
|
||||
leaveEvent() {
|
||||
eventFetch(`/events/${this.id}/leave`, this.$store)
|
||||
eventFetch(`/events/${this.uuid}/leave`, this.$store)
|
||||
.then(response => response.json())
|
||||
.then((data) => {
|
||||
console.log(data);
|
||||
});
|
||||
},
|
||||
downloadIcsEvent() {
|
||||
eventFetch('/events/' + this.event.id + '/ics', this.$store, {responseType: 'arraybuffer'})
|
||||
eventFetch(`/events/${this.uuid}/ics`, this.$store, {responseType: 'arraybuffer'})
|
||||
.then((response) => response.text())
|
||||
.then(response => {
|
||||
const blob = new Blob([response],{type: 'text/calendar'});
|
||||
|
@ -156,7 +167,12 @@
|
|||
})
|
||||
},
|
||||
},
|
||||
props: ['id'],
|
||||
props: {
|
||||
uuid: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchData();
|
||||
},
|
||||
|
|
|
@ -1,45 +1,56 @@
|
|||
<template>
|
||||
<v-container>
|
||||
<v-layout>
|
||||
<v-flex xs12 sm8 offset-sm2>
|
||||
<v-card>
|
||||
<h1>{{ $t("event.list.title") }}</h1>
|
||||
|
||||
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
|
||||
<v-chip close v-model="locationChip" label color="pink" text-color="white" v-if="$router.currentRoute.params.location">
|
||||
<v-icon left>location_city</v-icon>{{ locationText }}
|
||||
</v-chip>
|
||||
<v-layout row wrap justify-space-around>
|
||||
<v-flex xs12 md3 v-for="event in events" :key="event.id">
|
||||
<v-container grid-list-sm fluid>
|
||||
<v-layout row wrap>
|
||||
<v-flex xs4 v-for="event in events" :key="event.id">
|
||||
<v-card>
|
||||
<v-card-media v-if="event.image"
|
||||
<v-card-media v-if="!event.image"
|
||||
class="white--text"
|
||||
height="200px"
|
||||
src="http://lorempixel.com/400/200/"
|
||||
src="https://picsum.photos/g/400/200/"
|
||||
>
|
||||
<v-container fill-height fluid>
|
||||
<v-layout fill-height>
|
||||
<v-flex xs12 align-end flexbox>
|
||||
<span class="headline">{{ event.title }}</span>
|
||||
<span class="headline black--text">{{ event.title }}</span>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-card-media>
|
||||
<v-card-title v-else primary-title>
|
||||
<div class="headline">{{ event.title }}</div>
|
||||
<v-card-title primary-title>
|
||||
<div>
|
||||
<span class="grey--text">{{ event.begins_on | formatDate }}</span><br>
|
||||
<router-link :to="{name: 'Account', params: { name: event.organizer.username } }">
|
||||
<v-avatar size="25px">
|
||||
<img class="img-circle elevation-7 mb-1"
|
||||
:src="event.organizer.avatar"
|
||||
>
|
||||
</v-avatar>
|
||||
</router-link>
|
||||
<span v-if="event.organizer">Organisé par <router-link :to="{name: 'Account', params: {'name': event.organizer.username}}">{{ event.organizer.username }}</router-link></span>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-container>
|
||||
<!--<span class="grey--text">{{ event.startDate | formatDate }} à <router-link :to="{name: 'EventList', params: {location: geocode(event.address.geo.latitude, event.address.geo.longitude, 10) }}">{{ event.address.addressLocality }}</router-link></span><br>-->
|
||||
<p><vue-markdown>{{ event.description }}</vue-markdown></p>
|
||||
<p v-if="event.organizer">Organisé par <router-link :to="{name: 'Account', params: {'id': event.organizer.id}}">{{ event.organizer.username }}</router-link></p>
|
||||
</v-container>
|
||||
<v-card-actions>
|
||||
<v-btn flat color="orange" @click="downloadIcsEvent(event)">Share</v-btn>
|
||||
<v-btn flat color="orange" @click="viewEvent(event.id)">Explore</v-btn>
|
||||
<v-btn flat color="red" @click="deleteEvent(event.id)">Delete</v-btn>
|
||||
<v-btn flat color="orange" @click="viewEvent(event)">Explore</v-btn>
|
||||
<v-btn flat color="red" @click="deleteEvent(event)">Delete</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
<router-link :to="{ name: 'CreateEvent' }" class="btn btn-default">Create</router-link>
|
||||
</v-container>
|
||||
<router-link :to="{ name: 'CreateEvent' }" class="btn btn-default">Create</router-link>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -96,18 +107,19 @@
|
|||
.then((response) => {
|
||||
this.loading = false;
|
||||
this.events = response.data;
|
||||
console.log(this.events);
|
||||
});
|
||||
},
|
||||
deleteEvent(id) {
|
||||
deleteEvent(event) {
|
||||
const router = this.$router;
|
||||
eventFetch('/events/' + id, this.$store, {'method': 'DELETE'})
|
||||
eventFetch(`/events/${event.uuid}`, this.$store, {'method': 'DELETE'})
|
||||
.then(() => router.push('/events'));
|
||||
},
|
||||
viewEvent(id) {
|
||||
this.$router.push({ name: 'Event', params: { id } })
|
||||
viewEvent(event) {
|
||||
this.$router.push({ name: 'Event', params: { uuid: event.uuid } })
|
||||
},
|
||||
downloadIcsEvent(event) {
|
||||
eventFetch('/events/' + event.id + '/export', this.$store, {responseType: 'arraybuffer'})
|
||||
eventFetch(`/events/${event.uuid}/ics`, this.$store, {responseType: 'arraybuffer'})
|
||||
.then((response) => response.text())
|
||||
.then(response => {
|
||||
const blob = new Blob([response],{type: 'text/calendar'});
|
||||
|
|
|
@ -6,7 +6,15 @@
|
|||
<v-flex xs12>
|
||||
<v-text-field
|
||||
label="Title"
|
||||
v-model="group.title"
|
||||
v-model="group.preferred_username"
|
||||
:counter="100"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs12>
|
||||
<v-text-field
|
||||
label="Title"
|
||||
v-model="group.name"
|
||||
:counter="100"
|
||||
required
|
||||
></v-text-field>
|
||||
|
@ -14,7 +22,7 @@
|
|||
<v-flex md6>
|
||||
<v-text-field
|
||||
label="Description"
|
||||
v-model="group.description"
|
||||
v-model="group.summary"
|
||||
multiLine
|
||||
required
|
||||
></v-text-field>
|
||||
|
@ -22,34 +30,34 @@
|
|||
<v-flex md6>
|
||||
<vue-markdown class="markdown-render"
|
||||
:watches="['show','html','breaks','linkify','emoji','typographer','toc']"
|
||||
:source="group.description"
|
||||
:source="group.summary"
|
||||
:show="true" :html="false" :breaks="true" :linkify="true"
|
||||
:emoji="true" :typographer="true" :toc="false"
|
||||
></vue-markdown>
|
||||
</v-flex>
|
||||
<v-flex md12>
|
||||
<vuetify-google-autocomplete
|
||||
id="map"
|
||||
append-icon="search"
|
||||
classname="form-control"
|
||||
placeholder="Start typing"
|
||||
enable-geolocation
|
||||
v-on:placechanged="getAddressData"
|
||||
>
|
||||
</vuetify-google-autocomplete>
|
||||
</v-flex>
|
||||
<v-flex md12>
|
||||
<v-select
|
||||
v-bind:items="categories"
|
||||
v-model="group.category"
|
||||
item-text="title"
|
||||
item-value="@id"
|
||||
label="Categories"
|
||||
single-line
|
||||
bottom
|
||||
types="(cities)"
|
||||
></v-select>
|
||||
</v-flex>
|
||||
<!--<v-flex md12>-->
|
||||
<!--<vuetify-google-autocomplete-->
|
||||
<!--id="map"-->
|
||||
<!--append-icon="search"-->
|
||||
<!--classname="form-control"-->
|
||||
<!--placeholder="Start typing"-->
|
||||
<!--enable-geolocation-->
|
||||
<!--v-on:placechanged="getAddressData"-->
|
||||
<!-->-->
|
||||
<!--</vuetify-google-autocomplete>-->
|
||||
<!--</v-flex>-->
|
||||
<!--<v-flex md12>-->
|
||||
<!--<v-select-->
|
||||
<!--v-bind:items="categories"-->
|
||||
<!--v-model="group.category"-->
|
||||
<!--item-text="title"-->
|
||||
<!--item-value="@id"-->
|
||||
<!--label="Categories"-->
|
||||
<!--single-line-->
|
||||
<!--bottom-->
|
||||
<!--types="(cities)"-->
|
||||
<!--></v-select>-->
|
||||
<!--</v-flex>-->
|
||||
</v-layout>
|
||||
</v-form>
|
||||
<v-btn color="primary" @click="create">Create group</v-btn>
|
||||
|
@ -72,9 +80,10 @@
|
|||
return {
|
||||
e1: 0,
|
||||
group: {
|
||||
title: '',
|
||||
description: '',
|
||||
category: null,
|
||||
preferred_username: '',
|
||||
name: '',
|
||||
summary: '',
|
||||
// category: null,
|
||||
},
|
||||
categories: [],
|
||||
};
|
||||
|
|
|
@ -4,15 +4,16 @@
|
|||
<v-flex xs12 sm6 offset-sm3>
|
||||
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
|
||||
<v-card v-if="!loading">
|
||||
<v-card-media :src="group.banner" height="400px">
|
||||
<v-layout column class="media">
|
||||
<v-card-title>
|
||||
<v-btn icon @click="$router.go(-1)">
|
||||
<v-icon>chevron_left</v-icon>
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon class="mr-3" v-if="$store.state.user">
|
||||
<v-icon>edit</v-icon>
|
||||
</v-btn>
|
||||
<!--<v-btn icon class="mr-3" v-if="$store.state.user && $store.state.user.actor.id === actor.id">-->
|
||||
<!--<v-icon>edit</v-icon>-->
|
||||
<!--</v-btn>-->
|
||||
<v-btn icon>
|
||||
<v-icon>more_vert</v-icon>
|
||||
</v-btn>
|
||||
|
@ -20,21 +21,40 @@
|
|||
<v-spacer></v-spacer>
|
||||
<div class="text-xs-center">
|
||||
<v-avatar size="125px">
|
||||
<img v-if="!group.avatar_url"
|
||||
<img v-if="!group.avatar"
|
||||
class="img-circle elevation-7 mb-1"
|
||||
src="http://lorempixel.com/125/125/"
|
||||
src="https://picsum.photos/125/125/"
|
||||
>
|
||||
<img v-else
|
||||
class="img-circle elevation-7 mb-1"
|
||||
:src="group.avatar_url"
|
||||
:src="group.avatar"
|
||||
>
|
||||
</v-avatar>
|
||||
<v-card-title class="pl-5 pt-5">
|
||||
<div class="display-1 pl-5 pt-5">{{ group.title }}<span v-if="group.server">@{{ group.server.address }}</span></div>
|
||||
</v-card-title>
|
||||
<v-card-text v-html="group.description"></v-card-text>
|
||||
</div>
|
||||
<v-container fluid grid-list-lg>
|
||||
<v-layout row>
|
||||
<v-flex xs7>
|
||||
<div class="headline">{{ group.display_name }}</div>
|
||||
<div>
|
||||
<span class="subheading">
|
||||
~{{ group.username }}
|
||||
<span v-if="group.domain">
|
||||
@{{ group.domain }}
|
||||
</span>
|
||||
</span>
|
||||
<v-chip color="indigo" text-color="white">
|
||||
<v-avatar>
|
||||
<v-icon>group</v-icon>
|
||||
</v-avatar>
|
||||
Group
|
||||
</v-chip>
|
||||
</div>
|
||||
<v-card-text v-if="group.description" v-html="group.description"></v-card-text>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-layout>
|
||||
</v-card-media>
|
||||
<v-list three-line>
|
||||
<v-list-tile>
|
||||
<v-list-tile-action>
|
||||
|
@ -59,48 +79,90 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
<v-divider inset></v-divider>
|
||||
<v-list-tile v-if="group.address">
|
||||
<v-list-tile>
|
||||
<v-list-tile-action>
|
||||
<v-icon color="indigo">location_on</v-icon>
|
||||
</v-list-tile-action>
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>{{ group.address.streetAddress }}</v-list-tile-title>
|
||||
<v-list-tile-sub-title>{{ group.address.postalCode }} {{ group.address.locality }}</v-list-tile-sub-title>
|
||||
<v-list-tile-title>1400 Main Street</v-list-tile-title>
|
||||
<v-list-tile-sub-title>Orlando, FL 79938</v-list-tile-sub-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
</v-list>
|
||||
<v-container fluid grid-list-md v-if="group.members.length > 0">
|
||||
<v-subheader>Membres</v-subheader>
|
||||
<v-layout row>
|
||||
<v-flex xs2 v-for="member in group.members" :key="member.id">
|
||||
<router-link :to="{name: 'Account', params: {'id': member.account.id}}">
|
||||
<v-flex xs2 v-for="member in group.members" :key="member.actor.username">
|
||||
<router-link :to="{name: 'Account', params: { name: member.actor.username } }">
|
||||
<v-badge overlap>
|
||||
<span slot="badge" v-if="member.role == 3"><v-icon>stars</v-icon></span>
|
||||
<span slot="badge" v-if="member.role === 1"><v-icon>star_half</v-icon></span>
|
||||
<span slot="badge" v-if="member.role === 2"><v-icon>star</v-icon></span>
|
||||
<v-avatar size="75px">
|
||||
<img v-if="!member.account.avatar_url"
|
||||
<img v-if="!member.actor.avatar"
|
||||
class="img-circle elevation-7 mb-1"
|
||||
src="http://lorempixel.com/125/125/"
|
||||
src="https://picsum.photos/125/125/"
|
||||
>
|
||||
<img v-else
|
||||
class="img-circle elevation-7 mb-1"
|
||||
:src="member.account.avatar_url"
|
||||
:src="member.actor.avatar"
|
||||
>
|
||||
</v-avatar>
|
||||
</v-badge>
|
||||
</router-link>
|
||||
<span>{{ groupAccount.account.username }}</span>
|
||||
<span>{{ member.actor.username }}</span>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
<v-container fluid grid-list-md v-if="group.events.length > 0">
|
||||
<v-container fluid grid-list-md v-if="group.participatingEvents && group.participatingEvents.length > 0">
|
||||
<v-subheader>Participated at</v-subheader>
|
||||
<v-layout row wrap>
|
||||
<v-flex v-for="event in group.events" :key="event.id">
|
||||
<v-flex v-for="event in group.participatingEvents" :key="event.id">
|
||||
<v-card>
|
||||
<v-card-media
|
||||
class="black--text"
|
||||
height="200px"
|
||||
src="http://lorempixel.com/400/200/"
|
||||
src="https://picsum.photos/400/200/"
|
||||
>
|
||||
<v-container fill-height fluid>
|
||||
<v-layout fill-height>
|
||||
<v-flex xs12 align-end flexbox>
|
||||
<span class="headline">{{ event.title }}</span>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-card-media>
|
||||
<v-card-title>
|
||||
<div>
|
||||
<span class="grey--text">{{ event.startDate | formatDate }} à {{ event.location }}</span><br>
|
||||
<p>{{ event.description }}</p>
|
||||
<p v-if="event.organizer">Organisé par <router-link :to="{name: 'Account', params: {'id': event.organizer.id}}">{{ event.organizer.username }}</router-link></p>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon>
|
||||
<v-icon>favorite</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon>
|
||||
<v-icon>bookmark</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon>
|
||||
<v-icon>share</v-icon>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
<v-container fluid grid-list-md v-if="group.organizingEvents && group.organizingEvents.length > 0">
|
||||
<v-subheader>Organized events</v-subheader>
|
||||
<v-layout row wrap>
|
||||
<v-flex v-for="event in group.organizingEvents" :key="event.id">
|
||||
<v-card>
|
||||
<v-card-media
|
||||
class="black--text"
|
||||
height="200px"
|
||||
src="https://picsum.photos/400/200/"
|
||||
>
|
||||
<v-container fill-height fluid>
|
||||
<v-layout fill-height>
|
||||
|
@ -140,40 +202,39 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import eventFetch from '@/api/eventFetch';
|
||||
|
||||
export default {
|
||||
name: 'Group',
|
||||
props: ['id'],
|
||||
data() {
|
||||
return {
|
||||
group: {
|
||||
id: this.id,
|
||||
title: '',
|
||||
description: '',
|
||||
},
|
||||
group: null,
|
||||
loading: true,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchData() {
|
||||
eventFetch(`/groups/${this.id}`, this.$store)
|
||||
.then(response => response.json())
|
||||
.then((data) => {
|
||||
this.loading = false;
|
||||
this.group = data.data;
|
||||
});
|
||||
},
|
||||
deleteGroup() {
|
||||
const router = this.$router;
|
||||
eventFetch(`/groups/${this.id}`, this.$store, { method: 'DELETE' })
|
||||
.then(response => response.json())
|
||||
.then(() => router.push('/groups'));
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchData();
|
||||
},
|
||||
watch: {
|
||||
// call again the method if the route changes
|
||||
'$route': 'fetchData'
|
||||
},
|
||||
methods: {
|
||||
fetchData() {
|
||||
eventFetch(`/actors/${this.name}`, this.$store)
|
||||
.then(response => response.json())
|
||||
.then((response) => {
|
||||
this.group = response.data;
|
||||
this.loading = false;
|
||||
console.log(this.group);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -9,27 +9,26 @@
|
|||
<v-card-media
|
||||
class="black--text"
|
||||
height="200px"
|
||||
src="http://lorempixel.com/400/200/"
|
||||
src="https://picsum.photos/400/200/"
|
||||
>
|
||||
<v-container fill-height fluid>
|
||||
<v-layout fill-height>
|
||||
<v-flex xs12 align-end flexbox>
|
||||
<span class="headline">{{ group.title }}</span>
|
||||
<span class="headline">{{ group.username }}</span>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-card-media>
|
||||
<v-card-title>
|
||||
<div>
|
||||
<span class="grey--text">{{ group.startDate | formatDate }} à {{ group.location }}</span><br>
|
||||
<p>{{ group.description }}</p>
|
||||
<p>{{ group.summary }}</p>
|
||||
<p v-if="group.organizer">Organisé par <router-link :to="{name: 'Account', params: {'id': group.organizer.id}}">{{ group.organizer.username }}</router-link></p>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-card-actions>
|
||||
<v-btn flat color="green" @click="joinGroup(group.id)"><v-icon v-if="group.locked">lock</v-icon>Join</v-btn>
|
||||
<v-btn flat color="orange" @click="viewEvent(group.id)">Explore</v-btn>
|
||||
<v-btn flat color="red" @click="deleteEvent(group.id)">Delete</v-btn>
|
||||
<v-btn flat color="green" @click="joinGroup(group)"><v-icon v-if="group.locked">lock</v-icon>Join</v-btn>
|
||||
<v-btn flat color="orange" @click="viewActor(group)">Explore</v-btn>
|
||||
<v-btn flat color="red" @click="deleteGroup(group)">Delete</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
|
@ -53,28 +52,32 @@
|
|||
this.fetchData();
|
||||
},
|
||||
methods: {
|
||||
username_with_domain(actor) {
|
||||
return actor.username + (actor.domain === null ? '' : `@${actor.domain}`)
|
||||
},
|
||||
fetchData() {
|
||||
eventFetch('/groups', this.$store)
|
||||
.then(response => response.json())
|
||||
.then((data) => {
|
||||
console.log(data);
|
||||
this.loading = false;
|
||||
this.groups = data.data;
|
||||
});
|
||||
},
|
||||
deleteEvent(id) {
|
||||
deleteGroup(group) {
|
||||
const router = this.$router;
|
||||
eventFetch('/groups/' + id, this.$store, {'method': 'DELETE'})
|
||||
eventFetch(`/groups/${this.username_with_domain(group)}`, this.$store, {'method': 'DELETE'})
|
||||
.then(response => response.json())
|
||||
.then(() => router.push('/groups'));
|
||||
},
|
||||
viewEvent(id) {
|
||||
this.$router.push({ name: 'Group', params: { id } })
|
||||
viewActor(actor) {
|
||||
this.$router.push({ name: 'Group', params: { name: this.username_with_domain(actor) } })
|
||||
},
|
||||
joinGroup(id) {
|
||||
joinGroup(group) {
|
||||
const router = this.$router;
|
||||
eventFetch('/groups/' + id + '/join', this.$store)
|
||||
eventFetch(`/groups/${this.username_with_domain(group)}/join`, this.$store, { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(() => router.push('/group/' + id))
|
||||
.then(() => router.push({ name: 'Group', params: { name: this.username_with_domain(group) } }));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,10 +1,59 @@
|
|||
<template>
|
||||
<v-container>
|
||||
<h1 class="welcome" v-if="$store.state.user">{{ $t("home.welcome", { 'username': this.displayed_name }) }}</h1>
|
||||
<h1 class="welcome" v-else>{{ $t("home.welcome_off", { 'username': $store.state.user.username}) }}</h1>
|
||||
<router-link :to="{ name: 'EventList' }">{{ $t('home.events') }}</router-link>
|
||||
<router-link v-if="$store.state.user === false" :to="{ name: 'Login' }">{{ $t('home.login') }}</router-link>
|
||||
<router-link v-if="$store.state.user === false" :to="{ name: 'Register' }">{{ $t('home.register') }}</router-link>
|
||||
<v-jumbotron
|
||||
:gradient="gradient"
|
||||
src="https://picsum.photos/1200/900"
|
||||
dark
|
||||
v-if="$store.state.user === false"
|
||||
>
|
||||
<v-container fill-height>
|
||||
<v-layout align-center>
|
||||
<v-flex text-xs-center>
|
||||
<h1 class="display-3">Find events you like</h1>
|
||||
<h2>Share it with Eventos</h2>
|
||||
<v-btn>{{ $t("home.register") }}</v-btn>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-jumbotron>
|
||||
<v-layout>
|
||||
<v-flex xs12 sm8 offset-sm2>
|
||||
<v-card>
|
||||
<v-layout row wrap>
|
||||
<v-flex xs4 v-for="event in events" :key="event.uuid">
|
||||
<v-card :to="{ name: 'Event', params:{ uuid: event.uuid } }">
|
||||
<v-card-media v-if="!event.image"
|
||||
class="white--text"
|
||||
height="200px"
|
||||
src="https://picsum.photos/g/400/200/"
|
||||
>
|
||||
<v-container fill-height fluid>
|
||||
<v-layout fill-height>
|
||||
<v-flex xs12 align-end flexbox>
|
||||
<span class="headline black--text">{{ event.title }}</span>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-card-media>
|
||||
<v-card-title primary-title>
|
||||
<div>
|
||||
<span class="grey--text">{{ event.begins_on | formatDate }}</span><br>
|
||||
<router-link :to="{name: 'Account', params: { name: event.organizer.username } }">
|
||||
<v-avatar size="25px">
|
||||
<img class="img-circle elevation-7 mb-1"
|
||||
:src="event.organizer.avatar"
|
||||
>
|
||||
</v-avatar>
|
||||
</router-link>
|
||||
<span v-if="event.organizer">Organisé par {{ event.organizer.display_name }}</span>
|
||||
</div>
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
<v-layout row>
|
||||
<v-flex xs6>
|
||||
<v-btn large @click="geoLocalize"><v-icon>my_location</v-icon>Me géolocaliser</v-btn>
|
||||
|
@ -36,6 +85,7 @@ export default {
|
|||
name: 'Home',
|
||||
data() {
|
||||
return {
|
||||
gradient: 'to top right, rgba(63,81,181, .7), rgba(25,32,72, .7)',
|
||||
user: null,
|
||||
searchTerm: null,
|
||||
location_field: {
|
||||
|
@ -43,14 +93,15 @@ export default {
|
|||
search: null,
|
||||
},
|
||||
locations: [],
|
||||
events: [],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// this.fetchLocations();
|
||||
created() {
|
||||
this.fetchData();
|
||||
},
|
||||
computed: {
|
||||
displayed_name: function() {
|
||||
return this.$store.state.user.account.display_name === null ? this.$store.state.user.account.username : this.$store.state.user.account.display_name
|
||||
return this.$store.state.user.actor.display_name === null ? this.$store.state.user.actor.username : this.$store.state.user.actor.display_name
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -61,6 +112,14 @@ export default {
|
|||
this.locations = response;
|
||||
});
|
||||
},
|
||||
fetchData() {
|
||||
eventFetch('/events', this.$store)
|
||||
.then(response => response.json())
|
||||
.then((response) => {
|
||||
this.loading = false;
|
||||
this.events = response.data;
|
||||
});
|
||||
},
|
||||
geoLocalize() {
|
||||
const router = this.$router;
|
||||
if (sessionStorage.getItem('City')) {
|
||||
|
@ -85,6 +144,9 @@ export default {
|
|||
sessionStorage.setItem('City', geohash);
|
||||
this.$router.push({name: 'EventList', params: {location: geohash}});
|
||||
},
|
||||
viewEvent(event) {
|
||||
this.$router.push({ name: 'Event', params: { uuid: event.uuid } })
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<v-toolbar-title style="width: 300px" class="ml-0 pl-3">
|
||||
<v-toolbar-side-icon @click.stop="drawer = !drawer"></v-toolbar-side-icon>
|
||||
<router-link :to="{ name: 'Home' }">
|
||||
Libre-Event
|
||||
Eventos
|
||||
</router-link>
|
||||
</v-toolbar-title>
|
||||
<v-select
|
||||
|
@ -24,7 +24,22 @@
|
|||
:items="searchElement.items"
|
||||
:search-input.sync="search"
|
||||
v-model="searchSelect"
|
||||
></v-select>
|
||||
>
|
||||
<template slot="item" slot-scope="data">
|
||||
<template v-if="typeof data.item !== 'object'">
|
||||
<v-list-tile-content v-text="data.item"></v-list-tile-content>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-list-tile-avatar>
|
||||
<img :src="data.item.avatar">
|
||||
</v-list-tile-avatar>
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title v-html="username_with_domain(data.item)"></v-list-tile-title>
|
||||
<v-list-tile-sub-title v-html="data.item.type"></v-list-tile-sub-title>
|
||||
</v-list-tile-content>
|
||||
</template>
|
||||
</template>
|
||||
</v-select>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu
|
||||
offset-y
|
||||
|
@ -58,7 +73,7 @@
|
|||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
<v-btn flat @click="$router.push({name: 'Account', params: {'id': getUser().account.id}})" v-if="$store.state.user">{{ this.displayed_name }}</v-btn>
|
||||
<v-btn flat @click="$router.push({name: 'Account', params: { name: getUser().actor.username }})" v-if="$store.state.user">{{ this.displayed_name }}</v-btn>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
|
||||
|
@ -88,48 +103,62 @@
|
|||
},
|
||||
searchSelect(val) {
|
||||
console.log(val);
|
||||
if (val.hasOwnProperty('addressLocality')) {
|
||||
if (val.type === 'Event') {
|
||||
this.$router.push({name: 'Event', params: { name: val.organizer.username, slug: val.slug }});
|
||||
} else if (val.type === 'Locality') {
|
||||
this.$router.push({name: 'EventList', params: {location: val.geohash}});
|
||||
} else {
|
||||
this.$router.push({name: 'Account', params: {id: val.id}});
|
||||
this.$router.push({name: 'Account', params: { name : this.username_with_domain(val) }});
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayed_name: function() {
|
||||
return this.$store.state.user.account.display_name === null ? this.$store.state.user.account.username : this.$store.state.user.account.display_name
|
||||
return this.$store.state.user.actor.display_name === null ? this.$store.state.user.actor.username : this.$store.state.user.actor.display_name
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
username_with_domain(actor) {
|
||||
if (actor.type !== 'Event') {
|
||||
return actor.username + (actor.domain === null ? '' : `@${actor.domain}`)
|
||||
}
|
||||
return actor.title;
|
||||
},
|
||||
getUser() {
|
||||
return this.$store.state.user === undefined ? false : this.$store.state.user;
|
||||
},
|
||||
querySelections(searchTerm) {
|
||||
this.searchElement.loading = true;
|
||||
eventFetch('/find/', this.$store, {method: 'POST', body: JSON.stringify({search: searchTerm})})
|
||||
eventFetch(`/search/${searchTerm}`, this.$store)
|
||||
.then(response => response.json())
|
||||
.then((results) => {
|
||||
console.log('results');
|
||||
console.log(results);
|
||||
const accountResults = results.accounts.map((result) => {
|
||||
if (result.server) {
|
||||
result.displayedText = `${result.username}@${result.server.address}`;
|
||||
const accountResults = results.data.actors.map((result) => {
|
||||
if (result.domain) {
|
||||
result.displayedText = `${result.username}@${result.domain}`;
|
||||
} else {
|
||||
result.displayedText = result.username;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
const cities = new Set();
|
||||
const placeResults = results.places.map((result) => {
|
||||
result.displayedText = result.addressLocality;
|
||||
|
||||
const eventsResults = results.data.events.map((result) => {
|
||||
result.displayedText = result.title;
|
||||
return result;
|
||||
}).filter((result) => {
|
||||
if (cities.has(result.addressLocality)) {
|
||||
return false;
|
||||
}
|
||||
cities.add(result.addressLocality);
|
||||
return true;
|
||||
});
|
||||
this.searchElement.items = accountResults.concat(placeResults);
|
||||
// const cities = new Set();
|
||||
// const placeResults = results.places.map((result) => {
|
||||
// result.displayedText = result.addressLocality;
|
||||
// return result;
|
||||
// }).filter((result) => {
|
||||
// if (cities.has(result.addressLocality)) {
|
||||
// return false;
|
||||
// }
|
||||
// cities.add(result.addressLocality);
|
||||
// return true;
|
||||
// });
|
||||
this.searchElement.items = accountResults.concat(eventsResults);
|
||||
this.searchElement.loading = false;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<v-layout row>
|
||||
<v-flex xs12 sm6 offset-sm3>
|
||||
<h1>404 !</h1>
|
||||
<img src="../../static/oh_no.jpg" />
|
||||
<img src="../assets/oh_no.jpg" />
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export default {
|
||||
home: {
|
||||
welcome: 'Welcome on Libre-Event, {username}',
|
||||
welcome_off: 'Welcome on Libre-Event',
|
||||
welcome: 'Welcome on Eventos, {username}',
|
||||
welcome_off: 'Welcome on Eventos',
|
||||
events: 'Events',
|
||||
groups: 'Groups',
|
||||
login: 'Login',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export default {
|
||||
home: {
|
||||
welcome: 'Bienvenue sur Libre-Event, {username}!',
|
||||
welcome_off: 'Bienvenue sur Libre-Event',
|
||||
welcome: 'Bienvenue sur Eventos, {username}!',
|
||||
welcome_off: 'Bienvenue sur Eventos',
|
||||
events: 'Événements',
|
||||
groups: 'Groupes',
|
||||
login: 'Se connecter',
|
||||
|
|
|
@ -3,10 +3,12 @@
|
|||
import Vue from 'vue';
|
||||
// import * as VueGoogleMaps from 'vue2-google-maps';
|
||||
import VueMarkdown from 'vue-markdown';
|
||||
import VuetifyGoogleAutocomplete from 'vuetify-google-autocomplete';
|
||||
import Vuetify from 'vuetify';
|
||||
import Vuex from 'vuex';
|
||||
import moment from 'moment';
|
||||
import VuexI18n from 'vuex-i18n';
|
||||
import 'material-design-icons/iconfont/material-icons.css';
|
||||
import 'vuetify/dist/vuetify.min.css';
|
||||
import App from '@/App';
|
||||
import router from '@/router';
|
||||
|
@ -16,6 +18,10 @@ import auth from '@/auth';
|
|||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
Vue.use(VuetifyGoogleAutocomplete, {
|
||||
apiKey: 'AIzaSyBF37pw38j0giICt73TCAPNogc07Upe_Q4', // Can also be an object. E.g, for Google Maps Premium API, pass `{ client: <YOUR-CLIENT-ID> }`
|
||||
});
|
||||
|
||||
/*Vue.use(VueGoogleMaps, {
|
||||
load: {
|
||||
key: 'AIzaSyBF37pw38j0giICt73TCAPNogc07Upe_Q4',
|
||||
|
@ -31,6 +37,7 @@ let language = window.navigator.userLanguage || window.navigator.language;
|
|||
moment.locale(language);
|
||||
|
||||
Vue.filter('formatDate', value => (value ? moment(String(value)).format('LLLL') : null));
|
||||
Vue.filter('formatDay', value => (value ? moment(String(value)).format('LL') : null));
|
||||
|
||||
if (!(language in translations)) {
|
||||
[language] = language.split('-', 1);
|
||||
|
|
24
js/src/registerServiceWorker.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
/* eslint-disable no-console */
|
||||
|
||||
import { register } from 'register-service-worker';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||
ready() {
|
||||
console.log('App is being served from cache by a service worker.\n' +
|
||||
'For more details, visit https://goo.gl/AFskqB');
|
||||
},
|
||||
cached() {
|
||||
console.log('Content has been cached for offline use.');
|
||||
},
|
||||
updated() {
|
||||
console.log('New content is available; please refresh.');
|
||||
},
|
||||
offline() {
|
||||
console.log('No internet connection found. App is running in offline mode.');
|
||||
},
|
||||
error(error) {
|
||||
console.error('Error during service worker registration:', error);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -33,13 +33,6 @@ const router = new Router({
|
|||
component: EventList,
|
||||
meta: { requiredAuth: false },
|
||||
},
|
||||
{
|
||||
path: '/events/:id(\\d+)',
|
||||
name: 'Event',
|
||||
component: Event,
|
||||
props: true,
|
||||
meta: { requiredAuth: false },
|
||||
},
|
||||
{
|
||||
path: '/events/create',
|
||||
name: 'CreateEvent',
|
||||
|
@ -84,14 +77,7 @@ const router = new Router({
|
|||
meta: { requiredAuth: false },
|
||||
},
|
||||
{
|
||||
path: '/accounts/:id(\\d+)',
|
||||
name: 'Account',
|
||||
component: Account,
|
||||
props: true,
|
||||
meta: { requiredAuth: false },
|
||||
},
|
||||
{
|
||||
path: '/group',
|
||||
path: '/groups',
|
||||
name: 'GroupList',
|
||||
component: GroupList,
|
||||
meta: { requiredAuth: false },
|
||||
|
@ -103,12 +89,26 @@ const router = new Router({
|
|||
meta: { requiredAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/group/:id',
|
||||
path: '/~:name',
|
||||
name: 'Group',
|
||||
component: Group,
|
||||
props: true,
|
||||
meta: { requiredAuth: false },
|
||||
},
|
||||
{
|
||||
path: '/@:name',
|
||||
name: 'Account',
|
||||
component: Account,
|
||||
props: true,
|
||||
meta: { requiredAuth: false },
|
||||
},
|
||||
{
|
||||
path: '/events/:uuid',
|
||||
name: 'Event',
|
||||
component: Event,
|
||||
props: true,
|
||||
meta: { requiredAuth: false },
|
||||
},
|
||||
{ path: "*",
|
||||
name: 'PageNotFound',
|
||||
component: PageNotFound,
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
// A custom Nightwatch assertion.
|
||||
// the name of the method is the filename.
|
||||
// can be used in tests like this:
|
||||
//
|
||||
// browser.assert.elementCount(selector, count)
|
||||
//
|
||||
// for how to write custom assertions see
|
||||
// http://nightwatchjs.org/guide#writing-custom-assertions
|
||||
exports.assertion = function (selector, count) {
|
||||
this.message = 'Testing if element <' + selector + '> has count: ' + count;
|
||||
this.expected = count;
|
||||
this.pass = function (val) {
|
||||
return val === this.expected;
|
||||
}
|
||||
this.value = function (res) {
|
||||
return res.value;
|
||||
}
|
||||
this.command = function (cb) {
|
||||
var self = this;
|
||||
return this.api.execute(function (selector) {
|
||||
return document.querySelectorAll(selector).length;
|
||||
}, [selector], function (res) {
|
||||
cb.call(self, res);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
require('babel-register')
|
||||
var config = require('../../config')
|
||||
|
||||
// http://nightwatchjs.org/gettingstarted#settings-file
|
||||
module.exports = {
|
||||
src_folders: ['test/e2e/specs'],
|
||||
output_folder: 'test/e2e/reports',
|
||||
custom_assertions_path: ['test/e2e/custom-assertions'],
|
||||
|
||||
selenium: {
|
||||
start_process: true,
|
||||
server_path: require('selenium-server').path,
|
||||
host: '127.0.0.1',
|
||||
port: 4444,
|
||||
cli_args: {
|
||||
'webdriver.chrome.driver': require('chromedriver').path
|
||||
}
|
||||
},
|
||||
|
||||
test_settings: {
|
||||
default: {
|
||||
selenium_port: 4444,
|
||||
selenium_host: 'localhost',
|
||||
silent: true,
|
||||
globals: {
|
||||
devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port)
|
||||
}
|
||||
},
|
||||
|
||||
chrome: {
|
||||
desiredCapabilities: {
|
||||
browserName: 'chrome',
|
||||
javascriptEnabled: true,
|
||||
acceptSslCerts: true
|
||||
}
|
||||
},
|
||||
|
||||
firefox: {
|
||||
desiredCapabilities: {
|
||||
browserName: 'firefox',
|
||||
javascriptEnabled: true,
|
||||
acceptSslCerts: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
// 1. start the dev server using production config
|
||||
process.env.NODE_ENV = 'testing';
|
||||
var server = require('../../build/dev-server.js');
|
||||
|
||||
server.ready.then(() => {
|
||||
// 2. run the nightwatch test suite against it
|
||||
// to run in additional browsers:
|
||||
// 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings"
|
||||
// 2. add it to the --env flag below
|
||||
// or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
|
||||
// For more information on Nightwatch's config file, see
|
||||
// http://nightwatchjs.org/guide#settings-file
|
||||
var opts = process.argv.slice(2);
|
||||
if (opts.indexOf('--config') === -1) {
|
||||
opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']);
|
||||
}
|
||||
if (opts.indexOf('--env') === -1) {
|
||||
opts = opts.concat(['--env', 'chrome']);
|
||||
}
|
||||
|
||||
var spawn = require('cross-spawn');
|
||||
var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' });
|
||||
|
||||
runner.on('exit', function (code) {
|
||||
server.close();
|
||||
process.exit(code);
|
||||
});
|
||||
|
||||
runner.on('error', function (err) {
|
||||
server.close();
|
||||
throw err;
|
||||
});
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"globals": {
|
||||
"expect": true,
|
||||
"sinon": true
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
// require all test files (files that ends with .spec.js)
|
||||
const testsContext = require.context('./specs', true, /\.spec$/);
|
||||
testsContext.keys().forEach(testsContext);
|
||||
|
||||
// require all src files except main.js for coverage.
|
||||
// you can also change this to match only the subset of files that
|
||||
// you want coverage for.
|
||||
const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/);
|
||||
srcContext.keys().forEach(srcContext);
|
|
@ -1,33 +0,0 @@
|
|||
// This is a karma config file. For more details see
|
||||
// http://karma-runner.github.io/0.13/config/configuration-file.html
|
||||
// we are also using it with karma-webpack
|
||||
// https://github.com/webpack/karma-webpack
|
||||
|
||||
var webpackConfig = require('../../build/webpack.test.conf');
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
// to run in additional browsers:
|
||||
// 1. install corresponding karma launcher
|
||||
// http://karma-runner.github.io/0.13/config/browsers.html
|
||||
// 2. add it to the `browsers` array below.
|
||||
browsers: ['PhantomJS'],
|
||||
frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
|
||||
reporters: ['spec', 'coverage'],
|
||||
files: ['./index.js'],
|
||||
preprocessors: {
|
||||
'./index.js': ['webpack', 'sourcemap']
|
||||
},
|
||||
webpack: webpackConfig,
|
||||
webpackMiddleware: {
|
||||
noInfo: true,
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: './coverage',
|
||||
reporters: [
|
||||
{ type: 'lcov', subdir: '.' },
|
||||
{ type: 'text-summary' },
|
||||
]
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,11 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import Hello from '@/components/Home';
|
||||
|
||||
describe('Hello.vue', () => {
|
||||
it('should render correct contents', () => {
|
||||
const Constructor = Vue.extend(Hello);
|
||||
const vm = new Constructor().$mount();
|
||||
expect(vm.$el.querySelector('.hello h1').textContent)
|
||||
.to.equal('Welcome to Your Vue.js App');
|
||||
});
|
||||
});
|
19
js/tests/e2e/custom-assertions/elementCount.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
// A custom Nightwatch assertion.
|
||||
// The assertion name is the filename.
|
||||
// Example usage:
|
||||
//
|
||||
// browser.assert.elementCount(selector, count)
|
||||
//
|
||||
// For more information on custom assertions see:
|
||||
// http://nightwatchjs.org/guide#writing-custom-assertions
|
||||
|
||||
exports.assertion = function elementCount(selector, count) {
|
||||
this.message = `Testing if element <${selector}> has count: ${count}`;
|
||||
this.expected = count;
|
||||
this.pass = val => val === count;
|
||||
this.value = res => res.value;
|
||||
function evaluator(_selector) {
|
||||
return document.querySelectorAll(_selector).length;
|
||||
}
|
||||
this.command = cb => this.api.execute(evaluator, [selector], cb);
|
||||
};
|
|
@ -2,14 +2,9 @@
|
|||
// http://nightwatchjs.org/guide#usage
|
||||
|
||||
module.exports = {
|
||||
'default e2e tests': function test(browser) {
|
||||
// automatically uses dev Server port from /config.index.js
|
||||
// default: http://localhost:8080
|
||||
// see nightwatch.conf.js
|
||||
const devServer = browser.globals.devServerURL;
|
||||
|
||||
'default e2e tests': (browser) => {
|
||||
browser
|
||||
.url(devServer)
|
||||
.url(process.env.VUE_DEV_SERVER_URL)
|
||||
.waitForElementVisible('#app', 5000)
|
||||
.assert.elementPresent('.hello')
|
||||
.assert.containsText('h1', 'Welcome to Your Vue.js App')
|
8
js/tests/unit/.eslintrc.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
mocha: true
|
||||
},
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': 'off'
|
||||
}
|
||||
}
|
13
js/tests/unit/HelloWorld.spec.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { expect } from 'chai';
|
||||
import { shallow } from '@vue/test-utils';
|
||||
import HelloWorld from '@/components/HelloWorld.vue';
|
||||
|
||||
describe('HelloWorld.vue', () => {
|
||||
it('renders props.msg when passed', () => {
|
||||
const msg = 'new message';
|
||||
const wrapper = shallow(HelloWorld, {
|
||||
propsData: { msg },
|
||||
});
|
||||
expect(wrapper.text()).to.include(msg);
|
||||
});
|
||||
});
|
11
js/vue.config.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
const Dotenv = require('dotenv-webpack');
|
||||
|
||||
module.exports = {
|
||||
lintOnSave: false,
|
||||
compiler: true,
|
||||
configureWebpack: {
|
||||
plugins: [
|
||||
new Dotenv(),
|
||||
],
|
||||
},
|
||||
};
|
|
@ -1,45 +0,0 @@
|
|||
defmodule Eventos.Accounts.Account do
|
||||
@moduledoc """
|
||||
Represents an account (local and remote users)
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Eventos.Accounts.{Account, User}
|
||||
alias Eventos.Groups.{Group, Member, Request}
|
||||
alias Eventos.Events.Event
|
||||
|
||||
schema "accounts" do
|
||||
field :description, :string
|
||||
field :display_name, :string
|
||||
field :domain, :string
|
||||
field :private_key, :string
|
||||
field :public_key, :string
|
||||
field :suspended, :boolean, default: false
|
||||
field :uri, :string
|
||||
field :url, :string
|
||||
field :username, :string
|
||||
field :avatar_url, :string
|
||||
field :banner_url, :string
|
||||
has_many :organized_events, Event, [foreign_key: :organizer_account_id]
|
||||
many_to_many :groups, Group, join_through: Member
|
||||
has_many :group_request, Request
|
||||
has_one :user, User
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Account{} = account, attrs) do
|
||||
account
|
||||
|> cast(attrs, [:username, :domain, :display_name, :description, :private_key, :public_key, :suspended, :uri, :url, :avatar_url, :banner_url])
|
||||
|> validate_required([:username, :public_key, :suspended, :uri, :url])
|
||||
|> unique_constraint(:username, name: :accounts_username_domain_index)
|
||||
end
|
||||
|
||||
def registration_changeset(%Account{} = account, attrs) do
|
||||
account
|
||||
|> cast(attrs, [:username, :domain, :display_name, :description, :private_key, :public_key, :suspended, :uri, :url, :avatar_url, :banner_url])
|
||||
|> validate_required([:username, :public_key, :suspended, :uri, :url])
|
||||
|> unique_constraint(:username)
|
||||
end
|
||||
end
|
|
@ -1,290 +0,0 @@
|
|||
defmodule Eventos.Accounts do
|
||||
@moduledoc """
|
||||
The Accounts context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
import Exgravatar
|
||||
|
||||
alias Eventos.Repo
|
||||
alias Eventos.Accounts.Account
|
||||
|
||||
@doc """
|
||||
Returns the list of accounts.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_accounts()
|
||||
[%Account{}, ...]
|
||||
|
||||
"""
|
||||
def list_accounts do
|
||||
Repo.all(Account)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single account.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Account does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_account!(123)
|
||||
%Account{}
|
||||
|
||||
iex> get_account!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_account!(id) do
|
||||
Repo.get!(Account, id)
|
||||
end
|
||||
|
||||
def get_account_with_everything!(id) do
|
||||
account = Repo.get!(Account, id)
|
||||
Repo.preload(account, :organized_events)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a account.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_account(%{field: value})
|
||||
{:ok, %Account{}}
|
||||
|
||||
iex> create_account(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_account(attrs \\ %{}) do
|
||||
%Account{}
|
||||
|> Account.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a account.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_account(account, %{field: new_value})
|
||||
{:ok, %Account{}}
|
||||
|
||||
iex> update_account(account, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_account(%Account{} = account, attrs) do
|
||||
account
|
||||
|> Account.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a Account.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_account(account)
|
||||
{:ok, %Account{}}
|
||||
|
||||
iex> delete_account(account)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_account(%Account{} = account) do
|
||||
Repo.delete(account)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking account changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_account(account)
|
||||
%Ecto.Changeset{source: %Account{}}
|
||||
|
||||
"""
|
||||
def change_account(%Account{} = account) do
|
||||
Account.changeset(account, %{})
|
||||
end
|
||||
|
||||
alias Eventos.Accounts.User
|
||||
|
||||
@doc """
|
||||
Returns the list of users.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_users()
|
||||
[%User{}, ...]
|
||||
|
||||
"""
|
||||
def list_users do
|
||||
Repo.all(User)
|
||||
end
|
||||
|
||||
def list_users_with_accounts do
|
||||
users = Repo.all(User)
|
||||
Repo.preload(users, :account)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single user.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the User does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_user!(123)
|
||||
%User{}
|
||||
|
||||
iex> get_user!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_user!(id), do: Repo.get!(User, id)
|
||||
|
||||
def get_user_with_account!(id) do
|
||||
user = Repo.get!(User, id)
|
||||
Repo.preload(user, :account)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get an user by email
|
||||
"""
|
||||
def find_by_email(email) do
|
||||
user = Repo.get_by(User, email: email)
|
||||
Repo.preload(user, :account)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Authenticate user
|
||||
"""
|
||||
def authenticate(%{user: user, password: password}) do
|
||||
# Does password match the one stored in the database?
|
||||
case Comeonin.Argon2.checkpw(password, user.password_hash) do
|
||||
true ->
|
||||
# Yes, create and return the token
|
||||
EventosWeb.Guardian.encode_and_sign(user)
|
||||
_ ->
|
||||
# No, return an error
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetch gravatar url for email and set it as avatar if it exists
|
||||
"""
|
||||
defp gravatar(email) do
|
||||
url = gravatar_url(email, default: "404")
|
||||
case HTTPoison.get(url, [], [ssl: [{:versions, [:'tlsv1.2']}]]) do # See https://github.com/edgurgel/httpoison#note-about-broken-ssl-in-erlang-19
|
||||
{:ok, %HTTPoison.Response{status_code: 200}} ->
|
||||
url
|
||||
_ -> # User doesn't have a gravatar email, or other issues
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Register user
|
||||
"""
|
||||
def register(%{email: email, password: password, username: username}) do
|
||||
{:ok, {privkey, pubkey}} = RsaEx.generate_keypair("4096")
|
||||
|
||||
|
||||
avatar = gravatar(email)
|
||||
account = Eventos.Accounts.Account.registration_changeset(%Eventos.Accounts.Account{}, %{
|
||||
username: username,
|
||||
domain: nil,
|
||||
private_key: privkey,
|
||||
public_key: pubkey,
|
||||
uri: "h",
|
||||
url: "h",
|
||||
avatar_url: avatar,
|
||||
})
|
||||
|
||||
user = Eventos.Accounts.User.registration_changeset(%Eventos.Accounts.User{}, %{
|
||||
email: email,
|
||||
password: password
|
||||
})
|
||||
|
||||
|
||||
account_with_user = Ecto.Changeset.put_assoc(account, :user, user)
|
||||
|
||||
try do
|
||||
Eventos.Repo.insert!(account_with_user)
|
||||
user = find_by_email(email)
|
||||
{:ok, user}
|
||||
rescue
|
||||
e in Ecto.InvalidChangesetError ->
|
||||
{:error, e.changeset.changes.user.errors}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a user.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_user(%{field: value})
|
||||
{:ok, %User{}}
|
||||
|
||||
iex> create_user(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_user(attrs \\ %{}) do
|
||||
%User{}
|
||||
|> User.registration_changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a user.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_user(user, %{field: new_value})
|
||||
{:ok, %User{}}
|
||||
|
||||
iex> update_user(user, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_user(%User{} = user, attrs) do
|
||||
user
|
||||
|> User.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a User.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_user(user)
|
||||
{:ok, %User{}}
|
||||
|
||||
iex> delete_user(user)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_user(%User{} = user) do
|
||||
Repo.delete(user)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking user changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_user(user)
|
||||
%Ecto.Changeset{source: %User{}}
|
||||
|
||||
"""
|
||||
def change_user(%User{} = user) do
|
||||
User.changeset(user, %{})
|
||||
end
|
||||
end
|
7
lib/eventos/activity.ex
Normal file
|
@ -0,0 +1,7 @@
|
|||
defmodule Eventos.Activity do
|
||||
@moduledoc """
|
||||
Represents an activity
|
||||
"""
|
||||
|
||||
defstruct [:id, :data, :local, :actor, :recipients, :notifications]
|
||||
end
|
169
lib/eventos/actors/actor.ex
Normal file
|
@ -0,0 +1,169 @@
|
|||
defmodule Eventos.Actors.Actor.TitleSlug do
|
||||
@moduledoc """
|
||||
Slug generation for groups
|
||||
"""
|
||||
alias Eventos.Actors.Actor
|
||||
import Ecto.Query
|
||||
alias Eventos.Repo
|
||||
use EctoAutoslugField.Slug, from: :title, to: :slug
|
||||
|
||||
def build_slug(sources, changeset) do
|
||||
slug = super(sources, changeset)
|
||||
build_unique_slug(slug, changeset)
|
||||
end
|
||||
|
||||
defp build_unique_slug(slug, changeset) do
|
||||
query = from a in Actor,
|
||||
where: a.slug == ^slug
|
||||
|
||||
case Repo.one(query) do
|
||||
nil -> slug
|
||||
_story ->
|
||||
slug
|
||||
|> Eventos.Slug.increment_slug()
|
||||
|> build_unique_slug(changeset)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
import EctoEnum
|
||||
defenum Eventos.Actors.ActorTypeEnum, :actor_type, [:Person, :Application, :Group, :Organization, :Service]
|
||||
|
||||
|
||||
defmodule Eventos.Actors.Actor do
|
||||
@moduledoc """
|
||||
Represents an actor (local and remote actors)
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Eventos.Actors
|
||||
alias Eventos.Actors.{Actor, User, Follower, Member}
|
||||
alias Eventos.Events.Event
|
||||
alias Eventos.Service.ActivityPub
|
||||
|
||||
import Ecto.Query
|
||||
alias Eventos.Repo
|
||||
|
||||
import Logger
|
||||
|
||||
# @type t :: %Actor{description: String.t, id: integer(), inserted_at: DateTime.t, updated_at: DateTime.t, display_name: String.t, domain: String.t, private_key: String.t, public_key: String.t, suspended: boolean(), url: String.t, username: String.t, organized_events: list(), groups: list(), group_request: list(), user: User.t, field: ActorTypeEnum.t}
|
||||
|
||||
schema "actors" do
|
||||
field :url, :string
|
||||
field :outbox_url, :string
|
||||
field :inbox_url, :string
|
||||
field :following_url, :string
|
||||
field :followers_url, :string
|
||||
field :shared_inbox_url, :string
|
||||
field :type, Eventos.Actors.ActorTypeEnum
|
||||
field :name, :string
|
||||
field :domain, :string
|
||||
field :summary, :string
|
||||
field :preferred_username, :string
|
||||
field :public_key, :string
|
||||
field :private_key, :string
|
||||
field :manually_approves_followers, :boolean, default: false
|
||||
field :suspended, :boolean, default: false
|
||||
field :avatar_url, :string
|
||||
field :banner_url, :string
|
||||
many_to_many :followers, Actor, join_through: Follower
|
||||
has_many :organized_events, Event, [foreign_key: :organizer_actor_id]
|
||||
many_to_many :memberships, Actor, join_through: Member
|
||||
has_one :user, User
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Actor{} = actor, attrs) do
|
||||
actor
|
||||
|> Ecto.Changeset.cast(attrs, [:url, :outbox_url, :inbox_url, :shared_inbox_url, :following_url, :followers_url, :type, :name, :domain, :summary, :preferred_username, :public_key, :private_key, :manually_approves_followers, :suspended, :avatar_url, :banner_url])
|
||||
|> validate_required([:preferred_username, :public_key, :suspended, :url])
|
||||
|> unique_constraint(:prefered_username, name: :actors_preferred_username_domain_index)
|
||||
end
|
||||
|
||||
def registration_changeset(%Actor{} = actor, attrs) do
|
||||
actor
|
||||
|> Ecto.Changeset.cast(attrs, [:preferred_username, :domain, :name, :summary, :private_key, :public_key, :suspended, :url, :type])
|
||||
|> validate_required([:preferred_username, :public_key, :suspended, :url, :type])
|
||||
|> unique_constraint(:prefered_username, name: :actors_preferred_username_domain_index)
|
||||
end
|
||||
|
||||
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
|
||||
def remote_actor_creation(params) do
|
||||
changes =
|
||||
%Actor{}
|
||||
|> Ecto.Changeset.cast(params, [:url, :outbox_url, :inbox_url, :shared_inbox_url, :following_url, :followers_url, :type, :name, :domain, :summary, :preferred_username, :public_key, :manually_approves_followers, :avatar_url, :banner_url])
|
||||
|> validate_required([:url, :outbox_url, :inbox_url, :type, :name, :domain, :preferred_username, :public_key])
|
||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_index)
|
||||
|> validate_length(:summary, max: 5000)
|
||||
|> validate_length(:preferred_username, max: 100)
|
||||
|> put_change(:local, false)
|
||||
|
||||
Logger.debug("Remote actor creation")
|
||||
Logger.debug(inspect changes)
|
||||
changes
|
||||
end
|
||||
|
||||
def group_creation(%Actor{} = actor, params) do
|
||||
actor
|
||||
|> Ecto.Changeset.cast(params, [:url, :outbox_url, :inbox_url, :shared_inbox_url, :type, :name, :domain, :summary, :preferred_username, :avatar_url, :banner_url])
|
||||
|> put_change(:outbox_url, "#{EventosWeb.Endpoint.url()}/@#{params["prefered_username"]}/outbox")
|
||||
|> put_change(:inbox_url, "#{EventosWeb.Endpoint.url()}/@#{params["prefered_username"]}/inbox")
|
||||
|> put_change(:shared_inbox_url, "#{EventosWeb.Endpoint.url()}/inbox")
|
||||
|> put_change(:url, "#{EventosWeb.Endpoint.url()}/@#{params["prefered_username"]}")
|
||||
|> put_change(:domain, nil)
|
||||
|> put_change(:type, "Group")
|
||||
|> validate_required([:url, :outbox_url, :inbox_url, :type, :name, :preferred_username])
|
||||
|> validate_length(:summary, max: 5000)
|
||||
|> validate_length(:preferred_username, max: 100)
|
||||
|> put_change(:local, true)
|
||||
end
|
||||
|
||||
def get_or_fetch_by_url(url) do
|
||||
if user = Actors.get_actor_by_url(url) do
|
||||
user
|
||||
else
|
||||
case ActivityPub.make_actor_from_url(url) do
|
||||
{:ok, user} ->
|
||||
user
|
||||
_ -> {:error, "Could not fetch by AP id"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#@spec get_public_key_for_url(Actor.t) :: {:ok, String.t}
|
||||
def get_public_key_for_url(url) do
|
||||
with %Actor{} = actor <- get_or_fetch_by_url(url) do
|
||||
get_public_key_for_actor(actor)
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
#@spec get_public_key_for_actor(Actor.t) :: {:ok, String.t}
|
||||
def get_public_key_for_actor(%Actor{} = actor) do
|
||||
{:ok, actor.public_key}
|
||||
end
|
||||
|
||||
#@spec get_private_key_for_actor(Actor.t) :: {:ok, String.t}
|
||||
def get_private_key_for_actor(%Actor{} = actor) do
|
||||
actor.private_key
|
||||
end
|
||||
|
||||
def get_followers(%Actor{id: actor_id} = actor) do
|
||||
Repo.all(
|
||||
from a in Actor,
|
||||
join: f in Follower, on: a.id == f.actor_id,
|
||||
where: f.target_actor_id == ^actor_id
|
||||
)
|
||||
end
|
||||
|
||||
def get_followings(%Actor{id: actor_id} = actor) do
|
||||
Repo.all(
|
||||
from a in Actor,
|
||||
join: f in Follower, on: a.id == f.target_actor_id,
|
||||
where: f.actor_id == ^actor_id
|
||||
)
|
||||
end
|
||||
end
|
674
lib/eventos/actors/actors.ex
Normal file
|
@ -0,0 +1,674 @@
|
|||
defmodule Eventos.Actors do
|
||||
@moduledoc """
|
||||
The Actors context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Eventos.Repo
|
||||
|
||||
alias Eventos.Actors.Actor
|
||||
alias Eventos.Actors
|
||||
|
||||
alias Eventos.Service.ActivityPub
|
||||
|
||||
@doc """
|
||||
Returns the list of actors.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_actors()
|
||||
[%Actor{}, ...]
|
||||
|
||||
"""
|
||||
def list_actors do
|
||||
Repo.all(Actor)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single actor.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Actor does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_actor!(123)
|
||||
%Actor{}
|
||||
|
||||
iex> get_actor!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_actor!(id) do
|
||||
Repo.get!(Actor, id)
|
||||
end
|
||||
|
||||
def get_actor_with_everything!(id) do
|
||||
actor = Repo.get!(Actor, id)
|
||||
Repo.preload(actor, :organized_events)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a actor.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_actor(%{field: value})
|
||||
{:ok, %Actor{}}
|
||||
|
||||
iex> create_actor(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_actor(attrs \\ %{}) do
|
||||
%Actor{}
|
||||
|> Actor.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a actor.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_actor(actor, %{field: new_value})
|
||||
{:ok, %Actor{}}
|
||||
|
||||
iex> update_actor(actor, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_actor(%Actor{} = actor, attrs) do
|
||||
actor
|
||||
|> Actor.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a Actor.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_actor(actor)
|
||||
{:ok, %Actor{}}
|
||||
|
||||
iex> delete_actor(actor)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_actor(%Actor{} = actor) do
|
||||
Repo.delete(actor)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking actor changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_actor(actor)
|
||||
%Ecto.Changeset{source: %Actor{}}
|
||||
|
||||
"""
|
||||
def change_actor(%Actor{} = actor) do
|
||||
Actor.changeset(actor, %{})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a text representation of a local actor like user@domain.tld
|
||||
"""
|
||||
def actor_to_local_name_and_domain(actor) do
|
||||
"#{actor.preferred_username}@#{Application.get_env(:my, EventosWeb.Endpoint)[:url][:host]}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a webfinger representation of an actor
|
||||
"""
|
||||
def actor_to_webfinger_s(actor) do
|
||||
"acct:#{actor_to_local_name_and_domain(actor)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
List the groups
|
||||
"""
|
||||
def list_groups do
|
||||
Repo.all(from a in Actor, where: a.type == "Group")
|
||||
end
|
||||
|
||||
def get_group_by_name(name) do
|
||||
actor = case String.split(name, "@") do
|
||||
[name] ->
|
||||
Repo.get_by(Actor, preferred_username: name, type: :Group)
|
||||
[name, domain] ->
|
||||
Repo.get_by(Actor, preferred_username: name, domain: domain, type: :Group)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a group.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_group(%{field: value})
|
||||
{:ok, %Actor{}}
|
||||
|
||||
iex> create_group(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_group(attrs \\ %{}) do
|
||||
%Actor{}
|
||||
|> Actor.group_creation(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
alias Eventos.Actors.User
|
||||
|
||||
@doc """
|
||||
Returns the list of users.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_users()
|
||||
[%User{}, ...]
|
||||
|
||||
"""
|
||||
def list_users do
|
||||
Repo.all(User)
|
||||
end
|
||||
|
||||
def list_users_with_actors do
|
||||
users = Repo.all(User)
|
||||
Repo.preload(users, :actor)
|
||||
end
|
||||
|
||||
defp blank?(""), do: nil
|
||||
defp blank?(n), do: n
|
||||
|
||||
def insert_or_update_actor(data) do
|
||||
cs = Actor.remote_actor_creation(data)
|
||||
Repo.insert(cs, on_conflict: [set: [public_key: data.public_key, avatar_url: data.avatar_url, banner_url: data.banner_url, name: data.name]], conflict_target: [:preferred_username, :domain])
|
||||
end
|
||||
|
||||
# def increase_event_count(%Actor{} = actor) do
|
||||
# event_count = (actor.info["event_count"] || 0) + 1
|
||||
# new_info = Map.put(actor.info, "note_count", note_count)
|
||||
#
|
||||
# cs = info_changeset(actor, %{info: new_info})
|
||||
#
|
||||
# update_and_set_cache(cs)
|
||||
# end
|
||||
|
||||
def count_users() do
|
||||
Repo.one(
|
||||
from u in User,
|
||||
select: count(u.id)
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single user.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the User does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_user!(123)
|
||||
%User{}
|
||||
|
||||
iex> get_user!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_user!(id), do: Repo.get!(User, id)
|
||||
|
||||
def get_user_with_actor!(id) do
|
||||
user = Repo.get!(User, id)
|
||||
Repo.preload(user, :actor)
|
||||
end
|
||||
|
||||
def get_actor_by_url(url) do
|
||||
Repo.get_by(Actor, url: url)
|
||||
end
|
||||
|
||||
def get_actor_by_name(name) do
|
||||
actor = case String.split(name, "@") do
|
||||
[name] ->
|
||||
Repo.get_by(Actor, preferred_username: name)
|
||||
[name, domain] ->
|
||||
Repo.get_by(Actor, preferred_username: name, domain: domain)
|
||||
end
|
||||
end
|
||||
|
||||
def get_local_actor_by_name(name) do
|
||||
Repo.one from a in Actor, where: a.preferred_username == ^name and is_nil(a.domain)
|
||||
end
|
||||
|
||||
def get_local_actor_by_name_with_everything(name) do
|
||||
actor = Repo.one from a in Actor, where: a.preferred_username == ^name and is_nil(a.domain)
|
||||
Repo.preload(actor, :organized_events)
|
||||
end
|
||||
|
||||
def get_actor_by_name_with_everything(name) do
|
||||
actor = case String.split(name, "@") do
|
||||
[name] -> Repo.one from a in Actor, where: a.preferred_username == ^name and is_nil(a.domain)
|
||||
[name, domain] -> Repo.one from a in Actor, where: a.preferred_username == ^name and a.domain == ^domain
|
||||
end
|
||||
Repo.preload(actor, :organized_events)
|
||||
end
|
||||
|
||||
def get_or_fetch_by_url(url) do
|
||||
if actor = get_actor_by_url(url) do
|
||||
actor
|
||||
else
|
||||
ap_try = ActivityPub.make_actor_from_url(url)
|
||||
|
||||
case ap_try do
|
||||
{:ok, actor} ->
|
||||
actor
|
||||
|
||||
_ -> {:error, "Could not fetch by AP id"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Find local users by it's username
|
||||
"""
|
||||
def find_local_by_username(username) do
|
||||
actors = Repo.all from a in Actor, where: (ilike(a.preferred_username, ^like_sanitize(username)) or ilike(a.name, ^like_sanitize(username))) and is_nil(a.domain)
|
||||
Repo.preload(actors, :organized_events)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Find actors by their name or displayed name
|
||||
"""
|
||||
def find_actors_by_username(username) do
|
||||
Repo.all from a in Actor, where: ilike(a.preferred_username, ^like_sanitize(username)) or ilike(a.name, ^like_sanitize(username))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sanitize the LIKE queries
|
||||
"""
|
||||
defp like_sanitize(value) do
|
||||
"%" <> String.replace(value, ~r/([\\%_])/, "\\1") <> "%"
|
||||
end
|
||||
|
||||
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
|
||||
def search(name) do
|
||||
case find_actors_by_username(name) do # find already saved accounts
|
||||
[] ->
|
||||
with true <- Regex.match?(@email_regex, name), # no accounts found, let's test if it's an username@domain.tld
|
||||
{:ok, actor} <- ActivityPub.find_or_make_actor_from_nickname(name) do # creating the actor in that case
|
||||
{:ok, [actor]}
|
||||
else
|
||||
false -> {:ok, []}
|
||||
{:error, err} -> {:error, err} # error fingering the actor
|
||||
end
|
||||
actors = [_|_] ->
|
||||
{:ok, actors} # actors already saved found !
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get an user by email
|
||||
"""
|
||||
def find_by_email(email) do
|
||||
user = Repo.get_by(User, email: email)
|
||||
Repo.preload(user, :actor)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Authenticate user
|
||||
"""
|
||||
def authenticate(%{user: user, password: password}) do
|
||||
# Does password match the one stored in the database?
|
||||
case Comeonin.Argon2.checkpw(password, user.password_hash) do
|
||||
true ->
|
||||
# Yes, create and return the token
|
||||
EventosWeb.Guardian.encode_and_sign(user)
|
||||
_ ->
|
||||
# No, return an error
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Register user
|
||||
"""
|
||||
def register(%{email: email, password: password, username: username}) do
|
||||
#{:ok, {privkey, pubkey}} = RsaEx.generate_keypair("4096")
|
||||
{:ok, rsa_priv_key} = ExPublicKey.generate_key()
|
||||
{:ok, rsa_pub_key} = ExPublicKey.public_key_from_private_key(rsa_priv_key)
|
||||
|
||||
actor = Eventos.Actors.Actor.registration_changeset(%Eventos.Actors.Actor{}, %{
|
||||
preferred_username: username,
|
||||
domain: nil,
|
||||
private_key: rsa_priv_key |> ExPublicKey.pem_encode(),
|
||||
public_key: rsa_pub_key |> ExPublicKey.pem_encode(),
|
||||
url: EventosWeb.Endpoint.url() <> "/@" <> username,
|
||||
})
|
||||
|
||||
user = Eventos.Actors.User.registration_changeset(%Eventos.Actors.User{}, %{
|
||||
email: email,
|
||||
password: password
|
||||
})
|
||||
|
||||
|
||||
actor_with_user = Ecto.Changeset.put_assoc(actor, :user, user)
|
||||
|
||||
try do
|
||||
Eventos.Repo.insert!(actor_with_user)
|
||||
user = find_by_email(email)
|
||||
{:ok, user}
|
||||
rescue
|
||||
e in Ecto.InvalidChangesetError ->
|
||||
{:error, e.changeset.changes.user.errors}
|
||||
end
|
||||
end
|
||||
|
||||
def register_bot_account(%{name: name, summary: summary}) do
|
||||
key = :public_key.generate_key({:rsa, 2048, 65537})
|
||||
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
||||
pem = :public_key.pem_encode([entry]) |> String.trim_trailing()
|
||||
|
||||
{:ok, rsa_priv_key} = ExPublicKey.generate_key()
|
||||
{:ok, rsa_pub_key} = ExPublicKey.public_key_from_private_key(rsa_priv_key)
|
||||
|
||||
actor = Eventos.Actors.Actor.registration_changeset(%Eventos.Actors.Actor{}, %{
|
||||
preferred_username: name,
|
||||
domain: nil,
|
||||
private_key: pem,
|
||||
public_key: "toto",
|
||||
url: EventosWeb.Endpoint.url() <> "/@" <> name,
|
||||
summary: summary,
|
||||
type: :Service
|
||||
})
|
||||
|
||||
try do
|
||||
Eventos.Repo.insert!(actor)
|
||||
rescue
|
||||
e in Ecto.InvalidChangesetError ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@doc """
|
||||
Creates a user.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_user(%{field: value})
|
||||
{:ok, %User{}}
|
||||
|
||||
iex> create_user(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_user(attrs \\ %{}) do
|
||||
%User{}
|
||||
|> User.registration_changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a user.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_user(user, %{field: new_value})
|
||||
{:ok, %User{}}
|
||||
|
||||
iex> update_user(user, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_user(%User{} = user, attrs) do
|
||||
user
|
||||
|> User.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a User.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_user(user)
|
||||
{:ok, %User{}}
|
||||
|
||||
iex> delete_user(user)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_user(%User{} = user) do
|
||||
Repo.delete(user)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking user changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_user(user)
|
||||
%Ecto.Changeset{source: %User{}}
|
||||
|
||||
"""
|
||||
def change_user(%User{} = user) do
|
||||
User.changeset(user, %{})
|
||||
end
|
||||
|
||||
alias Eventos.Actors.Member
|
||||
|
||||
@doc """
|
||||
Returns the list of members.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_members()
|
||||
[%Member{}, ...]
|
||||
|
||||
"""
|
||||
def list_members do
|
||||
Repo.all(Member)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single member.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Member does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_member!(123)
|
||||
%Member{}
|
||||
|
||||
iex> get_member!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_member!(id), do: Repo.get!(Member, id)
|
||||
|
||||
@doc """
|
||||
Creates a member.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_member(%{field: value})
|
||||
{:ok, %Member{}}
|
||||
|
||||
iex> create_member(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_member(attrs \\ %{}) do
|
||||
%Member{}
|
||||
|> Member.changeset(attrs)
|
||||
|> Repo.insert!()
|
||||
|> Repo.preload([:actor, :parent])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a member.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_member(member, %{field: new_value})
|
||||
{:ok, %Member{}}
|
||||
|
||||
iex> update_member(member, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_member(%Member{} = member, attrs) do
|
||||
member
|
||||
|> Member.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a Member.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_member(member)
|
||||
{:ok, %Member{}}
|
||||
|
||||
iex> delete_member(member)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_member(%Member{} = member) do
|
||||
Repo.delete(member)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking member changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_member(member)
|
||||
%Ecto.Changeset{source: %Member{}}
|
||||
|
||||
"""
|
||||
def change_member(%Member{} = member) do
|
||||
Member.changeset(member, %{})
|
||||
end
|
||||
|
||||
def groups_for_actor(%Actor{id: id} = _actor) do
|
||||
Repo.all(
|
||||
from m in Member,
|
||||
where: m.actor_id == ^id,
|
||||
preload: [:parent]
|
||||
)
|
||||
end
|
||||
|
||||
def members_for_group(%Actor{type: :Group, id: id} = _group) do
|
||||
Repo.all(
|
||||
from m in Member,
|
||||
where: m.parent_id == ^id,
|
||||
preload: [:parent, :actor]
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
alias Eventos.Actors.Bot
|
||||
|
||||
@doc """
|
||||
Returns the list of bots.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_bots()
|
||||
[%Bot{}, ...]
|
||||
|
||||
"""
|
||||
def list_bots do
|
||||
Repo.all(Bot)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single bot.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Bot does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_bot!(123)
|
||||
%Bot{}
|
||||
|
||||
iex> get_bot!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_bot!(id), do: Repo.get!(Bot, id)
|
||||
|
||||
@spec get_bot_by_actor(Actor.t) :: Bot.t
|
||||
def get_bot_by_actor(%Actor{} = actor) do
|
||||
Repo.get_by!(Bot, actor_id: actor.id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a bot.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_bot(%{field: value})
|
||||
{:ok, %Bot{}}
|
||||
|
||||
iex> create_bot(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_bot(attrs \\ %{}) do
|
||||
%Bot{}
|
||||
|> Bot.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a bot.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_bot(bot, %{field: new_value})
|
||||
{:ok, %Bot{}}
|
||||
|
||||
iex> update_bot(bot, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_bot(%Bot{} = bot, attrs) do
|
||||
bot
|
||||
|> Bot.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a Bot.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_bot(bot)
|
||||
{:ok, %Bot{}}
|
||||
|
||||
iex> delete_bot(bot)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_bot(%Bot{} = bot) do
|
||||
Repo.delete(bot)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking bot changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_bot(bot)
|
||||
%Ecto.Changeset{source: %Bot{}}
|
||||
|
||||
"""
|
||||
def change_bot(%Bot{} = bot) do
|
||||
Bot.changeset(bot, %{})
|
||||
end
|
||||
end
|
25
lib/eventos/actors/bot.ex
Normal file
|
@ -0,0 +1,25 @@
|
|||
defmodule Eventos.Actors.Bot do
|
||||
@moduledoc """
|
||||
Represents a local bot
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Eventos.Actors.{Actor, User, Bot}
|
||||
|
||||
|
||||
schema "bots" do
|
||||
field :source, :string
|
||||
field :type, :string, default: :ics
|
||||
belongs_to :actor, Actor
|
||||
belongs_to :user, User
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(bot, attrs) do
|
||||
bot
|
||||
|> cast(attrs, [:source, :type, :actor_id, :user_id])
|
||||
|> validate_required([:source])
|
||||
end
|
||||
end
|
26
lib/eventos/actors/follower.ex
Normal file
|
@ -0,0 +1,26 @@
|
|||
defmodule Eventos.Actors.Follower do
|
||||
@moduledoc """
|
||||
Represents the following of an actor to another actor
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Eventos.Actors.Follower
|
||||
alias Eventos.Actors.Actor
|
||||
|
||||
|
||||
schema "followers" do
|
||||
field :approved, :boolean, default: false
|
||||
field :score, :integer, default: 1000
|
||||
belongs_to :target_actor, Actor
|
||||
belongs_to :actor, Actor
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Follower{} = member, attrs) do
|
||||
member
|
||||
|> cast(attrs, [:role, :approved, :target_actor_id, :actor_id])
|
||||
|> validate_required([:role, :approved, :target_actor_id, :actor_id])
|
||||
end
|
||||
end
|
26
lib/eventos/actors/member.ex
Normal file
|
@ -0,0 +1,26 @@
|
|||
defmodule Eventos.Actors.Member do
|
||||
@moduledoc """
|
||||
Represents the membership of an actor to a group
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Eventos.Actors.Member
|
||||
alias Eventos.Actors.Actor
|
||||
|
||||
@primary_key false
|
||||
schema "members" do
|
||||
field :approved, :boolean, default: true
|
||||
field :role, :integer, default: 0 # 0 : Member, 1 : Moderator, 2 : Admin
|
||||
belongs_to :parent, Actor
|
||||
belongs_to :actor, Actor
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Member{} = member, attrs) do
|
||||
member
|
||||
|> cast(attrs, [:role, :approved, :parent_id, :actor_id])
|
||||
|> validate_required([:parent_id, :actor_id])
|
||||
end
|
||||
end
|
|
@ -1,17 +1,17 @@
|
|||
defmodule Eventos.Accounts.User do
|
||||
defmodule Eventos.Actors.User do
|
||||
@moduledoc """
|
||||
Represents a local user
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Eventos.Accounts.{Account, User}
|
||||
alias Eventos.Actors.{Actor, User}
|
||||
|
||||
schema "users" do
|
||||
field :email, :string
|
||||
field :password_hash, :string
|
||||
field :password, :string, virtual: true
|
||||
field :role, :integer, default: 0
|
||||
belongs_to :account, Account
|
||||
belongs_to :actor, Actor
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
@ -19,7 +19,7 @@ defmodule Eventos.Accounts.User do
|
|||
@doc false
|
||||
def changeset(%User{} = user, attrs) do
|
||||
user
|
||||
|> cast(attrs, [:email, :role, :password_hash, :account_id])
|
||||
|> cast(attrs, [:email, :role, :password_hash, :actor_id])
|
||||
|> validate_required([:email])
|
||||
|> unique_constraint(:email)
|
||||
|> validate_format(:email, ~r/@/)
|
|
@ -26,6 +26,5 @@ defmodule Eventos.Addresses.Address do
|
|||
def changeset(%Address{} = address, attrs) do
|
||||
address
|
||||
|> cast(attrs, [:description, :floor, :geom, :addressCountry, :addressLocality, :addressRegion, :postalCode, :streetAddress])
|
||||
|> validate_required([:geom])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,7 +17,8 @@ defmodule Eventos.Application do
|
|||
supervisor(EventosWeb.Endpoint, []),
|
||||
# Start your own worker by calling: Eventos.Worker.start_link(arg1, arg2, arg3)
|
||||
# worker(Eventos.Worker, [arg1, arg2, arg3]),
|
||||
worker(Guardian.DB.Token.SweeperServer, [])
|
||||
worker(Guardian.DB.Token.SweeperServer, []),
|
||||
worker(Eventos.Service.Federator, []),
|
||||
]
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
|
|
27
lib/eventos/events/comment.ex
Normal file
|
@ -0,0 +1,27 @@
|
|||
defmodule Eventos.Events.Comment do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Eventos.Events.Event
|
||||
alias Eventos.Actors.Actor
|
||||
alias Eventos.Actors.Comment
|
||||
|
||||
schema "comments" do
|
||||
field :text, :string
|
||||
field :url, :string
|
||||
field :local, :boolean, default: true
|
||||
belongs_to :actor, Actor, [foreign_key: :actor_id]
|
||||
belongs_to :event, Event, [foreign_key: :event_id]
|
||||
belongs_to :in_reply_to_comment, Comment, [foreign_key: :in_reply_to_comment_id]
|
||||
belongs_to :origin_comment, Comment, [foreign_key: :origin_comment_id]
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(comment, attrs) do
|
||||
comment
|
||||
|> cast(attrs, [:url, :text, :actor_id, :event_id, :in_reply_to_comment_id])
|
||||
|> validate_required([:url, :text, :actor_id])
|
||||
end
|
||||
end
|
|
@ -32,13 +32,14 @@ defmodule Eventos.Events.Event do
|
|||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Eventos.Events.{Event, Participant, Request, Tag, Category, Session, Track}
|
||||
alias Eventos.Events.{Event, Participant, Tag, Category, Session, Track}
|
||||
alias Eventos.Events.Event.TitleSlug
|
||||
alias Eventos.Accounts.Account
|
||||
alias Eventos.Groups.Group
|
||||
alias Eventos.Actors.Actor
|
||||
alias Eventos.Addresses.Address
|
||||
|
||||
schema "events" do
|
||||
field :url, :string
|
||||
field :local, :boolean, default: true
|
||||
field :begins_on, Timex.Ecto.DateTimeWithTimezone
|
||||
field :description, :string
|
||||
field :ends_on, Timex.Ecto.DateTimeWithTimezone
|
||||
|
@ -50,27 +51,31 @@ defmodule Eventos.Events.Event do
|
|||
field :thumbnail, :string
|
||||
field :large_image, :string
|
||||
field :publish_at, Timex.Ecto.DateTimeWithTimezone
|
||||
belongs_to :organizer_account, Account, [foreign_key: :organizer_account_id]
|
||||
belongs_to :organizer_group, Group, [foreign_key: :organizer_group_id]
|
||||
field :uuid, Ecto.UUID, default: Ecto.UUID.generate()
|
||||
belongs_to :organizer_actor, Actor, [foreign_key: :organizer_actor_id]
|
||||
many_to_many :tags, Tag, join_through: "events_tags"
|
||||
belongs_to :category, Category
|
||||
many_to_many :participants, Account, join_through: Participant
|
||||
has_many :event_request, Request
|
||||
many_to_many :participants, Actor, join_through: Participant
|
||||
has_many :tracks, Track
|
||||
has_many :sessions, Session
|
||||
belongs_to :address, Address
|
||||
|
||||
timestamps()
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Event{} = event, attrs) do
|
||||
event
|
||||
|> cast(attrs, [:title, :description, :begins_on, :ends_on, :organizer_account_id, :organizer_group_id, :category_id, :state, :status, :public, :thumbnail, :large_image, :publish_at])
|
||||
changeset = event
|
||||
|> cast(attrs, [:title, :description, :url, :begins_on, :ends_on, :organizer_actor_id, :category_id, :state, :status, :public, :thumbnail, :large_image, :publish_at])
|
||||
|> cast_assoc(:tags)
|
||||
|> cast_assoc(:address)
|
||||
|> validate_required([:title, :description, :begins_on, :ends_on, :organizer_account_id, :category_id])
|
||||
|> validate_required([:title, :description, :begins_on, :ends_on, :organizer_actor_id, :category_id])
|
||||
|> TitleSlug.maybe_generate_slug()
|
||||
|> TitleSlug.unique_constraint()
|
||||
|> put_change(:uuid, Ecto.UUID.generate())
|
||||
|
||||
import Logger
|
||||
Logger.debug(inspect changeset)
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,7 +7,8 @@ defmodule Eventos.Events do
|
|||
alias Eventos.Repo
|
||||
|
||||
alias Eventos.Events.Event
|
||||
alias Eventos.Accounts.Account
|
||||
alias Eventos.Events.Comment
|
||||
alias Eventos.Actors.Actor
|
||||
|
||||
@doc """
|
||||
Returns the list of events.
|
||||
|
@ -19,7 +20,38 @@ defmodule Eventos.Events do
|
|||
|
||||
"""
|
||||
def list_events do
|
||||
Repo.all(Event)
|
||||
events = Repo.all(Event)
|
||||
Repo.preload(events, [:organizer_actor])
|
||||
end
|
||||
|
||||
def get_events_for_actor(%Actor{id: actor_id} = _actor, page \\ 1, limit \\ 10) do
|
||||
start = (page - 1) * limit
|
||||
|
||||
query = from e in Event,
|
||||
where: e.organizer_actor_id == ^actor_id,
|
||||
limit: ^limit,
|
||||
order_by: [desc: :id],
|
||||
offset: ^start,
|
||||
preload: [:organizer_actor, :category, :sessions, :tracks, :tags, :participants, :address]
|
||||
events = Repo.all(query)
|
||||
count_events = Repo.one(from e in Event, select: count(e.id), where: e.organizer_actor_id == ^actor_id)
|
||||
{:ok, events, count_events}
|
||||
end
|
||||
|
||||
def count_local_events do
|
||||
Repo.one(
|
||||
from e in Event,
|
||||
select: count(e.id),
|
||||
where: e.local == ^true
|
||||
)
|
||||
end
|
||||
|
||||
def count_local_comments do
|
||||
Repo.one(
|
||||
from c in Comment,
|
||||
select: count(c.id),
|
||||
where: c.local == ^true
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -38,12 +70,73 @@ defmodule Eventos.Events do
|
|||
"""
|
||||
def get_event!(id), do: Repo.get!(Event, id)
|
||||
|
||||
@doc """
|
||||
Gets an event by it's URL
|
||||
"""
|
||||
def get_event_by_url!(url) do
|
||||
Repo.get_by(Event, url: url)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets an event by it's UUID
|
||||
"""
|
||||
def get_event_by_uuid(uuid) do
|
||||
Repo.get_by(Event, uuid: uuid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single event, with all associations loaded.
|
||||
"""
|
||||
def get_event_full!(id) do
|
||||
event = Repo.get!(Event, id)
|
||||
Repo.preload(event, [:organizer_account, :organizer_group, :category, :sessions, :tracks, :tags, :participants, :address])
|
||||
Repo.preload(event, [:organizer_actor, :category, :sessions, :tracks, :tags, :participants, :address])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets an event by it's URL
|
||||
"""
|
||||
def get_event_full_by_url!(url) do
|
||||
event = Repo.get_by(Event, url: url)
|
||||
Repo.preload(event, [:organizer_actor, :category, :sessions, :tracks, :tags, :participants, :address])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a full event by it's UUID
|
||||
"""
|
||||
def get_event_full_by_uuid(uuid) do
|
||||
event = Repo.get_by(Event, uuid: uuid)
|
||||
Repo.preload(event, [:organizer_actor, :category, :sessions, :tracks, :tags, :participants, :address])
|
||||
end
|
||||
|
||||
@spec get_event_full_by_name_and_slug!(String.t, String.t) :: Event.t
|
||||
def get_event_full_by_name_and_slug!(name, slug) do
|
||||
query = case String.split(name, "@") do
|
||||
[name, domain] -> from e in Event,
|
||||
join: a in Actor,
|
||||
on: a.id == e.organizer_actor_id and a.preferred_username == ^name and a.domain == ^domain,
|
||||
where: e.slug == ^slug
|
||||
[name] -> from e in Event,
|
||||
join: a in Actor,
|
||||
on: a.id == e.organizer_actor_id and a.preferred_username == ^name and is_nil(a.domain),
|
||||
where: e.slug == ^slug
|
||||
end
|
||||
event = Repo.one(query)
|
||||
Repo.preload(event, [:organizer_actor, :category, :sessions, :tracks, :tags, :participants, :address])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Find events by name
|
||||
"""
|
||||
def find_events_by_name(name) do
|
||||
events = Repo.all from a in Event, where: ilike(a.title, ^like_sanitize(name))
|
||||
Repo.preload(events, [:organizer_actor])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sanitize the LIKE queries
|
||||
"""
|
||||
defp like_sanitize(value) do
|
||||
"%" <> String.replace(value, ~r/([\\%_])/, "\\1") <> "%"
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -61,7 +154,8 @@ defmodule Eventos.Events do
|
|||
def create_event(attrs \\ %{}) do
|
||||
%Event{}
|
||||
|> Event.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
|> Repo.insert!()
|
||||
|> Repo.preload([:organizer_actor])
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -142,6 +236,11 @@ defmodule Eventos.Events do
|
|||
"""
|
||||
def get_category!(id), do: Repo.get!(Category, id)
|
||||
|
||||
@spec get_category_by_title(String.t) :: tuple()
|
||||
def get_category_by_title(title) when is_binary(title) do
|
||||
Repo.get_by(Category, title: title)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a category.
|
||||
|
||||
|
@ -332,8 +431,8 @@ defmodule Eventos.Events do
|
|||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_participant!(event_id, account_id) do
|
||||
Repo.get_by!(Participant, [event_id: event_id, account_id: account_id])
|
||||
def get_participant!(event_id, actor_id) do
|
||||
Repo.get_by!(Participant, [event_id: event_id, actor_id: actor_id])
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -401,104 +500,8 @@ defmodule Eventos.Events do
|
|||
Participant.changeset(participant, %{})
|
||||
end
|
||||
|
||||
alias Eventos.Events.Request
|
||||
|
||||
@doc """
|
||||
Returns the list of requests.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_requests()
|
||||
[%Request{}, ...]
|
||||
|
||||
"""
|
||||
def list_requests do
|
||||
Repo.all(Request)
|
||||
end
|
||||
|
||||
def list_requests_for_account(%Account{} = account) do
|
||||
Repo.all(from r in Request, where: r.account_id == ^account.id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single request.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Request does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_request!(123)
|
||||
%Request{}
|
||||
|
||||
iex> get_request!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_request!(id), do: Repo.get!(Request, id)
|
||||
|
||||
@doc """
|
||||
Creates a request.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_request(%{field: value})
|
||||
{:ok, %Request{}}
|
||||
|
||||
iex> create_request(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_request(attrs \\ %{}) do
|
||||
%Request{}
|
||||
|> Request.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a request.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_request(request, %{field: new_value})
|
||||
{:ok, %Request{}}
|
||||
|
||||
iex> update_request(request, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_request(%Request{} = request, attrs) do
|
||||
request
|
||||
|> Request.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a Request.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_request(request)
|
||||
{:ok, %Request{}}
|
||||
|
||||
iex> delete_request(request)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_request(%Request{} = request) do
|
||||
Repo.delete(request)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking request changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_request(request)
|
||||
%Ecto.Changeset{source: %Request{}}
|
||||
|
||||
"""
|
||||
def change_request(%Request{} = request) do
|
||||
Request.changeset(request, %{})
|
||||
def list_requests_for_actor(%Actor{} = actor) do
|
||||
Repo.all(from p in Participant, where: p.actor_id == ^actor.id and p.approved == false)
|
||||
end
|
||||
|
||||
alias Eventos.Events.Session
|
||||
|
@ -706,4 +709,100 @@ defmodule Eventos.Events do
|
|||
def change_track(%Track{} = track) do
|
||||
Track.changeset(track, %{})
|
||||
end
|
||||
|
||||
alias Eventos.Events.Comment
|
||||
|
||||
@doc """
|
||||
Returns the list of comments.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_comments()
|
||||
[%Comment{}, ...]
|
||||
|
||||
"""
|
||||
def list_comments do
|
||||
Repo.all(Comment)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single comment.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Comment does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_comment!(123)
|
||||
%Comment{}
|
||||
|
||||
iex> get_comment!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_comment!(id), do: Repo.get!(Comment, id)
|
||||
|
||||
@doc """
|
||||
Creates a comment.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_comment(%{field: value})
|
||||
{:ok, %Comment{}}
|
||||
|
||||
iex> create_comment(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_comment(attrs \\ %{}) do
|
||||
%Comment{}
|
||||
|> Comment.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a comment.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_comment(comment, %{field: new_value})
|
||||
{:ok, %Comment{}}
|
||||
|
||||
iex> update_comment(comment, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_comment(%Comment{} = comment, attrs) do
|
||||
comment
|
||||
|> Comment.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a Comment.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_comment(comment)
|
||||
{:ok, %Comment{}}
|
||||
|
||||
iex> delete_comment(comment)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_comment(%Comment{} = comment) do
|
||||
Repo.delete(comment)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking comment changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_comment(comment)
|
||||
%Ecto.Changeset{source: %Comment{}}
|
||||
|
||||
"""
|
||||
def change_comment(%Comment{} = comment) do
|
||||
Comment.changeset(comment, %{})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
defmodule Eventos.Events.Participant do
|
||||
@moduledoc """
|
||||
Represents a participant, an account participating to an event
|
||||
Represents a participant, an actor participating to an event
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Eventos.Events.{Participant, Event}
|
||||
alias Eventos.Accounts.Account
|
||||
alias Eventos.Actors.Actor
|
||||
|
||||
@primary_key false
|
||||
schema "participants" do
|
||||
field :role, :integer
|
||||
field :role, :integer, default: 0 # 0 : participant, 1 : moderator, 2 : administrator, 3 : creator
|
||||
field :approved, :boolean
|
||||
belongs_to :event, Event, primary_key: true
|
||||
belongs_to :account, Account, primary_key: true
|
||||
belongs_to :actor, Actor, primary_key: true
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
@ -19,7 +20,7 @@ defmodule Eventos.Events.Participant do
|
|||
@doc false
|
||||
def changeset(%Participant{} = participant, attrs) do
|
||||
participant
|
||||
|> cast(attrs, [:role, :event_id, :account_id])
|
||||
|> validate_required([:role, :event_id, :account_id])
|
||||
|> cast(attrs, [:role, :event_id, :actor_id])
|
||||
|> validate_required([:role, :event_id, :actor_id])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
defmodule Eventos.Events.Request do
|
||||
@moduledoc """
|
||||
Represents an account request to join an event
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Eventos.Events.{Request, Event}
|
||||
alias Eventos.Accounts.Account
|
||||
|
||||
schema "event_requests" do
|
||||
field :state, :integer
|
||||
belongs_to :event, Event
|
||||
belongs_to :account, Account
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Request{} = request, attrs) do
|
||||
request
|
||||
|> cast(attrs, [:state])
|
||||
|> validate_required([:state])
|
||||
end
|
||||
end
|
|
@ -11,7 +11,8 @@ defmodule Eventos.Export.ICalendar do
|
|||
summary: event.title,
|
||||
dtstart: event.begins_on,
|
||||
dtend: event.ends_on,
|
||||
description: event.description
|
||||
description: event.description,
|
||||
uid: event.uuid
|
||||
}]
|
||||
%ICalendar{events: events}
|
||||
|> ICalendar.to_ics()
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
defmodule Eventos.Groups.Group.TitleSlug do
|
||||
@moduledoc """
|
||||
Slug generation for groups
|
||||
"""
|
||||
alias Eventos.Groups.Group
|
||||
import Ecto.Query
|
||||
alias Eventos.Repo
|
||||
use EctoAutoslugField.Slug, from: :title, to: :slug
|
||||
|
||||
def build_slug(sources, changeset) do
|
||||
slug = super(sources, changeset)
|
||||
build_unique_slug(slug, changeset)
|
||||
end
|
||||
|
||||
defp build_unique_slug(slug, changeset) do
|
||||
query = from g in Group,
|
||||
where: g.slug == ^slug
|
||||
|
||||
case Repo.one(query) do
|
||||
nil -> slug
|
||||
_story ->
|
||||
slug
|
||||
|> Eventos.Slug.increment_slug()
|
||||
|> build_unique_slug(changeset)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Eventos.Groups.Group do
|
||||
@moduledoc """
|
||||
Represents a group
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Eventos.Events.Event
|
||||
alias Eventos.Groups.{Group, Member, Request}
|
||||
alias Eventos.Accounts.Account
|
||||
alias Eventos.Groups.Group.TitleSlug
|
||||
alias Eventos.Addresses.Address
|
||||
|
||||
schema "groups" do
|
||||
field :description, :string
|
||||
field :suspended, :boolean, default: false
|
||||
field :title, :string
|
||||
field :slug, TitleSlug.Type
|
||||
field :uri, :string
|
||||
field :url, :string
|
||||
many_to_many :members, Account, join_through: Member
|
||||
has_many :organized_events, Event, [foreign_key: :organizer_group_id]
|
||||
has_many :requests, Request
|
||||
belongs_to :address, Address
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Group{} = group, attrs) do
|
||||
group
|
||||
|> cast(attrs, [:title, :description, :suspended, :url, :uri, :address_id])
|
||||
|> validate_required([:title, :description, :suspended, :url, :uri])
|
||||
|> TitleSlug.maybe_generate_slug()
|
||||
|> TitleSlug.unique_constraint()
|
||||
end
|
||||
end
|
|
@ -1,304 +0,0 @@
|
|||
defmodule Eventos.Groups do
|
||||
@moduledoc """
|
||||
The Groups context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Eventos.Repo
|
||||
|
||||
alias Eventos.Groups.Group
|
||||
|
||||
@doc """
|
||||
Returns the list of groups.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_groups()
|
||||
[%Group{}, ...]
|
||||
|
||||
"""
|
||||
def list_groups do
|
||||
Repo.all(Group)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single group.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Group does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_group!(123)
|
||||
%Group{}
|
||||
|
||||
iex> get_group!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_group!(id), do: Repo.get!(Group, id)
|
||||
|
||||
@doc """
|
||||
Gets a single group, with all associations loaded.
|
||||
"""
|
||||
def get_group_full!(id) do
|
||||
group = Repo.get!(Group, id)
|
||||
Repo.preload(group, [:members, :organized_events])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a group.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_group(%{field: value})
|
||||
{:ok, %Group{}}
|
||||
|
||||
iex> create_group(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_group(attrs \\ %{}) do
|
||||
%Group{}
|
||||
|> Group.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a group.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_group(group, %{field: new_value})
|
||||
{:ok, %Group{}}
|
||||
|
||||
iex> update_group(group, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_group(%Group{} = group, attrs) do
|
||||
group
|
||||
|> Group.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a Group.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_group(group)
|
||||
{:ok, %Group{}}
|
||||
|
||||
iex> delete_group(group)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_group(%Group{} = group) do
|
||||
Repo.delete(group)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking group changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_group(group)
|
||||
%Ecto.Changeset{source: %Group{}}
|
||||
|
||||
"""
|
||||
def change_group(%Group{} = group) do
|
||||
Group.changeset(group, %{})
|
||||
end
|
||||
|
||||
alias Eventos.Groups.Member
|
||||
|
||||
@doc """
|
||||
Returns the list of members.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_members()
|
||||
[%Member{}, ...]
|
||||
|
||||
"""
|
||||
def list_members do
|
||||
Repo.all(Member)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single member.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Member does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_member!(123)
|
||||
%Member{}
|
||||
|
||||
iex> get_member!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_member!(id), do: Repo.get!(Member, id)
|
||||
|
||||
@doc """
|
||||
Creates a member.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_member(%{field: value})
|
||||
{:ok, %Member{}}
|
||||
|
||||
iex> create_member(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_member(attrs \\ %{}) do
|
||||
%Member{}
|
||||
|> Member.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a member.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_member(member, %{field: new_value})
|
||||
{:ok, %Member{}}
|
||||
|
||||
iex> update_member(member, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_member(%Member{} = member, attrs) do
|
||||
member
|
||||
|> Member.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a Member.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_member(member)
|
||||
{:ok, %Member{}}
|
||||
|
||||
iex> delete_member(member)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_member(%Member{} = member) do
|
||||
Repo.delete(member)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking member changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_member(member)
|
||||
%Ecto.Changeset{source: %Member{}}
|
||||
|
||||
"""
|
||||
def change_member(%Member{} = member) do
|
||||
Member.changeset(member, %{})
|
||||
end
|
||||
|
||||
alias Eventos.Groups.Request
|
||||
|
||||
@doc """
|
||||
Returns the list of requests.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_requests()
|
||||
[%Request{}, ...]
|
||||
|
||||
"""
|
||||
def list_requests do
|
||||
Repo.all(Request)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single request.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Request does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_request!(123)
|
||||
%Request{}
|
||||
|
||||
iex> get_request!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_request!(id), do: Repo.get!(Request, id)
|
||||
|
||||
@doc """
|
||||
Creates a request.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_request(%{field: value})
|
||||
{:ok, %Request{}}
|
||||
|
||||
iex> create_request(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_request(attrs \\ %{}) do
|
||||
%Request{}
|
||||
|> Request.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a request.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_request(request, %{field: new_value})
|
||||
{:ok, %Request{}}
|
||||
|
||||
iex> update_request(request, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_request(%Request{} = request, attrs) do
|
||||
request
|
||||
|> Request.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a Request.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_request(request)
|
||||
{:ok, %Request{}}
|
||||
|
||||
iex> delete_request(request)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_request(%Request{} = request) do
|
||||
Repo.delete(request)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking request changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_request(request)
|
||||
%Ecto.Changeset{source: %Request{}}
|
||||
|
||||
"""
|
||||
def change_request(%Request{} = request) do
|
||||
Request.changeset(request, %{})
|
||||
end
|
||||
end
|
|
@ -1,25 +0,0 @@
|
|||
defmodule Eventos.Groups.Member do
|
||||
@moduledoc """
|
||||
Represents the membership of an account to a group
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Eventos.Groups.{Member, Group}
|
||||
alias Eventos.Accounts.Account
|
||||
|
||||
|
||||
schema "members" do
|
||||
field :role, :integer
|
||||
belongs_to :group, Group
|
||||
belongs_to :account, Account
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Member{} = member, attrs) do
|
||||
member
|
||||
|> cast(attrs, [:role])
|
||||
|> validate_required([:role])
|
||||
end
|
||||
end
|
|
@ -1,24 +0,0 @@
|
|||
defmodule Eventos.Groups.Request do
|
||||
@moduledoc """
|
||||
Represents a group request, when an user wants to be member of a group
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Eventos.Groups.Request
|
||||
|
||||
|
||||
schema "group_requests" do
|
||||
field :state, :integer
|
||||
field :group_id, :integer
|
||||
field :account_id, :integer
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Request{} = request, attrs) do
|
||||
request
|
||||
|> cast(attrs, [:state])
|
||||
|> validate_required([:state])
|
||||
end
|
||||
end
|
|
@ -1,41 +0,0 @@
|
|||
defmodule EventosWeb.AccountController do
|
||||
@moduledoc """
|
||||
Controller for Accounts
|
||||
"""
|
||||
use EventosWeb, :controller
|
||||
|
||||
alias Eventos.Accounts
|
||||
alias Eventos.Accounts.Account
|
||||
|
||||
action_fallback EventosWeb.FallbackController
|
||||
|
||||
def index(conn, _params) do
|
||||
accounts = Accounts.list_accounts()
|
||||
render(conn, "index.json", accounts: accounts)
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
account = Accounts.get_account_with_everything!(id)
|
||||
render(conn, "show.json", account: account)
|
||||
end
|
||||
|
||||
def update(conn, %{"id" => id, "account" => account_params}) do
|
||||
account = Accounts.get_account!(id)
|
||||
|
||||
with {:ok, %Account{} = account} <- Accounts.update_account(account, account_params) do
|
||||
render(conn, "show.json", account: account)
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id_str}) do
|
||||
{id, _} = Integer.parse(id_str)
|
||||
if Guardian.Plug.current_resource(conn).account.id == id do
|
||||
account = Accounts.get_account!(id)
|
||||
with {:ok, %Account{}} <- Accounts.delete_account(account) do
|
||||
send_resp(conn, :no_content, "")
|
||||
end
|
||||
else
|
||||
send_resp(conn, 401, "")
|
||||
end
|
||||
end
|
||||
end
|