Merge branch 'activitypub' into 'master'

Activitypub

See merge request tcit/eventos!12
This commit is contained in:
Thomas Citharel 2018-06-06 18:28:59 +02:00
commit 48a9e3d8cb
163 changed files with 13352 additions and 8720 deletions

View File

@ -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"],

View File

@ -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,

View File

@ -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"
]
}

View File

@ -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
View File

@ -0,0 +1,3 @@
API_HOST=localhost
API_ORIGIN=http://localhost:4001
API_PATH=/api/v1

3
js/.env.dist Normal file
View File

@ -0,0 +1,3 @@
API_HOST=event.tcit.fr
API_ORIGIN=https://event.tcit.fr
API_PATH=/api/v1

View File

@ -1,2 +0,0 @@
build/*.js
config/*.js

View File

@ -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
View File

@ -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

View File

@ -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: {}
}
}

View File

@ -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).

View File

@ -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'
))
})
})

View File

@ -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)
}
}

View File

@ -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()
}
})

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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'
}
}

View File

@ -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]')
}
}
]
}
}

View File

@ -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()
]
})

View File

@ -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

View File

@ -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

View File

@ -1,6 +0,0 @@
var merge = require('webpack-merge')
var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})

View File

@ -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
}
}

View File

@ -1,3 +0,0 @@
module.exports = {
NODE_ENV: '"production"'
}

View File

@ -1,6 +0,0 @@
var merge = require('webpack-merge')
var devEnv = require('./dev.env')
module.exports = merge(devEnv, {
NODE_ENV: '"testing"'
})

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View 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
View 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
View 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"
}

View File

@ -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"

View File

@ -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;

View File

@ -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;

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -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 {

View File

@ -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);
})
}
}

View File

@ -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}`,
};
}
},
},
};

View File

@ -70,7 +70,6 @@
<vuetify-google-autocomplete
id="map"
append-icon="search"
classname="form-control"
placeholder="Start typing"
label="Location"
enable-geolocation

View File

@ -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&#45;&#45;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();
},

View File

@ -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&#45;&#45;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'});

View File

@ -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"-->
<!--&gt;-->
<!--</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)"-->
<!--&gt;</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: [],
};

View File

@ -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>

View File

@ -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) } }));
}
},
};

View File

@ -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>

View File

@ -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;
});
}

View File

@ -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>

View File

@ -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',

View File

@ -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',

View File

@ -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);

View 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);
},
});
}

View File

@ -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,

View File

View File

@ -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);
});
}
}

View File

@ -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
}
}
}
}

View File

@ -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;
});
});

View File

@ -1,9 +0,0 @@
{
"env": {
"mocha": true
},
"globals": {
"expect": true,
"sinon": true
}
}

View File

@ -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);

View File

@ -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' },
]
},
});
};

View File

@ -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');
});
});

View 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);
};

View File

@ -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')

View File

@ -0,0 +1,8 @@
module.exports = {
env: {
mocha: true
},
rules: {
'import/no-extraneous-dependencies': 'off'
}
}

View 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
View File

@ -0,0 +1,11 @@
const Dotenv = require('dotenv-webpack');
module.exports = {
lintOnSave: false,
compiler: true,
configureWebpack: {
plugins: [
new Dotenv(),
],
},
};

View File

@ -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

View File

@ -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
View 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
View 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

View 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
View 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

View 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

View 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

View File

@ -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/@/)

View File

@ -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

View File

@ -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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

Some files were not shown because too many files have changed in this diff Show More