Commit 5531552b by Kálmán Viktor

Merge branch 'feature-profile-view'

Conflicts:
	circle/dashboard/static/dashboard/dashboard.css
	circle/dashboard/urls.py
	circle/dashboard/views.py
parents 1acfd3ea fea2c505
...@@ -26,7 +26,7 @@ from django.contrib.auth.signals import user_logged_in ...@@ -26,7 +26,7 @@ from django.contrib.auth.signals import user_logged_in
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.models import ( from django.db.models import (
Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField, Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField,
DateTimeField, permalink, DateTimeField, permalink, BooleanField
) )
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _, override, ugettext from django.utils.translation import ugettext_lazy as _, override, ugettext
...@@ -83,13 +83,15 @@ class Profile(Model): ...@@ -83,13 +83,15 @@ class Profile(Model):
unique=True, blank=True, null=True, max_length=64, unique=True, blank=True, null=True, max_length=64,
help_text=_('Unique identifier of the person, e.g. a student number.')) help_text=_('Unique identifier of the person, e.g. a student number.'))
instance_limit = IntegerField(default=5) instance_limit = IntegerField(default=5)
use_gravatar = BooleanField(default=False)
def notify(self, subject, template, context={}, valid_until=None): def notify(self, subject, template, context={}, valid_until=None):
return Notification.send(self.user, subject, template, context, return Notification.send(self.user, subject, template, context,
valid_until) valid_until)
def get_absolute_url(self): def get_absolute_url(self):
return reverse("dashboard.views.profile") return reverse("dashboard.views.profile",
kwargs={'username': self.user.username})
class GroupProfile(AclBase): class GroupProfile(AclBase):
......
...@@ -692,3 +692,25 @@ textarea[name="list-new-namelist"] { ...@@ -692,3 +692,25 @@ textarea[name="list-new-namelist"] {
#vm-details-connection-string-copy { #vm-details-connection-string-copy {
cursor: pointer; cursor: pointer;
} }
.dashboard-profile-vm-list, .dashboard-profile-group-list {
list-style: none;
padding-left: 28px;
}
.dashboard-profile-vm-list a, .dashboard-profile-vm-list a:hover {
text-decoration: none;
color: #555;
}
#group-detail-user-table td:nth-child(2) a,
#group-detail-perm-table td:nth-child(2) a,
#vm-access-table td:nth-child(2) a,
.no-style-link, .no-style-link:hover {
color: #555 !important;
text-decoration: none;
}
#dashboard-profile-avatar {
max-width: 200px;
}
$(function() {
// change user avatar
$("#dashboard-profile-use-gravatar").click(function() {
var checked = $(this).prop("checked");
var user = $(this).data("user");
$.ajax({
type: 'POST',
url:"/dashboard/profile/" + user + "/use_gravatar/",
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re) {
if(re.new_avatar_url) {
$("#dashboard-profile-avatar").prop("src", re.new_avatar_url);
}
},
error: function(xhr, textStatus, error) {
if(xhr.status == 403) {
addMessage(gettext("You have no permission to change this profile."), "danger");
} else {
addMessage(gettext("Unknown error."), "danger");
}
}
});
});
});
...@@ -47,9 +47,13 @@ ...@@ -47,9 +47,13 @@
</ul> </ul>
</li> </li>
</ul> </ul>
{% if user.is_authenticated %} {% if user.is_authenticated and user.pk %}
<a class="navbar-brand pull-right" href="{% url "logout" %}?next={% url "login" %}" style="color: white; font-size: 10px;"><i class="icon-signout icon-sign-out"></i> {% trans "Log out" %}</a> <a class="navbar-brand pull-right" href="{% url "logout" %}?next={% url "login" %}" style="color: white; font-size: 10px;"><i class="icon-signout icon-sign-out"></i> {% trans "Log out" %}</a>
<a class="navbar-brand pull-right" href="{% url "dashboard.views.profile" %}" title="{% trans "User profile" %}" style="color: white; font-size: 10px;"><i class="icon-user "></i> {{user}}</a> <a class="navbar-brand pull-right" href="{% url "dashboard.views.profile" username=user.username %}"
title="{% trans "User profile" %}" style="color: white; font-size: 10px;">
<i class="icon-user"></i>
{% include "dashboard/_display-name.html" with user=user show_org=True %}
</a>
{% if user.is_superuser %} {% if user.is_superuser %}
<a class="navbar-brand pull-right" href="/network/" style="color: white; font-size: 10px;"><i class="icon-globe"></i> {% trans "Network" %}</a> <a class="navbar-brand pull-right" href="/network/" style="color: white; font-size: 10px;"><i class="icon-globe"></i> {% trans "Network" %}</a>
<a class="navbar-brand pull-right" href="/admin/" style="color: white; font-size: 10px;"><i class="icon-cogs"></i> {% trans "Admin" %}</a> <a class="navbar-brand pull-right" href="/admin/" style="color: white; font-size: 10px;"><i class="icon-cogs"></i> {% trans "Admin" %}</a>
......
{% load i18n %} {% load i18n %}
{% if user.get_full_name|length > 0 %} {% if user and user.pk %}
{% if user.get_full_name %}
{{ user.get_full_name }} {{ user.get_full_name }}
{% else %} {% else %}
{{ user.username }} {{ user.username }}
{% endif %} {% endif %}
{% if show_org %} {% if show_org %}
{% if user.profile and user.profile.org_id|length > 0 %} {% if user.profile and user.profile.org_id %}
({{ user.profile.org_id }}) ({{ user.profile.org_id }})
{% else %} {% else %}
({% trans "username" %}: {{ user.username }}) ({% trans "username" %}: {{ user.username }})
{% endif %} {% endif %}
{% endif %}
{% endif %} {% endif %}
...@@ -54,8 +54,8 @@ ...@@ -54,8 +54,8 @@
<i class="icon-user"></i> <i class="icon-user"></i>
</td> </td>
<td> <td>
<strong>{{i.username}}</strong>, <a href="{% url "dashboard.views.profile" username=i.username %}" title="{{ i.username }}"
{% include "dashboard/_display-name.html" with user=i show_org=True %} >{% include "dashboard/_display-name.html" with user=i show_org=True %}</a>
</td> </td>
<td> <td>
<a data-group_pk="{{ group.pk }}" data-member_pk="{{i.pk}}" href="{% url "dashboard.views.remove-user" member_pk=i.pk group_pk=group.pk %}" class="real-link delete-from-group btn btn-link btn-xs"><i class="icon-remove"><span class="sr-only">{% trans "remove" %}</span></i></a> <a data-group_pk="{{ group.pk }}" data-member_pk="{{i.pk}}" href="{% url "dashboard.views.remove-user" member_pk=i.pk group_pk=group.pk %}" class="real-link delete-from-group btn btn-link btn-xs"><i class="icon-remove"><span class="sr-only">{% trans "remove" %}</span></i></a>
...@@ -93,8 +93,8 @@ ...@@ -93,8 +93,8 @@
<i class="icon-user"></i> <i class="icon-user"></i>
</td> </td>
<td> <td>
<strong>{{i.user}}</strong>, <a href="{% url "dashboard.views.profile" username=i.user.username %}" title="{{ i.user.username }}"
{% include "dashboard/_display-name.html" with user=i.user show_org=True %} >{% include "dashboard/_display-name.html" with user=i.user show_org=True %}</a>
</td> </td>
<td> <td>
<select class="form-control" name="perm-u-{{i.user.id}}"> <select class="form-control" name="perm-u-{{i.user.id}}">
...@@ -109,7 +109,12 @@ ...@@ -109,7 +109,12 @@
{% for i in acl.groups %} {% for i in acl.groups %}
<tr> <tr>
<td><i class="icon-group"></i></td><td>{{ i.group }}</td> <td>
<i class="icon-group"></i>
</td>
<td>
<a href="{% url "dashboard.views.group-detail" pk=i.group.pk %}">{{ i.group }}</a>
</td>
<td> <td>
<select class="form-control" name="perm-g-{{ i.group.pk }}"> <select class="form-control" name="perm-g-{{ i.group.pk }}">
{% for id, name in acl.levels %} {% for id, name in acl.levels %}
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title-page %}{{ profile.username}} | {% trans "Profile" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.index" %}">{% trans "Back" %}</a>
<h3 class="no-margin">
<i class="icon-user"></i>
{% include "dashboard/_display-name.html" with user=profile show_org=True %}
</h3>
</div>
<div class="panel-body">
<div>
<div class="" style="float: left">
<img id="dashboard-profile-avatar" src="{{ avatar_url }}" class="img-rounded"/>
</div>
<div class="" style="padding-left: 215px;">
<p>{% trans "Username" %}: {{ profile.username }}</p>
<p>{% trans "Organization ID" %}: {{ profile.profile.org_id|default:"-" }}</p>
<p>{% trans "First name" %}: {{ profile.first_name|default:"-" }}</p>
<p>{% trans "Last name" %}: {{ profile.last_name|default:"-" }}</p>
<p>
{# if the group list is not empty the logged in user is somewhat related to the user #}
{% if perm_group_list %}
{% trans "Email address" %}: {{ profile.email }}
{% else %}
-
{% endif %}
</p>
{% if request.user == profile %}
<p>
{% trans "Use email address as Gravatar profile image" %}:
<input id="dashboard-profile-use-gravatar" data-user="{{ profile.username }}"
{% if profile.profile.use_gravatar %}checked="checked"{% endif %}
type="checkbox"/> <a href="https://gravatar.com">{% trans "What's Gravatar?" %}</a>
</p>
<a href="{% url "dashboard.views.profile-preferences" %}">{% trans "Change password and language" %}</a>
{% endif %}
</div>
<div class="clearfix"></div>
</div>
{% if perm_group_list %}
<hr />
<h4>
<i class="icon-group"></i> {% trans "Groups" %}
</h4>
<ul class="dashboard-profile-group-list">
{% for g in groups %}
<li>{{ g.name }}</li>
{% empty %}
{% trans "This user is not in any group." %}
{% endfor %}
</ul>
{% endif %}
<hr />
<h4>
<i class="icon-desktop"></i>
{% trans "Virtual machines owned by the user" %} ({{ instances_owned|length }})
</h4>
<ul class="dashboard-profile-vm-list">
{% for i in instances_owned %}
<li>
<a href="{{ i.get_absolute_url }}">
<i class="icon-li {{ i.get_status_icon }}"></i>
{{ i }}
</a>
</li>
{% empty %}
<li>
{% trans "This user have no virtual machines." %}
</li>
{% endfor %}
</ul>
<hr />
<h4>
<i class="icon-desktop"></i>
{% trans "Virtual machines with access" %} ({{ instances_with_access|length }})
</h4>
<ul class="dashboard-profile-vm-list">
{% for i in instances_with_access %}
<li>
<a href="{{ i.get_absolute_url }}">
<i class="icon-li {{ i.get_status_icon }}"></i>
{{ i }}
</a>
</li>
{% empty %}
<li>
{% trans "This user have no access to any virtual machine." %}
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ STATIC_URL }}dashboard/profile.js"></script>
{% endblock %}
...@@ -8,9 +8,10 @@ ...@@ -8,9 +8,10 @@
{% if user.is_superuser %}<a href="{{ a.get_absolute_url }}">{% endif %} {% if user.is_superuser %}<a href="{{ a.get_absolute_url }}">{% endif %}
{{ a.get_readable_name }}{% if user.is_superuser %}</a>{% endif %} {{ a.get_readable_name }}{% if user.is_superuser %}</a>{% endif %}
</strong> </strong>
{{ a.started|date:"Y-m-d H:i" }} {{ a.started|date:"Y-m-d H:i" }}{% if a.user %},
{% if a.user %}, <a class="no-style-link" href="{% url "dashboard.views.profile" username=a.user.username %}">
{% include "dashboard/_display-name.html" with user=a.user show_org=True %} {% include "dashboard/_display-name.html" with user=a.user show_org=True %}
</a>
{% endif %} {% endif %}
{% if a.is_abortable_for_user %} {% if a.is_abortable_for_user %}
<form action="{{ a.instance.get_absolute_url }}" method="POST" class="pull-right"> <form action="{{ a.instance.get_absolute_url }}" method="POST" class="pull-right">
......
...@@ -26,7 +26,10 @@ ...@@ -26,7 +26,10 @@
{% for i in acl.users %} {% for i in acl.users %}
<tr> <tr>
<td><i class="icon-user"></i></td> <td><i class="icon-user"></i></td>
<td>{{i.user}}</td> <td>
<a href="{% url "dashboard.views.profile" username=i.user.username %}" title="{{ i.user.username }}"
>{% include "dashboard/_display-name.html" with user=i.user show_org=True %}</a>
</td>
<td> <td>
<select class="form-control" name="perm-u-{{i.user.id}}"> <select class="form-control" name="perm-u-{{i.user.id}}">
{% for id, name in acl.levels %} {% for id, name in acl.levels %}
...@@ -41,7 +44,11 @@ ...@@ -41,7 +44,11 @@
{% endfor %} {% endfor %}
{% for i in acl.groups %} {% for i in acl.groups %}
<tr> <tr>
<td><i class="icon-group"></i></td><td>{{i.group}}</td> <td><i class="icon-group"></i></td>
<td>
<a href="{% url "dashboard.views.group-detail" pk=i.group.pk %}"
>{{ i.group.name }}</a>
</td>
<td> <td>
<select class="form-control" name="perm-g-{{i.group.id}}"> <select class="form-control" name="perm-g-{{i.group.id}}">
{% for id, name in acl.levels %} {% for id, name in acl.levels %}
......
...@@ -49,6 +49,7 @@ class ViewUserTestCase(unittest.TestCase): ...@@ -49,6 +49,7 @@ class ViewUserTestCase(unittest.TestCase):
with patch.object(InstanceActivityDetail, 'get_object') as go: with patch.object(InstanceActivityDetail, 'get_object') as go:
act = MagicMock(spec=InstanceActivity) act = MagicMock(spec=InstanceActivity)
act._meta.object_name = "InstanceActivity" act._meta.object_name = "InstanceActivity"
act.user.pk = 1
go.return_value = act go.return_value = act
view = InstanceActivityDetail.as_view() view = InstanceActivityDetail.as_view()
self.assertEquals(view(request, pk=1234).render().status_code, 200) self.assertEquals(view(request, pk=1234).render().status_code, 200)
......
...@@ -34,7 +34,8 @@ from .views import ( ...@@ -34,7 +34,8 @@ from .views import (
GroupCreate, GroupCreate,
TemplateChoose, TemplateChoose,
UserCreationView, UserCreationView,
get_vm_screenshot get_vm_screenshot,
ProfileView, toggle_use_gravatar,
) )
urlpatterns = patterns( urlpatterns = patterns(
...@@ -136,7 +137,11 @@ urlpatterns = patterns( ...@@ -136,7 +137,11 @@ urlpatterns = patterns(
name="dashboard.views.interface-delete"), name="dashboard.views.interface-delete"),
url(r'^profile/$', MyPreferencesView.as_view(), url(r'^profile/$', MyPreferencesView.as_view(),
name="dashboard.views.profile-preferences"),
url(r'^profile/(?P<username>\w+)/$', ProfileView.as_view(),
name="dashboard.views.profile"), name="dashboard.views.profile"),
url(r'^profile/(?P<username>\w+)/use_gravatar/$', toggle_use_gravatar),
url(r'^group/(?P<group_pk>\d+)/remove/acl/user/(?P<member_pk>\d+)/$', url(r'^group/(?P<group_pk>\d+)/remove/acl/user/(?P<member_pk>\d+)/$',
GroupRemoveAclUserView.as_view(), GroupRemoveAclUserView.as_view(),
name="dashboard.views.remove-acluser"), name="dashboard.views.remove-acluser"),
......
...@@ -21,6 +21,7 @@ from os import getenv ...@@ -21,6 +21,7 @@ from os import getenv
import json import json
import logging import logging
import re import re
from hashlib import md5
import requests import requests
from django.conf import settings from django.conf import settings
...@@ -36,7 +37,7 @@ from django.core.urlresolvers import reverse, reverse_lazy ...@@ -36,7 +37,7 @@ from django.core.urlresolvers import reverse, reverse_lazy
from django.db.models import Count from django.db.models import Count
from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.shortcuts import redirect, render, get_object_or_404 from django.shortcuts import redirect, render, get_object_or_404
from django.views.decorators.http import require_GET from django.views.decorators.http import require_GET, require_POST
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.views.generic import (TemplateView, DetailView, View, DeleteView, from django.views.generic import (TemplateView, DetailView, View, DeleteView,
UpdateView, CreateView, ListView) UpdateView, CreateView, ListView)
...@@ -46,6 +47,7 @@ from django.utils.translation import ungettext as __ ...@@ -46,6 +47,7 @@ from django.utils.translation import ungettext as __
from django.template.defaultfilters import title as title_filter from django.template.defaultfilters import title as title_filter
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.template import RequestContext from django.template import RequestContext
from django.templatetags.static import static
from django.forms.models import inlineformset_factory from django.forms.models import inlineformset_factory
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
...@@ -2487,7 +2489,7 @@ class MyPreferencesView(UpdateView): ...@@ -2487,7 +2489,7 @@ class MyPreferencesView(UpdateView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.ojbect = self.get_object() self.ojbect = self.get_object()
redirect_response = HttpResponseRedirect( redirect_response = HttpResponseRedirect(
reverse("dashboard.views.profile")) reverse("dashboard.views.profile-preferences"))
if "preferred_language" in request.POST: if "preferred_language" in request.POST:
form = MyProfileForm(request.POST, instance=self.get_object()) form = MyProfileForm(request.POST, instance=self.get_object())
if form.is_valid(): if form.is_valid():
...@@ -2689,3 +2691,70 @@ def get_vm_screenshot(request, pk): ...@@ -2689,3 +2691,70 @@ def get_vm_screenshot(request, pk):
raise Http404() raise Http404()
return HttpResponse(image, mimetype="image/png") return HttpResponse(image, mimetype="image/png")
class ProfileView(LoginRequiredMixin, DetailView):
template_name = "dashboard/profile.html"
model = User
slug_field = "username"
slug_url_kwarg = "username"
def get_context_data(self, **kwargs):
context = super(ProfileView, self).get_context_data(**kwargs)
user = self.get_object()
context['profile'] = user
context['avatar_url'] = get_user_avatar_url(user)
context['instances_owned'] = Instance.get_objects_with_level(
"owner", user, disregard_superuser=True).filter(destroyed_at=None)
context['instances_with_access'] = Instance.get_objects_with_level(
"user", user, disregard_superuser=True
).filter(destroyed_at=None).exclude(pk__in=context['instances_owned'])
group_profiles = GroupProfile.get_objects_with_level(
"operator", self.request.user)
groups = Group.objects.filter(groupprofile__in=group_profiles)
context['groups'] = user.groups.filter(pk__in=groups)
# permissions
# show groups only if the user is superuser, or have access
# to any of the groups the user belongs to
context['perm_group_list'] = (
self.request.user.is_superuser or len(context['groups']) > 0)
# filter the virtual machine list
# if the logged in user is not superuser or not the user itself
# filter the list so only those virtual machines are shown that are
# originated from templates the logged in user is operator or higher
if not (self.request.user.is_superuser or self.request.user == user):
it = InstanceTemplate.get_objects_with_level("operator",
self.request.user)
context['instances_owned'] = context['instances_owned'].filter(
template__in=it)
context['instances_with_access'] = context[
'instances_with_access'].filter(template__in=it)
return context
@require_POST
def toggle_use_gravatar(request, **kwargs):
user = get_object_or_404(User, username=kwargs['username'])
if not request.user == user:
raise PermissionDenied()
profile = user.profile
profile.use_gravatar = not profile.use_gravatar
profile.save()
new_avatar_url = get_user_avatar_url(user)
return HttpResponse(
json.dumps({'new_avatar_url': new_avatar_url}),
content_type="application/json",
)
def get_user_avatar_url(user):
if user.profile.use_gravatar:
gravatar_hash = md5(user.email).hexdigest()
return "https://secure.gravatar.com/avatar/%s?s=200" % gravatar_hash
else:
return static("dashboard/img/avatar.png")
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment