Skip to content

Commit e2d5121

Browse files
committed
AIRAVATA-3319 Simple user profile editor for editing first name, last name
1 parent eafe669 commit e2d5121

File tree

18 files changed

+9026
-6
lines changed

18 files changed

+9026
-6
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ package-lock.json
1616
# mkdocs
1717
site/
1818
.tox/
19+
yarn-error.log

django_airavata/apps/api/static/django_airavata_api/js/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ const services = {
139139
"UnverifiedEmailUsers"
140140
),
141141
UserProfileService: ServiceFactory.service("UserProfiles"),
142+
UserService: ServiceFactory.service("Users"),
142143
UserStoragePathService: ServiceFactory.service("UserStoragePaths"),
143144
WorkspacePreferencesService: ServiceFactory.service("WorkspacePreferences"),
144145
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import BaseModel from "./BaseModel";
2+
3+
const FIELDS = ["id", "username", "first_name", "last_name", "email"];
4+
5+
export default class User extends BaseModel {
6+
constructor(data = {}) {
7+
super(FIELDS, data);
8+
}
9+
}

django_airavata/apps/api/static/django_airavata_api/js/service_config.js

+12
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import SharedEntity from "./models/SharedEntity";
2424
import StoragePreference from "./models/StoragePreference";
2525
import StorageResourceDescription from "./models/StorageResourceDescription";
2626
import UnverifiedEmailUserProfile from "./models/UnverifiedEmailUserProfile";
27+
import User from "./models/User";
2728
import UserProfile from "./models/UserProfile";
2829
import UserStoragePath from "./models/UserStoragePath";
2930
import WorkspacePreferences from "./models/WorkspacePreferences";
@@ -369,6 +370,17 @@ export default {
369370
queryParams: ["limit", "offset"],
370371
modelClass: UnverifiedEmailUserProfile,
371372
},
373+
Users: {
374+
url: "/auth/users",
375+
viewSet: true,
376+
methods: {
377+
current: {
378+
url: "/auth/users/current/",
379+
requestType: "get"
380+
}
381+
},
382+
modelClass: User,
383+
},
372384
UserProfiles: {
373385
url: "/api/user-profiles",
374386
viewSet: ["list", "retrieve"],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist
2+
templates
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
presets: ["@vue/app"]
3+
};
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"name": "django-airavata-auth-views",
3+
"description": "A Vue.js project",
4+
"version": "1.0.0",
5+
"author": "Apache Airavata <[email protected]>",
6+
"private": true,
7+
"scripts": {
8+
"serve": "vue-cli-service serve",
9+
"build": "vue-cli-service build",
10+
"lint": "vue-cli-service lint",
11+
"format": "prettier --write ."
12+
},
13+
"dependencies": {
14+
"bootstrap": "^4.0.0-beta.2",
15+
"bootstrap-vue": "2.0.0-rc.26",
16+
"django-airavata-api": "link:../api/",
17+
"django-airavata-common-ui": "link:../../static/common/",
18+
"vue": "^2.5.21"
19+
},
20+
"devDependencies": {
21+
"@vue/cli-plugin-babel": "^3.1.1",
22+
"@vue/cli-plugin-eslint": "^3.1.1",
23+
"@vue/cli-service": "^3.1.1",
24+
"babel-eslint": "^10.0.1",
25+
"eslint": "^5.8.0",
26+
"eslint-plugin-vue": "^5.0.0-0",
27+
"prettier": "^2.1.2",
28+
"vue-template-compiler": "^2.5.21",
29+
"webpack-bundle-tracker": "^0.4.2-beta"
30+
},
31+
"eslintConfig": {
32+
"root": true,
33+
"env": {
34+
"node": true
35+
},
36+
"extends": [
37+
"plugin:vue/essential",
38+
"eslint:recommended"
39+
],
40+
"rules": {},
41+
"parserOptions": {
42+
"parser": "babel-eslint"
43+
}
44+
},
45+
"postcss": {
46+
"plugins": {
47+
"autoprefixer": {}
48+
}
49+
},
50+
"browserslist": [
51+
"> 1%",
52+
"last 2 versions",
53+
"not ie <= 8"
54+
]
55+
}

django_airavata/apps/auth/serializers.py

+6
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ class UserSerializer(serializers.ModelSerializer):
66
class Meta:
77
model = get_user_model()
88
fields = ['id', 'username', 'first_name', 'last_name', 'email']
9+
10+
def update(self, instance, validated_data):
11+
instance.first_name = validated_data['first_name']
12+
instance.last_name = validated_data['last_name']
13+
instance.save()
14+
return instance
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<template>
2+
<b-card>
3+
<b-form-group label="Username">
4+
<b-form-input disabled :value="user.username" />
5+
</b-form-group>
6+
<b-form-group label="First Name">
7+
<b-form-input v-model="user.first_name" />
8+
</b-form-group>
9+
<b-form-group label="Last Name">
10+
<b-form-input v-model="user.last_name" />
11+
</b-form-group>
12+
<b-form-group label="Email">
13+
<b-form-input disabled :value="user.email" />
14+
</b-form-group>
15+
<b-button variant="primary" @click="$emit('save', user)">Save</b-button>
16+
<b-button>Cancel</b-button>
17+
</b-card>
18+
</template>
19+
20+
<script>
21+
import { models } from "django-airavata-api";
22+
export default {
23+
name: "user-profile-editor",
24+
props: {
25+
value: {
26+
type: models.User,
27+
required: true,
28+
},
29+
},
30+
data() {
31+
return {
32+
user: this.cloneValue(),
33+
};
34+
},
35+
methods: {
36+
cloneValue() {
37+
return JSON.parse(JSON.stringify(this.value));
38+
},
39+
},
40+
};
41+
</script>
42+
43+
<style></style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<template>
2+
<div>
3+
<h1 class="h4 mb-4">User Profile Editor</h1>
4+
<user-profile-editor v-if="user" v-model="user" @save="onSave" />
5+
</div>
6+
</template>
7+
8+
<script>
9+
import { services } from "django-airavata-api";
10+
import UserProfileEditor from "../components/UserProfileEditor.vue";
11+
12+
export default {
13+
components: { UserProfileEditor },
14+
name: "user-profile-container",
15+
created() {
16+
services.UserService.current().then((user) => {
17+
this.user = user;
18+
});
19+
},
20+
data() {
21+
return {
22+
user: null,
23+
};
24+
},
25+
methods: {
26+
onSave(value) {
27+
services.UserService.update({
28+
lookup: value.id,
29+
data: value,
30+
}).then((user) => {
31+
this.user = user;
32+
});
33+
},
34+
},
35+
};
36+
</script>
37+
38+
<style></style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { components, entry } from "django-airavata-common-ui";
2+
import UserProfileContainer from "./containers/UserProfileContainer.vue";
3+
4+
entry(Vue => {
5+
new Vue({
6+
render: h => h(components.MainLayout, [h(UserProfileContainer)])
7+
}).$mount("#user-profile");
8+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{% extends 'base.html' %}
2+
3+
{% load static %}
4+
{% load render_bundle from webpack_loader %}
5+
6+
{% block css %}
7+
{% render_bundle 'chunk-vendors' 'css' 'AUTH' %}
8+
{% comment %}BUT NOTE: if you only have one entry point you won't have a 'chunk-common' bundle so you may need to comment out the next line until you have more than one entry point.{% endcomment %}
9+
{% comment %} {% render_bundle 'chunk-common' 'css' 'MYAPP' %} {% endcomment %}
10+
{% render_bundle bundle_name 'css' 'AUTH' %}
11+
{% endblock %}
12+
13+
{% block content %}
14+
<div id="{{ bundle_name }}"/>
15+
{% endblock %}
16+
17+
18+
{% block scripts %}
19+
{% render_bundle 'chunk-vendors' 'js' 'AUTH' %}
20+
{% comment %}BUT NOTE: if you only have one entry point you won't have a 'chunk-common' bundle so you may need to comment out the next line until you have more than one entry point.{% endcomment %}
21+
{% comment %} {% render_bundle 'chunk-common' 'js' 'MYAPP' %} {% endcomment %}
22+
{% render_bundle bundle_name 'js' 'AUTH' %}
23+
{% endblock %}

django_airavata/apps/auth/urls.py

+1
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@
3232
views.login_desktop_success, name="login_desktop_success"),
3333
url(r'^refreshed-token-desktop$', views.refreshed_token_desktop,
3434
name="refreshed_token_desktop"),
35+
url(r'^user-profile/', views.user_profile),
3536
]

django_airavata/apps/auth/views.py

+17-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.conf import settings
77
from django.contrib import messages
88
from django.contrib.auth import authenticate, get_user_model, login, logout
9+
from django.contrib.auth.decorators import login_required
910
from django.core.exceptions import ObjectDoesNotExist
1011
from django.forms import ValidationError
1112
from django.http import HttpResponseBadRequest, JsonResponse
@@ -15,6 +16,7 @@
1516
from django.views.decorators.debug import sensitive_variables
1617
from requests_oauthlib import OAuth2Session
1718
from rest_framework import permissions, viewsets
19+
from rest_framework.decorators import action
1820

1921
from django_airavata.apps.auth import serializers
2022

@@ -507,25 +509,37 @@ def _create_login_desktop_failed_response(request, idp_alias=None):
507509
"?" + urlencode(params))
508510

509511

510-
class IsUserOrReadOnlyForSuperuser(permissions.BasePermission):
512+
@login_required
513+
def user_profile(request):
514+
return render(request, "django_airavata_auth/base.html", {
515+
'bundle_name': "user-profile"
516+
})
517+
518+
519+
class IsUserOrReadOnlyForAdmins(permissions.BasePermission):
511520
def has_permission(self, request, view):
512521
return request.user.is_authenticated
513522

514523
def has_object_permission(self, request, view, obj):
515524
if (request.method in permissions.SAFE_METHODS and
516-
request.user.is_superuser):
525+
request.is_gateway_admin):
517526
return True
518527
return obj == request.user
519528

520529

530+
# TODO: disable deleting and creating?
521531
class UserViewSet(viewsets.ModelViewSet):
522532
serializer_class = serializers.UserSerializer
523533
queryset = get_user_model().objects.all()
524-
permission_classes = [IsUserOrReadOnlyForSuperuser]
534+
permission_classes = [IsUserOrReadOnlyForAdmins]
525535

526536
def get_queryset(self):
527537
user = self.request.user
528538
if user.is_superuser:
529539
return get_user_model().objects.all()
530540
else:
531541
return get_user_model().objects.get(pk=user.pk)
542+
543+
@action(detail=False)
544+
def current(self, request):
545+
return redirect(reverse('django_airavata_auth:user-detail', kwargs={'pk': request.user.id}))
+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const BundleTracker = require("webpack-bundle-tracker");
2+
const path = require("path");
3+
4+
module.exports = {
5+
publicPath:
6+
process.env.NODE_ENV === "development"
7+
? "http://localhost:9000/static/django_airavata_auth/dist/"
8+
: "/static/django_airavata_auth/dist/",
9+
outputDir: "./static/django_airavata_auth/dist",
10+
pages: {
11+
"user-profile": "./static/django_airavata_auth/js/entry-user-profile"
12+
// additional entry points go here ...
13+
},
14+
css: {
15+
loaderOptions: {
16+
postcss: {
17+
config: {
18+
path: __dirname
19+
}
20+
}
21+
}
22+
},
23+
configureWebpack: {
24+
plugins: [
25+
new BundleTracker({
26+
filename: "webpack-stats.json",
27+
path: "./static/django_airavata_auth/dist/"
28+
})
29+
],
30+
optimization: {
31+
/*
32+
* Force creating a vendor bundle so we can load the 'app' and 'vendor'
33+
* bundles on development as well as production using django-webpack-loader.
34+
* Otherwise there is no vendor bundle on development and we would need
35+
* some template logic to skip trying to load it.
36+
* See also: https://bitbucket.org/calidae/dejavu/src/d63d10b0030a951c3cafa6b574dad25b3bef3fe9/%7B%7Bcookiecutter.project_slug%7D%7D/frontend/vue.config.js?at=master&fileviewer=file-view-default#vue.config.js-27
37+
*/
38+
splitChunks: {
39+
cacheGroups: {
40+
vendors: {
41+
name: "chunk-vendors",
42+
test: /[\\/]node_modules[\\/]/,
43+
priority: -10,
44+
chunks: "initial"
45+
},
46+
common: {
47+
name: "chunk-common",
48+
minChunks: 2,
49+
priority: -20,
50+
chunks: "initial",
51+
reuseExistingChunk: true
52+
}
53+
}
54+
}
55+
}
56+
},
57+
chainWebpack: config => {
58+
/*
59+
* Specify the eslint config file otherwise it complains of a missing
60+
* config file for the ../api and ../../static/common packages
61+
*
62+
* See: https://github.com/vuejs/vue-cli/issues/2539#issuecomment-422295246
63+
*/
64+
config.module
65+
.rule("eslint")
66+
.use("eslint-loader")
67+
.tap(options => {
68+
options.configFile = path.resolve(__dirname, "package.json");
69+
return options;
70+
});
71+
},
72+
devServer: {
73+
port: 9000,
74+
headers: {
75+
"Access-Control-Allow-Origin": "*"
76+
},
77+
hot: true,
78+
hotOnly: true
79+
}
80+
};

0 commit comments

Comments
 (0)