This commit is contained in:
Dan Elbert 2018-06-09 12:36:46 -05:00
parent cd6f408d39
commit 46e8c9376f
24 changed files with 401 additions and 43 deletions

View File

@ -6,11 +6,6 @@ class UsersController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:verify_login]
def show
if current_user
render json: { id: current_user.id, name: current_user.display_name, admin: current_user.admin? }
else
render json: nil
end
end
def login
@ -48,11 +43,15 @@ class UsersController < ApplicationController
def create
@user = User.new(user_params)
respond_to do |format|
if @user.save
set_current_user(@user)
redirect_to root_path, notice: 'User was successfully created.'
format.html { redirect_to root_path, notice: 'User created.' }
format.json { render :show, status: :created, location: @user }
else
render action: :new
format.html { render :new }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end
@ -62,10 +61,15 @@ class UsersController < ApplicationController
def update
@user = current_user
respond_to do |format|
if @user.update(user_params)
redirect_to root_path, notice: 'User account updated'
format.html { redirect_to root_path, notice: 'User updated.' }
format.json { render :show, status: :created, location: @user }
else
render action: 'edit'
format.html { render :edit }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end

View File

@ -33,12 +33,6 @@
})
},
methods: {
...mapMutations([
'setUser'
])
},
watch: {
isLoading(val) {
if (val) {
@ -51,7 +45,7 @@
created() {
if (this.user === null && this.authChecked === false) {
this.loadResource(api.getCurrentUser().then(user => this.setUser(user)));
this.checkAuthentication();
}
// Hard coded values taken directly from Bulma css

View File

@ -13,14 +13,14 @@
</div>
<div class="navbar-menu" :class="{ 'is-active': menuActive}">
<div class="navbar-start">
<a v-if="updateAvailable" href="#" @click.prevent="updateApp">UPDATE AVAILABLE!</a>
<a class="navbar-item" v-if="updateAvailable" href="#" @click.prevent="updateApp">UPDATE AVAILABLE!</a>
<router-link to="/" class="navbar-item">Recipes</router-link>
<router-link to="/ingredients" class="navbar-item">Ingredients</router-link>
<router-link to="/calculator" class="navbar-item">Calculator</router-link>
<router-link v-if="isLoggedIn" to="/logs" class="navbar-item">Log</router-link>
<router-link v-if="isLoggedIn" to="/notes" class="navbar-item">Notes</router-link>
<router-link to="/about" class="navbar-item">About</router-link>
<a v-if="isAdmin" class="navbar-item" href="/admin/users">Admin</a>
<router-link v-if="isAdmin" to="/admin/users" class="navbar-item">Admin</router-link>
</div>
<div class="navbar-end">
@ -30,14 +30,17 @@
{{ user.name }}
</a>
<div class="navbar-dropdown is-boxed">
<a class="navbar-item" href="#">
<router-link to="/user/edit" class="navbar-item">
Profile
</a>
</router-link>
<router-link to="/logout" class="navbar-item">Logout</router-link>
</div>
</div>
<div v-else>
<user-login></user-login>
<user-login class="navbar-link"></user-login>
<div class="navbar-dropdown is-boxed">
<router-link to="/user/new" class="navbar-item">Create Account</router-link>
</div>
</div>
</div>

View File

@ -0,0 +1,21 @@
<template>
<div>
<div class="notification is-danger" v-if="errors !== null" v-for="(errs, prop) in errors">
{{ prop }}: {{ errs.join(", ") }}
</div>
</div>
</template>
<script>
export default {
props: {
errors: {
required: false,
type: Object,
default: {}
}
}
}
</script>

View File

@ -2,6 +2,8 @@
<div>
<h1 class="title">{{action}} {{ingredient.name || "[Unnamed Ingredient]"}}</h1>
<app-validation-errors :errors="validationErrors"></app-validation-errors>
<div class="field">
<label class="label is-small-mobile">Name</label>
<div class="control">
@ -143,6 +145,11 @@
required: true,
type: Object
},
validationErrors: {
required: false,
type: Object,
default: {}
},
action: {
required: false,
type: String,

View File

@ -4,6 +4,8 @@
<h1 class="title">Creating Log for {{ log.recipe.name }}</h1>
<app-validation-errors :errors="validationErrors"></app-validation-errors>
<div class="columns">
<div class="column">
<app-date-picker v-model="log.date" label="Date"></app-date-picker>
@ -38,7 +40,12 @@
log: {
required: true,
type: Object
}
},
validationErrors: {
required: false,
type: Object,
default: {}
},
},
components: {

View File

@ -0,0 +1,15 @@
<template>
</template>
<script>
export default {
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,55 @@
<template>
<div>
<h1 class="title">User List</h1>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Admin?</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="u in userList" :key="u.id">
<td>{{u.name}}</td>
<td>{{u.email}}</td>
<td>{{u.admin}}</td>
<td>
<button type="button" class="button is-danger">X</button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import api from "../lib/Api";
export default {
data() {
return {
userList: []
};
},
created() {
this.loadResource(
api.getAdminUserList()
.then(list => this.userList = list)
);
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -1,7 +1,7 @@
<template>
<div>
<ingredient-edit :ingredient="ingredient" action="Creating"></ingredient-edit>
<ingredient-edit :ingredient="ingredient" :validation-errors="validationErrors" action="Creating"></ingredient-edit>
<button type="button" class="button is-primary" @click="save">Save</button>
<router-link class="button is-secondary" to="/ingredients">Cancel</router-link>
@ -51,12 +51,13 @@
lipids: null,
ingredient_units: []
},
validationErrors: null
validationErrors: {}
}
},
methods: {
save() {
this.validationErrors = {}
this.loadResource(
api.postIngredient(this.ingredient)
.then(() => this.$router.push('/ingredients'))

View File

@ -5,7 +5,7 @@
Loading...
</div>
<div v-else>
<ingredient-edit :ingredient="ingredient"></ingredient-edit>
<ingredient-edit :ingredient="ingredient" :validation-errors="validationErrors"></ingredient-edit>
</div>
<button type="button" class="button is-primary" @click="save">Save</button>
@ -25,7 +25,7 @@
data: function () {
return {
ingredient: null,
validationErrors: null
validationErrors: {}
};
},
@ -37,6 +37,7 @@
methods: {
save() {
this.validationErrors = {};
this.loadResource(
api.patchIngredient(this.ingredient)
.then(() => this.$router.push({name: 'ingredient', params: {id: this.ingredientId }}))

View File

@ -1,7 +1,7 @@
<template>
<div>
<log-edit v-if="log.recipe !== null" :log="log">
<log-edit v-if="log.recipe !== null" :log="log" :validation-errors="validationErrors">
<div class="buttons">
<button type="button" class="button is-primary" @click="save">Save Log</button>
<router-link class="button is-secondary" to="/">Cancel</router-link>
@ -26,7 +26,7 @@
export default {
data() {
return {
validationErrors: [],
validationErrors: {},
log: {
date: null,
rating: null,
@ -45,6 +45,7 @@
methods: {
save() {
this.log.original_recipe_id = this.recipeId;
this.validationErrors = {};
this.loadResource(
api.postLog(this.log)
@ -56,7 +57,7 @@
created() {
this.loadResource(
api.getRecipe(this.recipeId, data => this.log.recipe = data)
api.getRecipe(this.recipeId, null, null, null, data => this.log.recipe = data)
);
},

View File

@ -1,7 +1,7 @@
<template>
<div>
<log-edit v-if="log !== null" :log="log">
<log-edit v-if="log !== null" :log="log" :validation-errors="validationErrors">
<div class="buttons">
<button type="button" class="button is-primary" @click="save">Save Log</button>
<router-link class="button is-secondary" to="/">Cancel</router-link>
@ -26,7 +26,7 @@
export default {
data() {
return {
validationErrors: [],
validationErrors: {},
log: null
}
},
@ -39,6 +39,7 @@
methods: {
save() {
this.validationErrors = {};
this.loadResource(
api.patchLog(this.log)
.then(() => this.$router.push('/'))

View File

@ -3,6 +3,8 @@
<h1 class="title">Creating {{ recipe.name || "[Unamed Recipe]" }}</h1>
<app-validation-errors :errors="validationErrors"></app-validation-errors>
<recipe-edit :recipe="recipe" action="Creating"></recipe-edit>
<button type="button" class="button is-primary" @click="save">Save</button>
@ -20,7 +22,7 @@
export default {
data() {
return {
validationErrors: [],
validationErrors: {},
recipe: {
name: null,
source: null,
@ -37,6 +39,7 @@
methods: {
save() {
this.validationErrors = {};
this.loadResource(
api.postRecipe(this.recipe)
.then(() => this.$router.push('/'))

View File

@ -6,6 +6,9 @@
</div>
<div v-else>
<h1 class="title">Editing {{ recipe.name || "[Unamed Recipe]" }}</h1>
<app-validation-errors :errors="validationErrors"></app-validation-errors>
<recipe-edit :recipe="recipe"></recipe-edit>
</div>
@ -26,7 +29,7 @@
data: function () {
return {
recipe: null,
validationErrors: []
validationErrors: {}
}
},
@ -38,6 +41,7 @@
methods: {
save() {
this.validationErrors = {};
this.loadResource(
api.patchRecipe(this.recipe)
.then(() => this.$router.push({name: 'recipe', params: {id: this.recipeId }}))

View File

@ -0,0 +1,57 @@
<template>
<div>
<h1 class="title">Create New User</h1>
<app-validation-errors :errors="validationErrors"></app-validation-errors>
<user-edit :user-obj="userObj" action="Creating"></user-edit>
<button type="button" class="button is-primary" @click="save">Save</button>
<router-link class="button is-secondary" to="/">Cancel</router-link>
</div>
</template>
<script>
import UserEdit from "./UserEdit";
import api from "../lib/Api";
import * as Errors from '../lib/Errors';
export default {
data() {
return {
validationErrors: {},
userObj: {
username: '',
full_name: '',
email: '',
password: '',
password_confirmation: ''
}
}
},
methods: {
save() {
this.validationErrors = {};
this.loadResource(
api.postUser(this.userObj)
.then(() => this.checkAuthentication())
.then(() => this.$router.push('/'))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
);
}
},
components: {
UserEdit
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,77 @@
<template>
<div>
<h1 class="title">Edit User</h1>
<app-validation-errors :errors="validationErrors"></app-validation-errors>
<user-edit v-if="userObj != null" :user-obj="userObj" action="Creating"></user-edit>
<button type="button" class="button is-primary" @click="save">Save</button>
<router-link class="button is-secondary" to="/">Cancel</router-link>
</div>
</template>
<script>
import UserEdit from "./UserEdit";
import api from "../lib/Api";
import * as Errors from '../lib/Errors';
export default {
data() {
return {
validationErrors: {},
userObj: null
}
},
created() {
this.refreshUser();
},
watch: {
user() {
this.refreshUser();
}
},
methods: {
refreshUser() {
if (this.user) {
this.userObj = {
username: this.user.username,
full_name: this.user.full_name,
email: this.user.email,
password: '',
password_confirmation: ''
};
} else {
this.userObj = null;
}
},
save() {
this.validationErrors = {};
this.loadResource(
api.patchUser(this.userObj)
.then(() => this.checkAuthentication())
.then(() => {
this.$router.push('/');
})
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
);
}
},
components: {
UserEdit
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,26 @@
<template>
<div>
<app-text-field label="Username" v-model="userObj.username"></app-text-field>
<app-text-field label="Name" v-model="userObj.full_name"></app-text-field>
<app-text-field label="Email" v-model="userObj.email"></app-text-field>
<app-text-field label="Password" v-model="userObj.password"></app-text-field>
<app-text-field label="Password Confirmation" v-model="userObj.password_confirmation"></app-text-field>
</div>
</template>
<script>
export default {
props: {
userObj: {
required: true,
type: Object
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -357,6 +357,38 @@ class Api {
return this.patch("/logs/" + log.id, this.buildLogParams(log));
}
getAdminUserList() {
return this.get("/admin/users");
}
postUser(userObj) {
const params = {
user: {
username: userObj.username,
full_name: userObj.full_name,
email: userObj.email,
password: userObj.password,
password_confirmation: userObj.password_confirmation
}
};
return this.post("/user/", params);
}
patchUser(userObj) {
const params = {
user: {
username: userObj.username,
full_name: userObj.full_name,
email: userObj.email,
password: userObj.password,
password_confirmation: userObj.password_confirmation
}
};
return this.patch("/user/", params);
}
postLogin(username, password) {
const params = {
username: username,

View File

@ -42,13 +42,13 @@ ApiValidationError.prototype = Object.assign(new ApiError(), {
name: "ApiValidationError",
validationErrors: function() {
const errors = new Map();
const errors = {};
if (this.json) {
for (let key in this.json) {
errors.set(key, this.json[key]);
errors[key] = this.json[key];
}
} else {
errors.set("base", ["unknown error"]);
errors["base"] = ["unknown error"];
}
return errors;
},

View File

@ -1,6 +1,7 @@
import Vue from 'vue';
import { mapGetters, mapMutations, mapState } from 'vuex';
import api from "../lib/Api";
Vue.mixin({
computed: {
@ -16,7 +17,8 @@ Vue.mixin({
methods: {
...mapMutations([
'setError',
'setLoading'
'setLoading',
'setUser'
]),
loadResource(promise) {
@ -25,6 +27,10 @@ Vue.mixin({
return promise
.catch(err => this.setError(err))
.then(() => this.setLoading(false));
},
checkAuthentication() {
return this.loadResource(api.getCurrentUser().then(user => this.setUser(user)));
}
}
});

View File

@ -21,6 +21,7 @@ import AppPager from "../components/AppPager";
import AppRating from "../components/AppRating";
import AppTagEditor from "../components/AppTagEditor";
import AppTextField from "../components/AppTextField";
import AppValidationErrors from "../components/AppValidationErrors";
Vue.component("AppAutocomplete", AppAutocomplete);
Vue.component("AppConfirm", AppConfirm);
@ -33,6 +34,7 @@ Vue.component("AppPager", AppPager);
Vue.component("AppRating", AppRating);
Vue.component("AppTagEditor", AppTagEditor);
Vue.component("AppTextField", AppTextField);
Vue.component("AppValidationErrors", AppValidationErrors);
Vue.use(VueProgressBar, {

View File

@ -20,6 +20,12 @@ import TheRecipeEditor from './components/TheRecipeEditor';
import TheRecipeCreator from './components/TheRecipeCreator';
import TheRecipeList from './components/TheRecipeList';
import TheUserCreator from './components/TheUserCreator';
import TheUserEditor from './components/TheUserEditor';
import TheAdminUserList from './components/TheAdminUserList';
import TheAdminUserEditor from './components/TheAdminUserEditor';
Vue.use(Router);
const router = new Router({
@ -116,6 +122,30 @@ router.addRoutes(
.then(() => next("/"));
}
},
{
path: "/user/new",
name: "new_user",
component: TheUserCreator
},
{
path: "/user/edit",
name: "edit_user",
component: TheUserEditor
},
{
path: "/admin/users",
name: "admin_users",
component: TheAdminUserList
},
{
path: "/admin/users/:id/edit",
name: "admin_edit_user",
component: TheAdminUserEditor
},
{
path: '*',
component: The404Page

View File

@ -0,0 +1,5 @@
json.array! @users do |u|
json.extract! u, :id, :username, :full_name, :email, :admin
json.name u.display_name
end

View File

@ -1,2 +1,8 @@
json.ex
if current_user
json.extract! current_user, :id, :username, :email, :full_name, :admin
json.name current_user.display_name
else
json.nil!
end