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 = {
"dashboard/dashboard.js",
"dashboard/activity.js",
"dashboard/group-details.js",
"dashboard/exam-list.js",
"dashboard/exam-details.js",
"dashboard/group-list.js",
"dashboard/js/stupidtable.min.js", # no bower file
"dashboard/node-create.js",
......@@ -396,6 +398,7 @@ LOCAL_APPS = (
'acl',
'monitor',
'request',
'exam',
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
......@@ -614,6 +617,7 @@ STORE_IDENTITY_FILE = get_env_variable("STORE_IDENTITY_FILE", "")
# possible options are "scp", or "rsync"
STORE_SSH_MODE = get_env_variable("STORE_SSH_MODE", "scp")
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) ^
(getnode() % 983)) & 0xffff)
......
......@@ -18,6 +18,7 @@ from urllib.parse import urlparse
import os
import pyotp
import requests
from crispy_forms.bootstrap import FormActions
from crispy_forms.helper import FormHelper
from crispy_forms.layout import (
......@@ -27,6 +28,7 @@ from crispy_forms.utils import render_field
from dal import autocomplete
from datetime import timedelta
from django import forms
from django.contrib import messages
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth.forms import (
AuthenticationForm, PasswordResetForm, SetPasswordForm,
......@@ -56,7 +58,9 @@ from storage.models import DataStore, Disk
from vm.models import (
InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, Instance
)
from exam.models import Exam
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
LANGUAGES_WITH_CODE = ((l[0], format_lazy("{} ({})",l[1], l[0]))
......@@ -256,6 +260,35 @@ class GroupCreateForm(NoFormTagMixin, forms.ModelForm):
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):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
......@@ -907,6 +940,162 @@ class VmDiskExportForm(OperationForm):
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):
size = forms.CharField(
widget=FileSizeWidget, initial=(10 << 30), label=_('Size'),
......@@ -1726,6 +1915,27 @@ class TemplateListSearchForm(forms.Form):
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):
use_required_attribute = False
......
......@@ -63,6 +63,7 @@ logger = getLogger(__name__)
from django.conf import settings
from django.db.models.signals import post_save
from django.contrib import messages
from django.dispatch import receiver
from rest_framework.authtoken.models import Token
......@@ -361,13 +362,19 @@ class GroupProfile(AclBase):
user = Profile.objects.get(org_id=org_id).user
user.groups.add(group)
except ObjectDoesNotExist:
future_member = FutureMember(org_id=org_id, group=group)
future_member.save()
try:
future_member = FutureMember(org_id=org_id, group=group)
future_member.save()
except Exception as e:
raise Exception("Can't find user %s!" % org_id)
for permission in data["permissions"]:
group.permissions.add(
Permission.objects.get_by_natural_key(*permission)
)
try:
group.permissions.add(
Permission.objects.get_by_natural_key(*permission)
)
except Exception as e:
messages.warning("Couldn't add %s permission to group!" % permission)
return group.profile
except (KeyError, ValueError, TypeError):
......
......@@ -7,6 +7,7 @@ from vm.models import Instance, InstanceTemplate, Lease, Interface, Node, Instan
from firewall.models import Vlan, Rule
from storage.models import Disk, StorageActivity
from vm.models.common import Variable
from exam.models import Exam
class RuleSerializer(serializers.ModelSerializer):
class Meta:
......@@ -35,6 +36,13 @@ class GroupSerializer(serializers.ModelSerializer):
model = Group
fields = ('id', 'name', 'user_set')
class ExamSerializer(serializers.ModelSerializer):
class Meta:
model = Exam
fields = ('id', 'name', 'owner', 'description', 'template')
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
......
......@@ -172,7 +172,9 @@ html {
#group-details-rename *,
#group-details-h1-name,
#group-list-rename,
#group-list-rename * {
#group-list-rename *,
#exam-list-rename,
#exam-list-rename *{
display: inline;
}
#vm-details-rename,
......@@ -180,10 +182,13 @@ html {
#node-details-rename,
#node-list-rename,
#group-details-rename,
#group-list-rename {
#group-list-rename,
#exam-details-rename,
#exam-list-rename {
display: none;
}
#group-details-rename-form {
#group-details-rename-form,
#exam-details-rename-form {
display: inline-block;
}
.vm-details-home-name,
......
......@@ -201,16 +201,16 @@ html {
}
#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;
}
#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;
}
#group-details-rename-form {
#group-details-rename-form, #exam-details-rename-form {
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
from storage.models import Disk
from vm.models import Node, InstanceTemplate, Lease
from dashboard.models import ConnectCommand, Message
from exam.models import Exam
class FileSizeColumn(Column):
......@@ -152,6 +153,39 @@ class GroupListTable(Table):
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):
username = LinkColumn(
'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 @@
{% blocktrans with owner=instance.owner name=instance.name id=instance.id%}
<strong>{{ owner }}</strong> offered to take the ownership of
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 %}
<div class="pull-right">
<form action="" method="POST">
......
......@@ -13,7 +13,7 @@
{% blocktrans with owner=instance.owner name=instance.name id=instance.id%}
<strong>{{ owner }}</strong> offered to take the ownership of
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 %}
<div class="pull-right">
<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 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 @@
{% include "dashboard/index-users.html" %}
</div>
{% 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>
{% endblock %}
......@@ -21,6 +21,7 @@ from django.conf.urls import url
from django.urls import path
from vm.models import Instance
from exam.models import Exam
from .views import (
AclUpdateView, FavouriteView, GroupAclUpdateView, GroupDelete,
GroupDetailView, GroupList, IndexView,
......@@ -28,7 +29,7 @@ from .views import (
MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete,
NodeDetailView, NodeList, NodeActivityDetail,
NotificationView, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList,
TemplateDelete, TemplateDetail, TemplateList, TemplateImport,
vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmList,
DiskRemoveView, get_disk_download_status,
......@@ -53,8 +54,8 @@ from .views import (
TransferInstanceOwnershipView, TransferInstanceOwnershipConfirmView,
TransferTemplateOwnershipView, TransferTemplateOwnershipConfirmView,
OpenSearchDescriptionView,
NodeActivityView,
UserList, TemplateREST, LeaseREST, DiskRest, InstanceREST,
NodeActivityView,
UserList, TemplateREST, LeaseREST, DiskRest, InstanceREST,
InterfaceREST, InstanceFromTemplateREST, InstanceFTforUsersREST,
DownloadDiskREST, GetInstanceREST, GetInterfaceREST, ShutdownInstanceREST,
GetLeaseREST, GetDiskRest, DeployInstanceREST, CreateDiskREST,
......@@ -68,7 +69,10 @@ from .views import (
EnableTwoFactorView, DisableTwoFactorView,
AclUserGroupAutocomplete, AclUserAutocomplete,
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.vm import vm_ops, vm_mass_ops
......@@ -132,6 +136,8 @@ urlpatterns = [
url(r'^template/create/$', TemplateCreate.as_view(),
name="dashboard.views.template-create"),
url(r'^template/import/$', TemplateImport.as_view(),
name="dashboard.views.template-import"),
url(r'^template/choose/$', TemplateChoose.as_view(),
name="dashboard.views.template-choose"),
url(r'template/(?P<pk>\d+)/acl/$', TemplateAclUpdateView.as_view(),
......@@ -222,6 +228,22 @@ urlpatterns = [
url(r'^notifications/$', NotificationView.as_view(),
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(),
name="dashboard.views.disk-remove"),
url(r'^disk/(?P<pk>\d+)/status/$', get_disk_download_status,
......
......@@ -28,6 +28,7 @@ from braces.views import LoginRequiredMixin
from dashboard.models import GroupProfile
from vm.models import Instance, Node, InstanceTemplate
from exam.models import Exam
from dashboard.views.vm import vm_ops
from ..store_api import Store
......@@ -120,6 +121,12 @@ class IndexView(LoginRequiredMixin, TemplateView):
else:
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
......
......@@ -694,6 +694,8 @@ def absolute_url(url):
class TransferOwnershipView(CheckedDetailView, DetailView):
required_permissions = []
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/_modal.html']
......@@ -711,6 +713,14 @@ class TransferOwnershipView(CheckedDetailView, DetailView):
})
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):
form = TransferOwnershipForm(request.POST)
if not form.is_valid():
......@@ -728,6 +738,10 @@ class TransferOwnershipView(CheckedDetailView, DetailView):
request.user.is_superuser):
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(
(obj.pk, new_owner.pk),
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