Commit 05df66fb by Carpoon

Exam functionality

Add exam functionality
Add export form for import and export
Fix some typo in ownership transfers
parent dcf38ab6
...@@ -224,6 +224,8 @@ PIPELINE = { ...@@ -224,6 +224,8 @@ PIPELINE = {
"dashboard/dashboard.js", "dashboard/dashboard.js",
"dashboard/activity.js", "dashboard/activity.js",
"dashboard/group-details.js", "dashboard/group-details.js",
"dashboard/exam-list.js",
"dashboard/exam-details.js",
"dashboard/group-list.js", "dashboard/group-list.js",
"dashboard/js/stupidtable.min.js", # no bower file "dashboard/js/stupidtable.min.js", # no bower file
"dashboard/node-create.js", "dashboard/node-create.js",
...@@ -396,6 +398,7 @@ LOCAL_APPS = ( ...@@ -396,6 +398,7 @@ LOCAL_APPS = (
'acl', 'acl',
'monitor', 'monitor',
'request', 'request',
'exam',
) )
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
...@@ -614,6 +617,7 @@ STORE_IDENTITY_FILE = get_env_variable("STORE_IDENTITY_FILE", "") ...@@ -614,6 +617,7 @@ STORE_IDENTITY_FILE = get_env_variable("STORE_IDENTITY_FILE", "")
# possible options are "scp", or "rsync" # possible options are "scp", or "rsync"
STORE_SSH_MODE = get_env_variable("STORE_SSH_MODE", "scp") STORE_SSH_MODE = get_env_variable("STORE_SSH_MODE", "scp")
EXPORT_DATASTORE = get_env_variable("EXPORT_DATASTORE", "default") EXPORT_DATASTORE = get_env_variable("EXPORT_DATASTORE", "default")
EXPORT_KEEP_TIME_IN_HOURS = int(get_env_variable("EXPORT_KEEP_TIME_IN_HOURS", "72"))
SESSION_COOKIE_NAME = "csessid%x" % (((getnode() // 139) ^ SESSION_COOKIE_NAME = "csessid%x" % (((getnode() // 139) ^
(getnode() % 983)) & 0xffff) (getnode() % 983)) & 0xffff)
......
...@@ -18,6 +18,7 @@ from urllib.parse import urlparse ...@@ -18,6 +18,7 @@ from urllib.parse import urlparse
import os import os
import pyotp import pyotp
import requests
from crispy_forms.bootstrap import FormActions from crispy_forms.bootstrap import FormActions
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import ( from crispy_forms.layout import (
...@@ -27,6 +28,7 @@ from crispy_forms.utils import render_field ...@@ -27,6 +28,7 @@ from crispy_forms.utils import render_field
from dal import autocomplete from dal import autocomplete
from datetime import timedelta from datetime import timedelta
from django import forms from django import forms
from django.contrib import messages
from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth.forms import ( from django.contrib.auth.forms import (
AuthenticationForm, PasswordResetForm, SetPasswordForm, AuthenticationForm, PasswordResetForm, SetPasswordForm,
...@@ -56,7 +58,9 @@ from storage.models import DataStore, Disk ...@@ -56,7 +58,9 @@ from storage.models import DataStore, Disk
from vm.models import ( from vm.models import (
InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, Instance InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, Instance
) )
from exam.models import Exam
from .models import Profile, GroupProfile, Message from .models import Profile, GroupProfile, Message
from .store_api import NoStoreException
from .validators import domain_validator, meta_data_validator, user_data_validator, network_data_validator from .validators import domain_validator, meta_data_validator, user_data_validator, network_data_validator
LANGUAGES_WITH_CODE = ((l[0], format_lazy("{} ({})",l[1], l[0])) LANGUAGES_WITH_CODE = ((l[0], format_lazy("{} ({})",l[1], l[0]))
...@@ -256,6 +260,35 @@ class GroupCreateForm(NoFormTagMixin, forms.ModelForm): ...@@ -256,6 +260,35 @@ class GroupCreateForm(NoFormTagMixin, forms.ModelForm):
fields = ('name',) fields = ('name',)
class ExamCreateForm(NoFormTagMixin, forms.ModelForm):
description = forms.CharField(label=_("Description"), required=False,
widget=forms.Textarea(attrs={'rows': 3}))
def __init__(self, *args, **kwargs):
super(ExamCreateForm, self).__init__(*args, **kwargs)
def setowner(self, user):
self.instance.owner = user
def save(self, commit=True):
if not commit:
raise AttributeError('Committing is mandatory.')
exam = super(ExamCreateForm, self).save()
exam.description = self.cleaned_data['description']
exam.save()
return exam
@property
def helper(self):
helper = super(ExamCreateForm, self).helper
helper.add_input(Submit("submit", _("Create")))
return helper
class Meta:
model = Exam
fields = ('name',)
class GroupImportForm(NoFormTagMixin, forms.Form): class GroupImportForm(NoFormTagMixin, forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user") self.user = kwargs.pop("user")
...@@ -907,6 +940,162 @@ class VmDiskExportForm(OperationForm): ...@@ -907,6 +940,162 @@ class VmDiskExportForm(OperationForm):
return helper return helper
class VmExportViewForm(OperationForm):
exported_name = forms.CharField(max_length=100, label=_('Filename'))
export_target = forms.ChoiceField(
choices=(('user_store', 'User store'), ('datastore', 'Datastore')),
label=_('Export target'))
def __init__(self, *args, **kwargs):
print(args)
print(kwargs)
super(VmExportViewForm, self).__init__(*args, **kwargs)
@property
def helper(self):
helper = super(VmExportViewForm, self).helper
helper.layout = Layout(
AnyTag(
"div",
HTML(format_html(
_("<label>VM :</label> {0}"),
None)),
css_class="form-group",
),
Field('exported_name'),
Field('export_target'),)
return helper
class VmDiskExportDatastoreForm(OperationForm):
exported_name = forms.CharField(max_length=100, label=_('Filename'))
disk_format = forms.ChoiceField(
choices=Disk.EXPORT_FORMATS,
label=_('Format'))
export_target = forms.ChoiceField(
choices=(('user_store', 'User store'), ('datastore', 'Datastore')),
label=_('Export target'))
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
self.disk = kwargs.pop('default')
super(VmDiskExportForm, self).__init__(*args, **kwargs)
self.fields['disk'] = forms.ModelChoiceField(
queryset=choices, initial=self.disk, required=True,
empty_label=None, label=_('Disk'))
if self.disk:
self.fields['disk'].widget = HiddenInput()
@property
def helper(self):
helper = super(VmDiskExportForm, self).helper
if self.disk:
helper.layout = Layout(
AnyTag(
"div",
HTML(_("<label>Disk:</label> %s") % escape(self.disk)),
css_class="form-group",
),
Field('disk'),
Field('exported_name'),
Field('disk_format'),
Field('export_target'),
)
return helper
class TemplateImportForm(NoFormTagMixin, forms.Form):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
super(TemplateImportForm, self).__init__(*args, **kwargs)
exported_tempalte_paths = Store(self.user).get_files_with_exts(["ova"])
exported_template_names = [
os.path.basename(item) for item in exported_tempalte_paths
]
self.fields["template_name"] = forms.CharField(
label=_("Template name")
)
self.choices = list(zip(exported_tempalte_paths, exported_template_names))
self.fields["ova_path"] = forms.ChoiceField(
label=_("Ova to import"),
choices=self.choices
)
lease_queryset = (Lease.get_objects_with_level("operator", self.user).distinct())
self.fields['lease'] = forms.ModelChoiceField(
queryset=lease_queryset, required=True,
empty_label=None, label=_('Lease'))
@property
def helper(self):
helper = super(TemplateImportForm, self).helper
helper.add_input(Submit("submit", _("Import")))
return helper
def post(self, request, *args, **kwargs):
if not request.user.has_module_perms('auth'):
raise PermissionDenied()
try:
Store(request.user)
except NoStoreException:
raise PermissionDenied()
form = self.form_class(request.POST, user=request.user)
if form.is_valid():
group_path = form.cleaned_data["ova_path"]
url = Store(request.user).request_download(group_path)
try:
response = requests.get(url, verify=False)
#response = requests.get(url)
if response.status_code == 200:
json_str = response.content
else:
messages.error(request, response.reason)
return self.get(request, form, *args, **kwargs)
except requests.exceptions.RequestException as e:
messages.error(e)
return self.get(request, form, *args, **kwargs)
else:
return self.get(request, form, *args, **kwargs)
class ExamForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ExamForm, self).__init__(*args, **kwargs)
self.generate_fields()
def save(self, commit=True):
data = self.cleaned_data
exam = super(self).save(commit=False)
if commit:
exam.save()
return exam
@property
def helper(self):
helper = FormHelper()
helper.layout = Layout(
Field('name'),
)
helper.add_input(Submit("submit", _("Save changes")))
return helper
def generate_fields(self):
pass
class Meta:
model = Exam
exclude = ()
class VmDiskResizeForm(OperationForm): class VmDiskResizeForm(OperationForm):
size = forms.CharField( size = forms.CharField(
widget=FileSizeWidget, initial=(10 << 30), label=_('Size'), widget=FileSizeWidget, initial=(10 << 30), label=_('Size'),
...@@ -1726,6 +1915,27 @@ class TemplateListSearchForm(forms.Form): ...@@ -1726,6 +1915,27 @@ class TemplateListSearchForm(forms.Form):
self.data = data self.data = data
class ExamListSearchForm(forms.Form):
use_required_attribute = False
s = forms.CharField(widget=forms.TextInput(attrs={
'class': "form-control input-tags",
'placeholder': _("Search...")
}))
stype = forms.ChoiceField(initial=vm_search_choices, widget=forms.Select(attrs={
'class': "btn btn-default input-tags",
}))
def __init__(self, *args, **kwargs):
super(ExamListSearchForm, self).__init__(*args, **kwargs)
# set initial value, otherwise it would be overwritten by request.GET
if not self.data.get("stype"):
data = self.data.copy()
data['stype'] = "owned"
self.data = data
class UserListSearchForm(forms.Form): class UserListSearchForm(forms.Form):
use_required_attribute = False use_required_attribute = False
......
...@@ -63,6 +63,7 @@ logger = getLogger(__name__) ...@@ -63,6 +63,7 @@ logger = getLogger(__name__)
from django.conf import settings from django.conf import settings
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.contrib import messages
from django.dispatch import receiver from django.dispatch import receiver
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
...@@ -361,13 +362,19 @@ class GroupProfile(AclBase): ...@@ -361,13 +362,19 @@ class GroupProfile(AclBase):
user = Profile.objects.get(org_id=org_id).user user = Profile.objects.get(org_id=org_id).user
user.groups.add(group) user.groups.add(group)
except ObjectDoesNotExist: except ObjectDoesNotExist:
try:
future_member = FutureMember(org_id=org_id, group=group) future_member = FutureMember(org_id=org_id, group=group)
future_member.save() future_member.save()
except Exception as e:
raise Exception("Can't find user %s!" % org_id)
for permission in data["permissions"]: for permission in data["permissions"]:
try:
group.permissions.add( group.permissions.add(
Permission.objects.get_by_natural_key(*permission) Permission.objects.get_by_natural_key(*permission)
) )
except Exception as e:
messages.warning("Couldn't add %s permission to group!" % permission)
return group.profile return group.profile
except (KeyError, ValueError, TypeError): except (KeyError, ValueError, TypeError):
......
...@@ -7,6 +7,7 @@ from vm.models import Instance, InstanceTemplate, Lease, Interface, Node, Instan ...@@ -7,6 +7,7 @@ from vm.models import Instance, InstanceTemplate, Lease, Interface, Node, Instan
from firewall.models import Vlan, Rule from firewall.models import Vlan, Rule
from storage.models import Disk, StorageActivity from storage.models import Disk, StorageActivity
from vm.models.common import Variable from vm.models.common import Variable
from exam.models import Exam
class RuleSerializer(serializers.ModelSerializer): class RuleSerializer(serializers.ModelSerializer):
class Meta: class Meta:
...@@ -35,6 +36,13 @@ class GroupSerializer(serializers.ModelSerializer): ...@@ -35,6 +36,13 @@ class GroupSerializer(serializers.ModelSerializer):
model = Group model = Group
fields = ('id', 'name', 'user_set') fields = ('id', 'name', 'user_set')
class ExamSerializer(serializers.ModelSerializer):
class Meta:
model = Exam
fields = ('id', 'name', 'owner', 'description', 'template')
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
......
...@@ -172,7 +172,9 @@ html { ...@@ -172,7 +172,9 @@ html {
#group-details-rename *, #group-details-rename *,
#group-details-h1-name, #group-details-h1-name,
#group-list-rename, #group-list-rename,
#group-list-rename * { #group-list-rename *,
#exam-list-rename,
#exam-list-rename *{
display: inline; display: inline;
} }
#vm-details-rename, #vm-details-rename,
...@@ -180,10 +182,13 @@ html { ...@@ -180,10 +182,13 @@ html {
#node-details-rename, #node-details-rename,
#node-list-rename, #node-list-rename,
#group-details-rename, #group-details-rename,
#group-list-rename { #group-list-rename,
#exam-details-rename,
#exam-list-rename {
display: none; display: none;
} }
#group-details-rename-form { #group-details-rename-form,
#exam-details-rename-form {
display: inline-block; display: inline-block;
} }
.vm-details-home-name, .vm-details-home-name,
......
...@@ -201,16 +201,16 @@ html { ...@@ -201,16 +201,16 @@ html {
} }
#vm-details-rename, #vm-details-h1-name, #vm-details-rename , #vm-details-rename, #vm-details-h1-name, #vm-details-rename ,
#node-details-rename, #node-details-rename *, #node-details-h1-name, #node-list-rename, #node-list-rename *#group-details-rename, #group-details-rename *, #group-details-h1-name, #group-list-rename, #group-list-rename * { #node-details-rename, #node-details-rename *, #node-details-h1-name, #node-list-rename, #node-list-rename *#group-details-rename, #group-details-rename *, #group-details-h1-name, #group-list-rename, #group-list-rename *, #exam-list-rename, #exam-list-rename * {
display: inline; display: inline;
} }
#vm-details-rename, #vm-list-rename, #node-details-rename, #node-list-rename, #group-details-rename, #group-list-rename { #vm-details-rename, #vm-list-rename, #node-details-rename, #node-list-rename, #group-details-rename, #group-list-rename, #exam-list-rename, #exam-details-rename {
display: none; display: none;
} }
#group-details-rename-form { #group-details-rename-form, #exam-details-rename-form {
display: inline-block; display: inline-block;
} }
......
$(function() {
/* rename */
$("#exam-details-h1-name, .exam-details-rename-button").click(function() {
$("#exam-details-h1-name span").hide();
$("#exam-details-rename-form").show().css('display', 'inline-block');
$("#exam-details-rename-name").select();
});
/* rename ajax */
$('#exam-details-rename-submit').click(function() {
if(!$("#exam-details-rename-name")[0].checkValidity()) {
return true;
}
var name = $('#exam-details-rename-name').val();
$.ajax({
method: 'POST',
url: location.href,
data: {'new_name': name},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
$("#exam-details-h1-name span").text(data.new_name).show();
$('#exam-details-rename-form').hide();
},
error: function(xhr, textStatus, error) {
addMessage("Error during renaming.", "danger");
}
});
return false;
});
/* update description click */
$(".exam-details-home-edit-description-click").click(function(e) {
$(".exam-details-home-edit-description-click").hide();
$("#exam-details-home-description").show();
var ta = $("#exam-details-home-description textarea");
var tmp = ta.val();
ta.val("");
ta.focus();
ta.val(tmp);
e.preventDefault();
});
/* description update ajax */
$('.exam-details-description-submit').click(function() {
var description = $(this).prev("textarea").val();
$.ajax({
method: 'POST',
url: location.href,
data: {'new_description': description},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
var new_desc = data.new_description;
/* we can't simply use $.text, because we need new lines */
var tagsToReplace = {
'&': "&amp;",
'<': "&lt;",
'>': "&gt;",
};
new_desc = new_desc.replace(/[&<>]/g, function(tag) {
return tagsToReplace[tag] || tag;
});
$(".exam-details-home-edit-description")
.html(new_desc.replace(/\n/g, "<br />"));
$(".exam-details-home-edit-description-click").show();
$("#exam-details-home-description").hide();
// update the textareia
$("exam-details-home-description textarea").text(data.new_description);
},
error: function(xhr, textStatus, error) {
addMessage("Error during renaming!", "danger");
}
});
return false;
});
});
$(function() {
/* rename */
$("#exam-list-rename-button, .exam-details-rename-button").click(function() {
$(".exam-list-column-name", $(this).closest("tr")).hide();
$("#exam-list-rename", $(this).closest("tr")).css('display', 'inline');
$("#exam-list-rename").find("input").select();
});
/* rename ajax */
$('.exam-list-rename-submit').click(function() {
var row = $(this).closest("tr");
var name = $('#exam-list-rename-name', row).val();
var url = row.find(".exam-list-column-name a").prop("href");
$.ajax({
method: 'POST',
url: url,
data: {'new_name': name},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
$(".exam-list-column-name", row).html(
$("<a/>", {
'class': "real-link",
href: "/dashboard/exam/" + data.exam_pk + "/",
text: data.new_name
})
).show();
$('#exam-list-rename', row).hide();
// addMessage(data['message'], "success");
},
error: function(xhr, textStatus, error) {
addMessage("uhoh", "danger");
}
});
return false;
});
});
...@@ -30,6 +30,7 @@ from simplesshkey.models import UserKey ...@@ -30,6 +30,7 @@ from simplesshkey.models import UserKey
from storage.models import Disk from storage.models import Disk
from vm.models import Node, InstanceTemplate, Lease from vm.models import Node, InstanceTemplate, Lease
from dashboard.models import ConnectCommand, Message from dashboard.models import ConnectCommand, Message
from exam.models import Exam
class FileSizeColumn(Column): class FileSizeColumn(Column):
...@@ -152,6 +153,39 @@ class GroupListTable(Table): ...@@ -152,6 +153,39 @@ class GroupListTable(Table):
order_by = ('pk', ) order_by = ('pk', )
class ExamListTable(Table):
name = TemplateColumn(
template_name="dashboard/exam-list/column-name.html",
attrs = {'th': {'data-sort': "string"}}
)
number_of_users = TemplateColumn(
verbose_name=_("Number of examinees"),
template_name='dashboard/exam-list/column-examinees.html',
attrs={'th': {'data-sort': "int"}},
)
owner = TemplateColumn(
template_name="dashboard/exam-list/column-exam-owner.html",
verbose_name=_("Owner"),
attrs={'th': {'data-sort': "string"}}
)
admin = TemplateColumn(
orderable=False,
verbose_name=_("Admin"),
template_name='dashboard/exam-list/column-admin.html',
attrs={'th': {'class': 'exam-list-table-admin'}},
)
class Meta:
model = Exam
attrs = {'class': ('table table-bordered table-striped table-hover '
'exam-list-table')}
fields = ('name', 'template', 'number_of_users', 'owner')
prefix = "exam-"
class UserListTable(Table): class UserListTable(Table):
username = LinkColumn( username = LinkColumn(
'dashboard.views.profile', 'dashboard.views.profile',
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
{% trans "Ownership transfer" %}
</h3>
</div>
<div class="panel-body">
{% blocktrans with owner=exam.owner name=exam.name id=exam.id%}
<strong>{{ owner }}</strong> offered to take the ownership of
virtual machine <strong>{{name}} ({{id}})</strong>.
Do you accept the responsibility of being the exam's owner?
{% endblocktrans %}
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
<a class="btn btn-default" href="{% url "dashboard.index" %}">{% trans "No" %}</a>
<input type="hidden" name="key" value="{{ key }}"/>
<button class="btn btn-danger" type="submit">{% trans "Yes" %}</button>
</form>
</div>
</div>
</div>
{% endblock %}
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
{% blocktrans with owner=instance.owner name=instance.name id=instance.id%} {% blocktrans with owner=instance.owner name=instance.name id=instance.id%}
<strong>{{ owner }}</strong> offered to take the ownership of <strong>{{ owner }}</strong> offered to take the ownership of
virtual machine <strong>{{name}} ({{id}})</strong>. virtual machine <strong>{{name}} ({{id}})</strong>.
Do you accept the responsility of being the host's owner? Do you accept the responsibility of being the host's owner?
{% endblocktrans %} {% endblocktrans %}
<div class="pull-right"> <div class="pull-right">
<form action="" method="POST"> <form action="" method="POST">
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
{% blocktrans with owner=instance.owner name=instance.name id=instance.id%} {% blocktrans with owner=instance.owner name=instance.name id=instance.id%}
<strong>{{ owner }}</strong> offered to take the ownership of <strong>{{ owner }}</strong> offered to take the ownership of
template <strong>{{name}} ({{id}})</strong>. template <strong>{{name}} ({{id}})</strong>.
Do you accept the responsility of being the template's owner? Do you accept the responsibility of being the template's owner?
{% endblocktrans %} {% endblocktrans %}
<div class="pull-right"> <div class="pull-right">
<form action="" method="POST"> <form action="" method="POST">
......
{% load crispy_forms_tags %}
{% load i18n %}
<p class="text-muted">
{% trans "Exams allow to start a template for a set of users." %}
</p>
<form method="POST" action="{% url "dashboard.views.exam-create" %}">
{% csrf_token %}
{% crispy form %}
</form>
{% load i18n %}
<h3>{% trans "Owner" %}</h3>
<p>
{% if user == instance.owner %}
{% blocktrans %}You are the current owner of this instance.{% endblocktrans %}
{% else %}
{% url "dashboard.views.profile" username=instance.owner.username as url %}
{% blocktrans with owner=instance.owner name=instance.owner.get_full_name%}
The current owner of this instance is <a href="{{url}}">{{name}} ({{owner}})</a>.
{% endblocktrans %}
{% endif %}
{% if user == instance.owner or user.is_superuser %}
<span class="operation-wrapper">
<!--a href="{% url "dashboard.views.vm-transfer-ownership" instance.pk %}"
class="btn btn-link operation">{% trans "Transfer ownership..." %}</a>
</span-->
{% endif %}
</p>
<h3>{% trans "Permissions"|capfirst %}</h3>
{% include "dashboard/_manage_access.html" with table_id="exam-access-table" %}
<dl class="well well-sm" id="exam-detail-access-help">
<dt>{% trans "Permissions" %}</dt>
<dd>
{% trans "With Permissions you can add Users and Groups with different levels to grant access to the virtual machine." %}
</dd>
<dt>{% trans "User" %}</dt>
<dd>
{% trans "User level grants access to the virtual machine's details page. Users are able to connect to this machine." %}
</dd>
<dt>{% trans "Operator" %}</dt>
<dd>
{% blocktrans %}
Operator level permit the modification of the name and description fields. Allow the operator to open ports and grant/revoke User level access to the virtual machine.
{% endblocktrans %}
</dd>
<dt>{% trans "Owner" %}</dt>
<dd>
{% blocktrans %}
Owner level enables all operations on the virtual machine. Owners are able to grant/revoke Operator, User and Owner level access to others.
The accountable owner (the one who deployed the machine) can not be demoted. The accountable ownership can be transferred
to other User via the "Transfer onwership" button.
{% endblocktrans %}
</dd>
</dl>
{% extends "dashboard/base.html" %}
{% load crispy_forms_tags %}
{% load i18n %}
{% load static %}
{% block title-page %}{{ exam.name }} | {% trans "group" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-7">
<div class="panel panel-default">
<div class="panel-heading">
<div class="pull-right" style="padding-top: 15px;">
<a title="{% trans "Rename" %}" class="btn btn-default btn-xs exam-details-rename-button">
<i class="fa fa-pencil"></i>
</a>
<a title="{% trans "Delete" %}" data-exam-pk="{{ exam.pk }}" class="btn btn-default btn-xs real-link exam-delete" href="{% url "dashboard.views.exam-delete" pk=exam.pk %}">
<i class="fa fa-trash-o"></i>
</a>
</div>
<h1>
<form action="" method="POST" id="exam-details-rename-form" class="js-hidden">
{% csrf_token %}
<div class="input-exam">
<input id="exam-details-rename-name" class="form-control" name="new_name"
type="text" value="{{ exam.name }}" required />
<span class="input-group-btn">
<button type="submit" id="exam-details-rename-submit" class="btn">
{% trans "Rename" %}
</button>
</span>
</div>
</form>
<div id="exam-details-h1-name">
<span class="no-js-hidden">{{ exam.name }}</span>
</div>
</h1>
</div><!-- .page-header -->
<div class="row">
<div class="col-md-12" id="exam-detail-pane">
<div class="panel panel-default panel-body" id="exam-detail-panel">
<dl id="description">
<dt style="margin-top: 5px;">
{% trans "Description" %}:
{% if is_operator %}
<a href="#" class="exam-details-home-edit-description-click"><i class="fa fa-pencil"></i></a>
{% endif %}
</dt>
<dd>
{% csrf_token %}
<div class="exam-details-home-edit-description-click">
<div class="exam-details-home-edit-description">{{ exam.description|linebreaks }}</div>
</div>
<div id="exam-details-home-description" class="js-hidden">
<form method="POST">
<textarea name="new_description" class="form-control">{{ exam.description }}</textarea>
<button type="submit" class="btn btn-xs btn-success exam-details-description-submit
{% if not is_operator %}disabled{% endif %}">
<i class="fa fa-pencil"></i> {% trans "Update" %}
</button>
</form>
</div>
</dd>
</dl>
<hr />
<h3 id="exam-detail-template-header">{% trans "Template" %}</h3>
<form action="." method="post">{% csrf_token %}
<select class="form-control" name="new-template"{% if not templates %} disabled{% endif %}>
{% if not exam.template.pk %} <option value="-1" selected="selected">---</option>{%endif%}
{% for template in templates %}
<option{% if template.pk == exam.template.pk %} selected="selected"{%endif%} value="{{template.pk}}">{{template.name}}</option>
{% endfor %}
</select>
<br />
<div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div>
</form>
<hr />
<h3 id="exam-detail-users-header">
{% trans "Examenees" %}
<a data-exam_pk="{{ exam.pk }}" href="{% url "dashboard.views.exam-detail.remove-all-user" exam_pk=exam.pk %}" class="btn btn-danger exam-remove-all-btn pull-right">
{% trans "Remove all users" %}
</a>
</h3>
<form action="" method="post">{% csrf_token %}
<table class="table table-striped table-with-form-fields table-bordered" id="exam-detail-user-table">
<tbody>
<thead><tr><th></th><th>{% trans "Who" %}</th><th>{% trans "Remove" %}</th></tr></thead>
{% for i in users %}
<tr>
<td>
<img class="profile-avatar" src="{{ i.profile.get_avatar_url}}"/>
</td>
<td>
<a href="{% url "dashboard.views.profile" username=i.username %}" title="{{ i.username }}"
>{% include "dashboard/_display-name.html" with user=i show_org=True %}</a>
</td>
<td>
<a data-member_pk="{{i.pk}}" href="{% url "dashboard.views.exam-detail.remove-user" exam_pk=exam.pk user_pk=i.pk %}" class="real-link delete-from-group btn btn-link btn-xs"><i class="fa fa-times">
<span class="sr-only">{% trans "remove" %}</span></i>
</a>
</td>
</tr>
{% endfor %}
<tr>
<td><i class="fa fa-plus"></i></td>
<td colspan="2">
{{addmemberform.new_member}}
</td>
</tr>
</tbody>
</table>
<textarea name="new_members" class="form-control"
placeholder="{% trans "Add multiple users at once (one identifier per line)." %}"></textarea>
<div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div>
</form>
<hr />
<h3 id="exam-detail-perm-header">{% trans "Access permissions" %}</h3>
{% include "dashboard/_manage_access.html" with table_id="exam-detail-perm-table" %}
</div>
</div>
</div>
</div>
</div>
<div class="col-md-5">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="no-margin"><i class="fa fa-desktop"></i> {% trans "Virtual machines" %}</h4>
</div>
<div class="panel-body">
<a href="{% url "dashboard.views.exam-start" pk=exam.pk %}" class="btn btn-xs btn-success mass-operation"
title="Deploy">
<i class="fa fa-play"></i>
</a>
<a href="{% url "dashboard.views.exam-shutdown" pk=exam.pk %}" class="btn btn-xs btn-warning mass-operation"
title="Shutdown">
<i class="fa fa-power-off"></i>
</a>
<a href="{% url "dashboard.views.exam-stop" pk=exam.pk %}" class="btn btn-xs btn-warning mass-operation"
title="Shut off">
<i class="fa fa-plug"></i>
</a>
<a href="{% url "dashboard.views.exam-destroy" pk=exam.pk %}" class="btn btn-xs btn-danger mass-operation"
title="Destroy">
<i class="fa fa-times"></i>
</a>
<hr />
<div class="table-responsive">
<table class="table table-bordered table-striped table-hover vm-list-table"
id="vm-list-table">
<thead><tr>
<th data-sort="string" class="orderable sortable">
{% trans "Owner" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="owner" %}
</th>
<th data-sort="string">
{% trans "State" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="status" %}
</th>
<th data-sort="string" class="name orderable sortable">
{% trans "Name" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="name" %}
</th>
</tr></thead><tbody>
{% for instance in instances %}
<tr class="{% cycle 'odd' 'even' %}" data-vm-pk="{{ instance.pk }}">
<td>
<a href="{% url "dashboard.views.profile" username=instance.owner.username %}" title="{{ instance.owner.username }}">
{% include "dashboard/_display-name.html" with user=instance.owner show_org=True %}
</a>
</td>
<td class="state">
<i class="fa fa-fw
{% if show_acts_in_progress and instance.is_in_status_change %}
fa-spin fa-spinner
{% else %}
{{ instance.get_status_icon }}{% endif %}"></i>
<span>{{ instance.get_status_display }}</span>
</td>
<td class="name"><a class="real-link" href="{% url "dashboard.views.detail" instance.pk %}">
{{ instance.name }}</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7">
{% if request.GET.s %}
<strong>{% trans "No result." %}</strong>
{% else %}
<strong>{% trans "You have no virtual machines." %}</strong>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div><!-- .table-responsive -->
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="no-margin"><i class="fa user"></i> {% trans "Owner" %}</h4>
</div>
{% if is_owner %}
{% blocktrans %}You are the current owner of this template.{% endblocktrans %}
{% else %}
{% url "dashboard.views.profile" username=object.owner.username as url %}
{% blocktrans with owner=object.owner name=object.owner.get_full_name%}
The current owner of this template is <a href="{{url}}">{{name}} ({{owner}})</a>.
{% endblocktrans %}
{% endif %}
{% if is_owner or user.is_superuser %}
<br />
<a href="{% url "dashboard.views.exam-transfer-ownership" exam.pk %}"
class="btn btn-link tx-tpl-ownership">{% trans "Transfer ownership..." %}</a>
{% endif %}
</div>
{% if is_owner %}
<div class="panel panel-default">
<div class="panel-heading">
<a href="{% url "dashboard.views.exam-delete" pk=exam.pk %}"
class="btn btn-xs btn-danger pull-right template-delete">
{% trans "Delete" %}
</a>
<h4 class="no-margin"><i class="fa fa-times"></i> {% trans "Delete Exam" %}</h4>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load static %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block title-page %}{% trans "Exam list" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
{% if perms.exam.create_exam %}
<a href="{% url "dashboard.views.exam-create" %}" class="pull-right btn btn-success btn-xs exam-new">
<i class="fa fa-plus"></i> {% trans "new exam" %}
</a>
{% endif %}
<h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Exams" %}</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-offset-8 col-md-4" id="exam-list-search">
<form action="" method="GET">
<div class="input-group">
{{ search_form.s }}
<div class="input-group-btn">
<button type="submit" class="btn btn-primary input-tags">
<i class="fa fa-search"></i>
</button>
</div>
</div><!-- .input-group -->
</form>
</div><!-- .col-md-4 #exam-list-search -->
</div>
</div>
<div class="panel-body">
<div class="table-responsive">
{% render_table table %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% load i18n %}
<a id="exam-list-rename-button-{{record.pk}}" class="btn btn-default btn-xs exam-details-rename-button" title data-original-title={% trans "Rename" %}>
<i class="fa fa-pencil"></i>
</a>
<a href="{% url "dashboard.views.exam-detail" pk=record.pk%}" id="exam-list-edit-button-{{record.pk}}" class="btn btn-default btn-xs" title="{% trans "Edit" %}">
<i class="fa fa-edit"></i>
</a>
<a data-template-pk="{{ record.pk }}" href="{% url "dashboard.views.exam-delete" pk=record.pk %}" class="btn btn-danger btn-xs template-delete" title="{% trans "Delete" %}">
<i class="fa fa-times"></i>
</a>
{% include "dashboard/_display-name.html" with user=record.owner show_org=True new_line=True %}
<div >{{ record.examinees.count }}</div>
\ No newline at end of file
<div id="exam-{{ record.pk }}">{{ record.pk }}</div>
{% load i18n %}
<div id="exam-list-rename">
<form action="{% url "dashboard.views.exam-detail" pk=record.pk %}" method="POST" id="exam-list-rename-form">
{% csrf_token %}
<input id="exam-list-rename-name" class="form-control input-sm" name="new_name" type="text" value="{{ record.name }}"/>
<button type="submit" class="exam-list-rename-submit btn btn-sm">{% trans "Rename" %}</button>
</form>
</div>
<div class="exam-list-column-name">
<a class="real-link" href="{% url "dashboard.views.exam-detail" pk=record.pk %}">{{ record.name }}</a>
</div>
<div >{{ record.template.name }}</div>
\ No newline at end of file
{% load i18n %}
<div class="pull-right">
<form action="{% url "dashboard.views.exam-transfer-ownership" pk=object.pk %}" method="POST" style="max-width: 400px;">
{% csrf_token %}
<label>
{{ form.name.label }}
</label>
<div class="input-group">
{{form.name}}
<div class="input-group-btn">
<input type="submit" value="{% trans "Save" %}" class="btn btn-primary">
</div>
</div>
</form>
</div>
{% load i18n %}
<div class="panel panel-default">
<div class="panel-heading">
<span class="btn btn-default btn-xs infobtn pull-right" data-container="body" title="{% trans "Start VMs for a group." %}">
<i class="fa fa-info-circle"></i>
</span>
<h3 class="no-margin"><i class="fa fa-file-text-o"></i> {% trans "Exams" %}
</h3>
</div>
<div class="list-group" id="exam-list-view">
<div id="dashboard-exam-list">
{% for exam in exams %}
<a href="{% url "dashboard.views.exam-detail" pk=exam.pk %}" class="list-group-item
{% if forloop.last and exams|length < 5 %} list-exam-item-last{% endif %}">
<span class="index-exam-list-name">
<i class="fa"></i> {{ exam.name }}
</span>
<div class="clearfix"></div>
</a>
{% empty %}
<div class="list-group-item">
<div class="alert alert-warning" style="margin: 10px;">
<p>
{% trans "You don't have any exams prepared." %}
</p>
</div>
</div>
{% endfor %}
</div>
<div class="list-group-item list-group-footer">
<div class="row">
<div class="col-xs-5 col-sm-6">
<form action="{% url "dashboard.views.exam-list" %}" method="GET" id="dashboard-exam-search-form">
<div class="input-group input-group-sm">
<input name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" />
<div class="input-group-btn">
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</form>
</div>
<div class="col-xs-7 col-sm-6 text-right">
<a href="{% url "dashboard.views.exam-list" %}" class="btn btn-primary btn-xs">
<i class="fa fa-chevron-circle-right"></i>
{% if more_exams > 0 %}
<strong>{{ more_exams }}</strong> more
{% else %}
{% trans "list" %}
{% endif %}
</a>
{% if perms.exam.create_exam %}
<a href="{% url "dashboard.views.exam-create" %}" class="btn btn-success btn-xs exam-create">
<i class="fa fa-plus-circle"></i> {% trans "new" %}
</a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
...@@ -54,6 +54,12 @@ ...@@ -54,6 +54,12 @@
{% include "dashboard/index-users.html" %} {% include "dashboard/index-users.html" %}
</div> </div>
{% endif %} {% endif %}
{% if perms.exam.start_exam or perms.exam.create_exam %}
<div class="col-lg-4 col-sm-6">
{% include "dashboard/index-exam.html" %}
</div>
{% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
...@@ -21,6 +21,7 @@ from django.conf.urls import url ...@@ -21,6 +21,7 @@ from django.conf.urls import url
from django.urls import path from django.urls import path
from vm.models import Instance from vm.models import Instance
from exam.models import Exam
from .views import ( from .views import (
AclUpdateView, FavouriteView, GroupAclUpdateView, GroupDelete, AclUpdateView, FavouriteView, GroupAclUpdateView, GroupDelete,
GroupDetailView, GroupList, IndexView, GroupDetailView, GroupList, IndexView,
...@@ -28,7 +29,7 @@ from .views import ( ...@@ -28,7 +29,7 @@ from .views import (
MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete, MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete,
NodeDetailView, NodeList, NodeActivityDetail, NodeDetailView, NodeList, NodeActivityDetail,
NotificationView, TemplateAclUpdateView, TemplateCreate, NotificationView, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TemplateDelete, TemplateDetail, TemplateList, TemplateImport,
vm_activity, VmCreate, VmDetailView, vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmList, VmDetailVncTokenView, VmList,
DiskRemoveView, get_disk_download_status, DiskRemoveView, get_disk_download_status,
...@@ -68,7 +69,10 @@ from .views import ( ...@@ -68,7 +69,10 @@ from .views import (
EnableTwoFactorView, DisableTwoFactorView, EnableTwoFactorView, DisableTwoFactorView,
AclUserGroupAutocomplete, AclUserAutocomplete, AclUserGroupAutocomplete, AclUserAutocomplete,
RescheduleView, GroupImportView, GroupExportView, RescheduleView, GroupImportView, GroupExportView,
VariableREST, GetVariableREST, HotplugMemSetREST, HotplugVCPUSetREST VariableREST, GetVariableREST, HotplugMemSetREST, HotplugVCPUSetREST,
ExamList, ExamCreate, ExamDetail, ExamDelete, ExamRemoveUserView, ExamRemoveAllUsersView,
TransferExamOwnershipView, TransferExamOwnershipConfirmView, ExamStart, ExamVMStop,
ExamVMDestroy, ExportedVMDelete
) )
from .views.node import node_ops, NodeREST, GetNodeREST from .views.node import node_ops, NodeREST, GetNodeREST
from .views.vm import vm_ops, vm_mass_ops from .views.vm import vm_ops, vm_mass_ops
...@@ -132,6 +136,8 @@ urlpatterns = [ ...@@ -132,6 +136,8 @@ urlpatterns = [
url(r'^template/create/$', TemplateCreate.as_view(), url(r'^template/create/$', TemplateCreate.as_view(),
name="dashboard.views.template-create"), name="dashboard.views.template-create"),
url(r'^template/import/$', TemplateImport.as_view(),
name="dashboard.views.template-import"),
url(r'^template/choose/$', TemplateChoose.as_view(), url(r'^template/choose/$', TemplateChoose.as_view(),
name="dashboard.views.template-choose"), name="dashboard.views.template-choose"),
url(r'template/(?P<pk>\d+)/acl/$', TemplateAclUpdateView.as_view(), url(r'template/(?P<pk>\d+)/acl/$', TemplateAclUpdateView.as_view(),
...@@ -222,6 +228,22 @@ urlpatterns = [ ...@@ -222,6 +228,22 @@ urlpatterns = [
url(r'^notifications/$', NotificationView.as_view(), url(r'^notifications/$', NotificationView.as_view(),
name="dashboard.views.notifications"), name="dashboard.views.notifications"),
url(r'^exam/list/$', ExamList.as_view(), name='dashboard.views.exam-list'),
url(r'^exam/create/$', ExamCreate.as_view(), name='dashboard.views.exam-create'),
url(r'^exam/(?P<pk>\d+)/$', ExamDetail.as_view(), name='dashboard.views.exam-detail'),
url(r"^exam/delete/(?P<pk>\d+)/$", ExamDelete.as_view(), name="dashboard.views.exam-delete"),
url(r'^exam/(?P<pk>\d+)/acl/$', AclUpdateView.as_view(model=Exam), name='dashboard.views.exam-detail.exam-acl'),
url(r'^exam/(?P<exam_pk>\d+)/remove/user/all/$', ExamRemoveAllUsersView.as_view(), name='dashboard.views.exam-detail.remove-all-user'),
url(r'^exam/(?P<exam_pk>\d+)/remove/user/(?P<user_pk>\d+)', ExamRemoveUserView.as_view(), name='dashboard.views.exam-detail.remove-user'),
url(r'^exam/(?P<pk>\d+)/tx/$', TransferExamOwnershipView.as_view(), name='dashboard.views.exam-transfer-ownership'),
url(r'^exam/tx/(?P<key>.*)/?$', TransferExamOwnershipConfirmView.as_view(), name='dashboard.views.exam-transfer-ownership-confirm'),
url(r'^exam/start/(?P<pk>\d+)/$', ExamStart.as_view(), name='dashboard.views.exam-start'),
url(r'^exam/shutdown/(?P<pk>\d+)/$', ExamVMStop.as_view(), name='dashboard.views.exam-shutdown'),
url(r'^exam/stop/(?P<pk>\d+)/$', ExamVMStop.as_view(), name='dashboard.views.exam-stop'),
url(r'^exam/destroy/(?P<pk>\d+)/$', ExamVMDestroy.as_view(), name='dashboard.views.exam-destroy'),
url(r"^exportedvm/delete/(?P<pk>\d+)/$", ExportedVMDelete.as_view(), name="dashboard.views.exportedvm-delete"),
url(r'^disk/(?P<pk>\d+)/remove/$', DiskRemoveView.as_view(), url(r'^disk/(?P<pk>\d+)/remove/$', DiskRemoveView.as_view(),
name="dashboard.views.disk-remove"), name="dashboard.views.disk-remove"),
url(r'^disk/(?P<pk>\d+)/status/$', get_disk_download_status, url(r'^disk/(?P<pk>\d+)/status/$', get_disk_download_status,
......
import json
import logging
import requests
from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.models import User, Group, Permission
from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.urls import reverse, reverse_lazy
from django.http import HttpResponse, Http404, JsonResponse
from django.shortcuts import redirect
from django.utils.translation import ugettext as _
from django.views.generic import UpdateView, TemplateView, DetailView
from django.views.generic.detail import SingleObjectMixin
from django_tables2 import SingleTableView
from django.db.models import Q
from itertools import chain
from django.utils.translation import ugettext as _, ugettext_noop
from . import GroupCodeMixin
from .util import (CheckedDetailView, AclUpdateView, search_user,
saml_available, DeleteViewBase,
AclUpdateView, FilterMixin,
TransferOwnershipConfirmView, TransferOwnershipView,
DeleteViewBase,
GraphMixin)
from ..forms import (
AddGroupMemberForm, AclUserOrGroupAddForm, GroupPermissionForm,
GroupCreateForm, GroupImportForm, GroupProfileUpdateForm, GroupExportForm,
ExamCreateForm, ExamListSearchForm, ExamForm
)
from ..models import FutureMember, GroupProfile
from ..store_api import Store, NoStoreException
from ..tables import ExamListTable
from exam.models import Exam
from dashboard.serializers import ExamSerializer
from vm.models import InstanceTemplate, Instance
from vm.operations import DestroyOperation, ShutdownOperation, ShutOffOperation, DeployOperation
class ExamList(LoginRequiredMixin, FilterMixin, SingleTableView):
template_name = "dashboard/exam-list.html"
model = Exam
table_class = ExamListTable
table_pagination = False
allowed_filters = {
'name': "name__icontains",
}
def get_context_data(self, *args, **kwargs):
context = super(ExamList, self).get_context_data(*args, **kwargs)
context['search_form'] = self.search_form
return context
def get(self, *args, **kwargs):
user = self.request.user
if not (user.has_perm('exam.create_exam') or user.has_perm('exam.start_exam')):
raise PermissionDenied()
self.search_form = ExamListSearchForm(self.request.GET)
self.search_form.full_clean()
if self.request.is_ajax():
exams = [{
'icon': i.os_type,
'system': i.system,
'url': reverse("dashboard.views.exam-detail",
kwargs={'pk': i.pk}),
'name': i.name} for i in self.get_queryset()]
return HttpResponse(
json.dumps(exams),
content_type="application/json",
)
else:
return super(ExamList, self).get(*args, **kwargs)
def create_acl_queryset(self, model):
queryset = super(ExamList, self).create_acl_queryset(model)
return queryset
def get_queryset(self):
qs = self.create_acl_queryset(Exam)
self.create_fake_get()
try:
filters, excludes = self.get_queryset_filters()
qs = qs.filter(**filters).exclude(**excludes).distinct()
except ValueError:
messages.error(self.request, _("Error during filtering."))
return qs.select_related("template", "owner", "owner__profile")
class ExamCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView):
form_class = ExamCreateForm
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/_modal.html']
else:
return ['dashboard/nojs-wrapper.html']
def get(self, request, form=None, *args, **kwargs):
if not request.user.has_perm('exam.create_exam'):
raise PermissionDenied()
if form is None:
form = self.form_class()
context = self.get_context_data(**kwargs)
context.update({
'template': 'dashboard/exam-create.html',
'box_title': _('Create an Exam'),
'form': form,
'ajax_title': True,
})
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
if not request.user.has_perm('exam.create_exam'):
raise PermissionDenied()
form = self.form_class(request.POST)
if not form.is_valid():
return self.get(request, form, *args, **kwargs)
form.cleaned_data
form.setowner(request.user)
savedform = form.save()
messages.success(request, _('Exam successfully created.'))
if request.is_ajax():
return HttpResponse(
json.dumps(
{'redirect': savedform.get_absolute_url()}
),
content_type="application/json"
)
else:
return redirect(savedform.get_absolute_url())
class ExamStart(LoginRequiredMixin, SuccessMessageMixin, DetailView):
model = Exam
def get(self, request, *args, **kwargs):
if not self.get_object().has_level(request.user, "operator"):
message = _("Only the operators can start the selected Exam VMs.")
messages.warning(request, message)
raise PermissionDenied()
else:
self.__start_vms(request)
return redirect(self.get_object().get_absolute_url())
def __start_vms(self, request):
exam: Exam = self.get_object()
if not exam.template:
messages.warning(request, "No template is set to start!")
return
owners = []
operators = []
for user, level in exam.get_users_with_level():
if level == "operator":
operators.append(user)
elif level == "owner":
owners.append(user)
users_have_vm = []
for instance in exam.instances.all():
users_have_vm.append(instance.owner)
if instance.state not in DeployOperation.deny_states:
DeployOperation(instance).call(user=self.request.user)
users_need_vm = set(exam.examinees.all()) - set(users_have_vm)
if users_need_vm:
missing_users, instances = Instance.mass_create_for_users(exam.template, users_need_vm, owners, operators)
for user in missing_users:
messages.warning(request, _('Error starting VM for user "%s".') % user)
for instance in instances:
exam.instances.add(instance)
class ExamVMShutdown(LoginRequiredMixin, SuccessMessageMixin, DetailView):
model = Exam
def get(self, request, *args, **kwargs):
if not self.get_object().has_level(request.user, "operator"):
message = _("Only the operators can shutdown the selected Exam VMs.")
messages.warning(request, message)
raise PermissionDenied()
else:
self.__shutdown_vms(request)
return redirect(self.get_object().get_absolute_url())
def __shutdown_vms(self, request):
exam: Exam = self.get_object()
for instance in exam.instances.all():
if instance.state in ShutdownOperation.accept_states:
ShutdownOperation(instance).call(user=self.request.user)
class ExamVMStop(LoginRequiredMixin, SuccessMessageMixin, DetailView):
model = Exam
def get(self, request, *args, **kwargs):
if not self.get_object().has_level(request.user, "operator"):
message = _("Only the operators can stop the selected Exam VMs.")
messages.warning(request, message)
raise PermissionDenied()
else:
self.__stop_vms(request)
return redirect(self.get_object().get_absolute_url())
def __stop_vms(self, request):
exam: Exam = self.get_object()
for instance in exam.instances.all():
if instance.state in ShutOffOperation.accept_states:
ShutOffOperation(instance).call(user=self.request.user)
class ExamVMDestroy(LoginRequiredMixin, SuccessMessageMixin, DetailView):
model = Exam
def get(self, request, *args, **kwargs):
if not self.get_object().has_level(request.user, "operator"):
message = _("Only the operators can destroy the selected Exam VMs.")
messages.warning(request, message)
raise PermissionDenied()
else:
self.__destroy_vms(request)
return redirect(self.get_object().get_absolute_url())
def __destroy_vms(self, request):
exam: Exam = self.get_object()
for instance in exam.instances.all():
if instance.destroyed_at:
exam.instances.remove(instance)
continue
DestroyOperation(instance).call(user=self.request.user)
exam.instances.remove(instance)
class ExamDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = Exam
form_class = ExamForm
template_name = "dashboard/exam-detail.html"
success_message = _("Successfully modified Exam.")
def get_context_data(self, *args, **kwargs):
obj = self.get_object()
context = super(ExamDetail, self).get_context_data(**kwargs)
exam = context['exam']
user = self.request.user
is_operator = exam.has_level(user, "operator")
is_owner = exam.has_level(user, "owner")
context = super(ExamDetail, self).get_context_data(*args, **kwargs)
context['is_operator'] = is_operator
context['is_owner'] = is_owner
template_queryset = ( InstanceTemplate.get_objects_with_level("operator", user).distinct())
context["templates"] = template_queryset
context['acl'] = AclUpdateView.get_acl_data(exam, user, 'dashboard.views.exam-detail.exam-acl')
context['aclform'] = AclUserOrGroupAddForm()
context['addmemberform'] = AddGroupMemberForm()
context['users'] = exam.examinees.all()
context['instances'] = exam.instances.all()
return context
def get_success_url(self):
return reverse_lazy("dashboard.views.exam-detail", kwargs=self.kwargs)
def get(self, request, *args, **kwargs):
if not self.get_object().has_level(request.user, "owner"):
message = _("Only the owners can modify the selected Exam.")
messages.warning(request, message)
return redirect(reverse_lazy("dashboard.views.exam-list"))
return super(ExamDetail, self).get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if not self.object.has_level(request.user, 'operator'):
raise PermissionDenied()
if request.POST.get('new_name'):
return self.__set_name(request)
if request.POST.get('new_description'):
return self.__set_description(request)
if request.POST.get('new-template'):
return self.__set_template(request)
if request.POST.get('new_member'):
return self.__set_member(request)
if request.POST.get('new_members'):
return self.__set_members(request)
return redirect(self.object.get_absolute_url())
def __set_name(self, request):
new_name = request.POST.get("new_name")
Exam.objects.filter(pk=self.object.pk).update(**{'name': new_name})
success_message = _("Exam successfully renamed.")
if request.is_ajax():
response = {
'message': success_message,
'new_name': new_name,
'exam_pk': self.object.pk
}
return HttpResponse(
json.dumps(response),
content_type="application/json"
)
else:
messages.success(request, success_message)
return redirect(self.object.get_absolute_url())
def __set_description(self, request):
new_description = request.POST.get("new_description")
Exam.objects.filter(pk=self.object.pk).update(**{'description': new_description})
success_message = _("Exam description successfully updated.")
if request.is_ajax():
response = {
'message': success_message,
'new_description': new_description,
'exam_pk': self.object.pk
}
return HttpResponse(
json.dumps(response),
content_type="application/json"
)
else:
messages.success(request, success_message)
return redirect(self.object.get_absolute_url())
def __set_template(self, request):
new_template_id = request.POST.get("new-template")
new_template = InstanceTemplate.objects.get(pk=new_template_id)
if not new_template:
raise PermissionDenied()
Exam.objects.filter(pk=self.object.pk).update(**{'template': new_template})
success_message = _("Exam template successfully changed.")
if request.is_ajax():
response = {
'message': success_message,
'new_template': new_template,
'exam_pk': self.object.pk
}
return HttpResponse(
json.dumps(response),
content_type="application/json"
)
else:
messages.success(request, success_message)
return redirect(self.object.get_absolute_url())
def __set_member(self, request):
new_member_name = request.POST.get("new_member")
new_member = search_user(new_member_name)
if not new_member:
raise PermissionDenied()
Exam.objects.get(pk=self.object.pk).examinees.add(new_member)
success_message = _("Exam users successfully changed.")
if request.is_ajax():
response = {
'message': success_message,
'new_member': new_member,
'exam_pk': self.object.pk
}
return HttpResponse(
json.dumps(response),
content_type="application/json"
)
else:
messages.success(request, success_message)
return redirect(self.object.get_absolute_url())
def __set_members(self, request):
new_member_names = request.POST.get("new_members").split('\r\n')
new_members = []
for name in new_member_names:
if not name:
return
try:
user = search_user(name)
Exam.objects.get(pk=self.object.pk).examinees.add(user)
new_members.append(name)
except User.DoesNotExist:
messages.warning(request, _('User "%s" not found.') % name)
success_message = _("Exam users successfully changed.")
if request.is_ajax():
response = {
'message': success_message,
'new_members': new_members,
'exam_pk': self.object.pk
}
return HttpResponse(
json.dumps(response),
content_type="application/json"
)
else:
messages.success(request, success_message)
return redirect(self.object.get_absolute_url())
class ExamDelete(DeleteViewBase):
model = Exam
success_message = _("Exam successfully deleted.")
def get_success_url(self):
return reverse("dashboard.views.exam-list")
def delete_obj(self, request, *args, **kwargs):
object = self.get_object()
object.delete()
class ExamRemoveAllUsersView(DeleteViewBase):
model = Exam
level = 'operator'
slug_field = 'pk'
slug_url_kwarg = 'exam_pk'
success_message = _("All users successfully removed from exam.")
def check_auth(self):
if not self.get_object().has_level(
self.request.user, self.level):
raise PermissionDenied()
def get_context_data(self, **kwargs):
context = super(ExamRemoveAllUsersView, self).get_context_data(**kwargs)
context['member'] = _("all examinees")
return context
def get_success_url(self):
return reverse_lazy("dashboard.views.exam-detail", kwargs={'pk': self.get_object().pk})
def delete_obj(self, request, *args, **kwargs):
exam = self.get_object()
exam.examinees.clear()
class ExamRemoveUserView(DeleteViewBase):
model = Exam
slug_field = 'pk'
slug_url_kwarg = 'exam_pk'
level = 'operator'
member_key = 'user_pk'
success_message = _("Member successfully removed from exam.")
def check_auth(self):
if not self.get_object().has_level(
self.request.user, self.level):
# self.request.user, self.level:
raise PermissionDenied()
def get_context_data(self, **kwargs):
context = super(ExamRemoveUserView, self).get_context_data(**kwargs)
try:
context['member'] = User.objects.get(pk=self.user_pk)
except User.DoesNotExist:
raise Http404()
return context
def get_success_url(self):
return reverse_lazy("dashboard.views.exam-detail", kwargs={'pk': self.get_object().pk})
def get(self, request, user_pk, *args, **kwargs):
self.user_pk = user_pk
return super(ExamRemoveUserView, self).get(request, *args, **kwargs)
def remove_member(self, pk):
exam = self.get_object()
exam.examinees.remove(User.objects.get(pk=pk))
def delete_obj(self, request, *args, **kwargs):
self.remove_member(kwargs[self.member_key])
class TransferExamOwnershipConfirmView(TransferOwnershipConfirmView):
template = "dashboard/confirm/transfer-exam-ownership.html"
model = Exam
class TransferExamOwnershipView(TransferOwnershipView):
confirm_view = TransferExamOwnershipConfirmView
model = Exam
notification_msg = ugettext_noop(
'%(owner)s offered you to take the ownership of '
'his/her exam called %(instance)s. '
'<a href="%(token)s" '
'class="btn btn-success btn-small">Accept</a>')
token_url = 'dashboard.views.exam-transfer-ownership-confirm'
template = "dashboard/exam-tx-owner.html"
required_permission_list = ["exam.create_exam", "exam.start_exam"]
...@@ -28,6 +28,7 @@ from braces.views import LoginRequiredMixin ...@@ -28,6 +28,7 @@ from braces.views import LoginRequiredMixin
from dashboard.models import GroupProfile from dashboard.models import GroupProfile
from vm.models import Instance, Node, InstanceTemplate from vm.models import Instance, Node, InstanceTemplate
from exam.models import Exam
from dashboard.views.vm import vm_ops from dashboard.views.vm import vm_ops
from ..store_api import Store from ..store_api import Store
...@@ -120,6 +121,12 @@ class IndexView(LoginRequiredMixin, TemplateView): ...@@ -120,6 +121,12 @@ class IndexView(LoginRequiredMixin, TemplateView):
else: else:
context['no_store'] = True context['no_store'] = True
# exams
if user.has_perm("exam.start_exam"):
exams = Exam.get_objects_with_level('user', user, disregard_superuser=True).all()
context['exams'] = exams[:5]
context['more_exams'] = exams.count() - len(exams[:5])
return context return context
......
...@@ -694,6 +694,8 @@ def absolute_url(url): ...@@ -694,6 +694,8 @@ def absolute_url(url):
class TransferOwnershipView(CheckedDetailView, DetailView): class TransferOwnershipView(CheckedDetailView, DetailView):
required_permissions = []
def get_template_names(self): def get_template_names(self):
if self.request.is_ajax(): if self.request.is_ajax():
return ['dashboard/_modal.html'] return ['dashboard/_modal.html']
...@@ -711,6 +713,14 @@ class TransferOwnershipView(CheckedDetailView, DetailView): ...@@ -711,6 +713,14 @@ class TransferOwnershipView(CheckedDetailView, DetailView):
}) })
return context return context
def _user_have_permission(self, user):
if self.required_permissions:
return True
else:
for permission in self.required_permissions:
if user.has_perm(permission):
return True
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
form = TransferOwnershipForm(request.POST) form = TransferOwnershipForm(request.POST)
if not form.is_valid(): if not form.is_valid():
...@@ -728,6 +738,10 @@ class TransferOwnershipView(CheckedDetailView, DetailView): ...@@ -728,6 +738,10 @@ class TransferOwnershipView(CheckedDetailView, DetailView):
request.user.is_superuser): request.user.is_superuser):
raise PermissionDenied() raise PermissionDenied()
if not self._user_have_permission(request.user):
messages.error(request, _("User doesn't have the required rights to own the object!"))
raise PermissionDenied()
token = signing.dumps( token = signing.dumps(
(obj.pk, new_owner.pk), (obj.pk, new_owner.pk),
salt=self.confirm_view.get_salt()) salt=self.confirm_view.get_salt())
......
# Copyright 2023 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
import random, string
from contextlib import contextmanager
from datetime import timedelta
from dbm import dumb
from functools import partial
from importlib import import_module
from ipaddress import ip_interface
from logging import getLogger
from urllib import request
from warnings import warn
from xml.dom.minidom import Text
import django.conf
from django.contrib.auth.models import User
from django.core import signing
from django.core.exceptions import PermissionDenied
from django.db.models import (BooleanField, CharField, DateTimeField,
IntegerField, ForeignKey, Manager,
ManyToManyField, SET_NULL, TextField, Model)
from django.db import IntegrityError
from django.dispatch import Signal
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext_noop
import jinja2
from passlib.hash import sha512_crypt
import yaml
import urllib3, json
from model_utils import Choices
from model_utils.managers import QueryManager
from model_utils.models import TimeStampedModel, StatusModel
from taggit.managers import TaggableManager
from simplesshkey.models import UserKey
from django.db import models
from acl.models import AclBase
from django import forms
from common.models import (
activitycontextimpl, create_readable, HumanReadableException,
)
from common.operations import OperatedMixin
from storage.models import DataStore
from vm.models import Instance, InstanceTemplate, Lease, Interface, Node, InstanceActivity
class Exam(AclBase):
"""Exam template.
"""
ACL_LEVELS = (
('user', _('user')), # see all details
('operator', _('operator')),
('owner', _('owner')), # superuser, can delete, delegate perms
)
name = CharField(max_length=100, verbose_name=_('name'), help_text=_('Human readable name of the exam.'))
description = TextField(verbose_name=_('description'), blank=True)
template = ForeignKey(InstanceTemplate, on_delete=models.CASCADE, null=True)
examinees = ManyToManyField(User, related_name="examinees")
instances = ManyToManyField(Instance, related_name="instances")
owner = ForeignKey(User, on_delete=models.CASCADE)
class Meta:
app_label = 'exam'
db_table = 'exam'
ordering = ('name', )
permissions = (
('create_exam', _('Can create an instance temp late.')),
('start_exam', _('Can start VMs for a group.')),
)
verbose_name = _('exam')
verbose_name_plural = _('exams')
def get_absolute_url(self):
return reverse('dashboard.views.exam-detail', kwargs=({'pk': self.pk}))
def __str__(self):
return self.name
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