Commit 12a8d210 by Guba Sándor

Merge branch 'feature-request' into 'master'

Request 

🙏

TODO
- send notifications
- send request to irc bot
- reason for decline?

See merge request !303
parents 8d01770a 41bf99d6
...@@ -355,6 +355,7 @@ LOCAL_APPS = ( ...@@ -355,6 +355,7 @@ LOCAL_APPS = (
'manager', 'manager',
'acl', 'acl',
'monitor', 'monitor',
'request',
) )
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
...@@ -555,3 +556,4 @@ ADMIN_ENABLED = False ...@@ -555,3 +556,4 @@ ADMIN_ENABLED = False
BLACKLIST_PASSWORD = get_env_variable("BLACKLIST_PASSWORD", "") BLACKLIST_PASSWORD = get_env_variable("BLACKLIST_PASSWORD", "")
BLACKLIST_HOOK_URL = get_env_variable("BLACKLIST_HOOK_URL", "") BLACKLIST_HOOK_URL = get_env_variable("BLACKLIST_HOOK_URL", "")
REQUEST_HOOK_URL = get_env_variable("REQUEST_HOOK_URL", "")
...@@ -56,6 +56,16 @@ LOGGING['handlers']['console'] = {'level': level, ...@@ -56,6 +56,16 @@ LOGGING['handlers']['console'] = {'level': level,
'formatter': 'simple'} 'formatter': 'simple'}
for i in LOCAL_APPS: for i in LOCAL_APPS:
LOGGING['loggers'][i] = {'handlers': ['console'], 'level': level} LOGGING['loggers'][i] = {'handlers': ['console'], 'level': level}
# don't print SQL queries
LOGGING['handlers']['null'] = {'level': "DEBUG",
'class': "django.utils.log.NullHandler"}
LOGGING['loggers']['django.db.backends'] = {
'handlers': ['null'],
'propagate': False,
'level': 'DEBUG',
}
# Forbid store usage # Forbid store usage
STORE_URL = "" STORE_URL = ""
......
...@@ -38,6 +38,7 @@ urlpatterns = patterns( ...@@ -38,6 +38,7 @@ urlpatterns = patterns(
url(r'^network/', include('network.urls')), url(r'^network/', include('network.urls')),
url(r'^blacklist-add/', add_blacklist_item), url(r'^blacklist-add/', add_blacklist_item),
url(r'^dashboard/', include('dashboard.urls')), url(r'^dashboard/', include('dashboard.urls')),
url(r'^request/', include('request.urls')),
# django/contrib/auth/urls.py (care when new version) # django/contrib/auth/urls.py (care when new version)
url((r'^accounts/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/' url((r'^accounts/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/'
......
...@@ -1395,6 +1395,7 @@ ...@@ -1395,6 +1395,7 @@
"vnc_port": 1234, "vnc_port": 1234,
"num_cores": 2, "num_cores": 2,
"status": "RUNNING", "status": "RUNNING",
"system": "system pls",
"modified": "2013-10-14T07:27:38.192Z" "modified": "2013-10-14T07:27:38.192Z"
} }
}, },
......
...@@ -1272,8 +1272,46 @@ textarea[name="new_members"] { ...@@ -1272,8 +1272,46 @@ textarea[name="new_members"] {
margin-top: 20px; margin-top: 20px;
} }
#vm-renew-request-lease, #vm-request-resource-form {
display: none;
}
.label-100 {
display: block;
width: 100%;
}
#modify-the-resources {
font-size: 18px;
display: none;
}
#vm-request-resource-form textarea {
max-width: 500px;
height: 150px;
}
#disk-list-table { #disk-list-table {
td:last-child { td:last-child {
text-align: center; text-align: center;
} }
} }
#request-buttons {
form {
display: inline;
}
textarea {
resize: none;
min-height: 80px;
}
}
.nowrap {
white-space: nowrap;
}
.little-margin-bottom {
margin-bottom: 5px;
}
...@@ -223,4 +223,25 @@ $(function() { ...@@ -223,4 +223,25 @@ $(function() {
return false; return false;
}); });
$(document).on("click", "#vm-renew-request-lease-button", function(e) {
$("#vm-renew-request-lease").stop().slideToggle();
e.preventDefault();
});
$("#vm-request-resource").click(function(e) {
$(".cpu-priority-slider, .cpu-count-slider, .ram-slider").simpleSlider("setDisabled", false);
$(".ram-input, .cpu-count-input, .cpu-priority-input").prop("disabled", false);
$("#vm-details-resources-form").prop("action", $(this).prop("href"));
$("#vm-request-resource-form").show();
$("#modify-the-resources").show();
$(this).hide();
$("html, body").animate({
scrollTop: $("#modify-the-resources").offset().top - 60
});
return e.preventDefault();
});
}); });
...@@ -79,10 +79,26 @@ ...@@ -79,10 +79,26 @@
</div> </div>
</div> </div>
{% empty %} {% empty %}
{% trans "You can't start new virtual machines because no templates are shared with you." %} {% if not template_access_types %}
{% trans "You can't start new virtual machines because no templates are shared with you." %}
{% else %}
{% trans "You can't start new virtual machines because no templates are shared with you however you can request them via the form below." %}
<hr />
{% include "request/_request-template-form.html" %}
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% if templates and template_access_types %}
{% url "request.views.request-template" as request_url %}
<hr />
<p class="text-right">
{% blocktrans with url=request_url %}
Need other templates? Submit a new <a href="{{ url }}">request</a>.
{% endblocktrans %}
</p>
{% endif %}
<style> <style>
.progress { .progress {
position: relative; position: relative;
......
{% extends "dashboard/operate.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block formbuttons %}
<div class="pull-right">
<a class="btn btn-default" href="{{object.get_absolute_url}}" data-dismiss="modal">
{% trans "Cancel" %}
</a>
<a class="btn btn-primary" id="vm-renew-request-lease-button"
href="{% url "request.views.request-lease" vm_pk=object.pk %}">
<i class="fa fa-forward"></i>
{% trans "Request longer lease" %}
</a>
<button class="btn btn-{{ opview.effect }} btn-op-form-send" type="submit" id="op-form-send">
{% if opview.icon %}<i class="fa fa-fw fa-{{opview.icon}}"></i> {% endif %}{{ op.name|capfirst }}
</button>
</div>
{% endblock %}
{% block extra %}
<div class="clearfix"></div>
<div id="vm-renew-request-lease">
<hr />
{% include "request/_request-lease-form.html" with form=lease_request_form vm=object %}
</div>
{% endblock %}
...@@ -22,16 +22,29 @@ ...@@ -22,16 +22,29 @@
{% if user.is_superuser %} {% if user.is_superuser %}
{% if ADMIN_ENABLED %} {% if ADMIN_ENABLED %}
<li> <li>
<a href="/admin/"><i class="fa fa-cogs"></i> {% trans "Admin" %}</a> <a href="/admin/">
<i class="fa fa-cogs"></i>
<span class="hidden-sm">{% trans "Admin" %}</span>
</a>
</li> </li>
{% endif %} {% endif %}
<li> <li>
<a href="{% url "dashboard.views.storage" %}"><i class="fa fa-database"></i> <a href="{% url "dashboard.views.storage" %}">
{% trans "Storage" %} <i class="fa fa-database"></i>
<span class="hidden-sm">{% trans "Storage" %}</span>
</a> </a>
</li> </li>
<li> <li>
<a href="/network/"><i class="fa fa-globe"></i> {% trans "Network" %}</a> <a href="{% url "network.index" %}">
<i class="fa fa-globe"></i>
<span class="hidden-sm">{% trans "Network" %}</span>
</a>
</li>
<li>
<a href="{% url "request.views.request-list" %}">
<i class="fa fa-phone"></i>
<span class="hidden-sm">{% trans "Requests" %}</span>
</a>
</li> </li>
{% endif %} {% endif %}
<li> <li>
......
...@@ -16,6 +16,7 @@ Do you want to perform the following operation on ...@@ -16,6 +16,7 @@ Do you want to perform the following operation on
{% crispy form %} {% crispy form %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block formbuttons %}
<div class="pull-right"> <div class="pull-right">
<a class="btn btn-default" href="{{object.get_absolute_url}}" <a class="btn btn-default" href="{{object.get_absolute_url}}"
data-dismiss="modal">{% trans "Cancel" %}</a> data-dismiss="modal">{% trans "Cancel" %}</a>
...@@ -23,4 +24,7 @@ Do you want to perform the following operation on ...@@ -23,4 +24,7 @@ Do you want to perform the following operation on
{% if opview.icon %}<i class="fa fa-fw fa-{{opview.icon}}"></i> {% endif %}{{ op.name|capfirst }} {% if opview.icon %}<i class="fa fa-fw fa-{{opview.icon}}"></i> {% endif %}{{ op.name|capfirst }}
</button> </button>
</div> </div>
{% endblock %}
</form> </form>
{% block extra %}{% endblock %}
...@@ -59,10 +59,11 @@ ...@@ -59,10 +59,11 @@
{% if instance.is_expiring %}<i class="fa fa-warning-sign text-danger"></i>{% endif %} {% if instance.is_expiring %}<i class="fa fa-warning-sign text-danger"></i>{% endif %}
<span id="vm-details-renew-op"> <span id="vm-details-renew-op">
{% with op=op.renew %}{% if op %} {% with op=op.renew %}{% if op %}
<a href="{{op.get_url}}" class="btn btn-success btn-xs <a href="{{op.get_url}}" class="btn btn-{{op.effect}} btn-xs
operation operation-{{op.op}}"> operation operation-{{op.op}}">
<i class="fa fa-{{op.icon}}"></i> <i class="fa fa-{{op.icon}}"></i>
{{op.name}} </a> {{op.name}}
</a>
{% endif %}{% endwith %} {% endif %}{% endwith %}
</span> </span>
</h4> </h4>
......
...@@ -2,19 +2,42 @@ ...@@ -2,19 +2,42 @@
{% load sizefieldtags %} {% load sizefieldtags %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
<div class="label label-info label-100" id="modify-the-resources">
{% trans "Modify the resources" %}
</div>
<form method="POST" action="{{ op.resources_change.get_url }}" id="vm-details-resources-form"> <form method="POST" action="{{ op.resources_change.get_url }}" id="vm-details-resources-form">
{% csrf_token %} {% csrf_token %}
{% include "dashboard/_resources-sliders.html" with field_priority=resources_form.priority field_num_cores=resources_form.num_cores field_ram_size=resources_form.ram_size %} {% include "dashboard/_resources-sliders.html" with field_priority=resources_form.priority field_num_cores=resources_form.num_cores field_ram_size=resources_form.ram_size %}
{% if op.resources_change %} {% if op.resources_change %}
<button type="submit" class="btn btn-success btn-sm change-resources-button" <button type="submit" class="btn btn-success btn-sm change-resources-button"
id="vm-details-resources-save" data-vm="{{ instance.pk }}" id="vm-details-resources-save" data-vm="{{ instance.pk }}"
{% if op.resources_change.disabled %}disabled{% endif %}> {% if op.resources_change.disabled %}disabled{% endif %}>
<i class="fa fa-floppy-o"></i> {% trans "Save resources" %} <i class="fa fa-floppy-o"></i> {% trans "Save resources" %}
</button> </button>
<span class="change-resources-help" <span class="change-resources-help"
{% if not op.resources_change.disabled %}style="display: none;"{% endif %} {% if not op.resources_change.disabled %}style="display: none;"{% endif %}>
>{% trans "Stop your VM to change resources." %}</span> {% trans "Stop your VM to change resources." %}
</span>
{% else %}
<div id="vm-request-resource-form">
<div class="alert alert-info text-justify">
{% trans "Changing resources is only possible on virtual machines with STOPPED state. We suggest to turn off the VM after submitting the request otherwise it will be automatically stopped in the future when the request is accepted." %}
</div>
<div class="form-group">
<label>{% trans "Message" %}*</label>
<textarea class="form-control" name="message">{% include "request/initials/resources.html" %}</textarea>
</div>
<input type="submit" class="btn btn-success btn-sm"/>
</div>
<a href="{% url "request.views.request-resource" vm_pk=object.pk %}"
class="btn btn-primary btn-sm" id="vm-request-resource">
<i class="fa fa-tasks"></i>
{% trans "Request resources" %}
</a>
{% endif %} {% endif %}
</form> </form>
......
...@@ -20,8 +20,7 @@ import json ...@@ -20,8 +20,7 @@ import json
# from unittest import skip # from unittest import skip
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group, Permission
from django.contrib.auth.models import Permission
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from common.tests.celery_mock import MockCeleryMixin from common.tests.celery_mock import MockCeleryMixin
......
...@@ -228,7 +228,6 @@ urlpatterns = patterns( ...@@ -228,7 +228,6 @@ urlpatterns = patterns(
url(r'^vm/opensearch.xml$', OpenSearchDescriptionView.as_view(), url(r'^vm/opensearch.xml$', OpenSearchDescriptionView.as_view(),
name="dashboard.views.vm-opensearch"), name="dashboard.views.vm-opensearch"),
url(r'^storage/$', StorageDetail.as_view(), url(r'^storage/$', StorageDetail.as_view(),
name="dashboard.views.storage"), name="dashboard.views.storage"),
url(r'^disk/(?P<pk>\d+)/$', DiskDetail.as_view(), url(r'^disk/(?P<pk>\d+)/$', DiskDetail.as_view(),
......
...@@ -13,3 +13,4 @@ from util import * ...@@ -13,3 +13,4 @@ from util import *
from vm import * from vm import *
from graph import * from graph import *
from storage import * from storage import *
from request import *
...@@ -66,6 +66,8 @@ from ..forms import ( ...@@ -66,6 +66,8 @@ from ..forms import (
VmPortRemoveForm, VmPortAddForm, VmPortRemoveForm, VmPortAddForm,
VmRemoveInterfaceForm, VmRemoveInterfaceForm,
) )
from request.models import TemplateAccessType
from request.forms import LeaseRequestForm, TemplateRequestForm
from ..models import Favourite from ..models import Favourite
from manager.scheduler import has_traits from manager.scheduler import has_traits
...@@ -651,10 +653,12 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView): ...@@ -651,10 +653,12 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
op = 'renew' op = 'renew'
icon = 'calendar' icon = 'calendar'
effect = 'info' effect = 'success'
show_in_toolbar = False show_in_toolbar = False
form_class = VmRenewForm form_class = VmRenewForm
wait_for_result = 0.5 wait_for_result = 0.5
template_name = 'dashboard/_vm-renew.html'
with_reload = True
def get_form_kwargs(self): def get_form_kwargs(self):
choices = Lease.get_objects_with_level("user", self.request.user) choices = Lease.get_objects_with_level("user", self.request.user)
...@@ -674,6 +678,11 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView): ...@@ -674,6 +678,11 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
instance.time_of_suspend) instance.time_of_suspend)
return extra return extra
def get_context_data(self, **kwargs):
context = super(VmRenewView, self).get_context_data(**kwargs)
context['lease_request_form'] = LeaseRequestForm(request=self.request)
return context
class VmStateChangeView(FormOperationMixin, VmOperationView): class VmStateChangeView(FormOperationMixin, VmOperationView):
op = 'emergency_change_state' op = 'emergency_change_state'
...@@ -1043,6 +1052,8 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -1043,6 +1052,8 @@ class VmCreate(LoginRequiredMixin, TemplateView):
'box_title': _('Create a VM'), 'box_title': _('Create a VM'),
'ajax_title': True, 'ajax_title': True,
'templates': templates.all(), 'templates': templates.all(),
'template_access_types': TemplateAccessType.objects.exists(),
'form': TemplateRequestForm(request=request),
}) })
return self.render_to_response(context) return self.render_to_response(context)
......
# Copyright 2014 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/>.
from django.forms import (
ModelForm, ModelChoiceField, ChoiceField, Form, CharField, RadioSelect,
Textarea,
)
from django.utils.translation import ugettext_lazy as _
from django.template import RequestContext
from django.template.loader import render_to_string
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from request.models import (
LeaseType, TemplateAccessType, TemplateAccessAction,
)
from dashboard.forms import VmResourcesForm
class LeaseTypeForm(ModelForm):
@property
def helper(self):
helper = FormHelper()
helper.add_input(Submit("submit", _("Save"),
css_class="btn btn-success", ))
return helper
class Meta:
model = LeaseType
fields = ["name", "lease", ]
class TemplateAccessTypeForm(ModelForm):
def __init__(self, *args, **kwargs):
super(TemplateAccessTypeForm, self).__init__(*args, **kwargs)
@property
def helper(self):
helper = FormHelper()
helper.add_input(Submit("submit", _("Save"),
css_class="btn btn-success", ))
return helper
class Meta:
model = TemplateAccessType
fields = ["name", "templates", ]
class InitialFromFileMixin(object):
def __init__(self, *args, **kwargs):
request = kwargs.pop("request", None)
super(InitialFromFileMixin, self).__init__(*args, **kwargs)
self.initial['message'] = render_to_string(
self.initial_template,
RequestContext(request, {}),
)
class TemplateRequestForm(InitialFromFileMixin, Form):
template = ModelChoiceField(TemplateAccessType.objects.all(),
label=_("Template share"))
level = ChoiceField(TemplateAccessAction.LEVELS, widget=RadioSelect,
initial=TemplateAccessAction.LEVELS.user)
message = CharField(widget=Textarea, label=_("Message"))
initial_template = "request/initials/template.html"
class LeaseRequestForm(InitialFromFileMixin, Form):
lease = ModelChoiceField(LeaseType.objects.all(), label=_("Lease"))
message = CharField(widget=Textarea)
initial_template = "request/initials/lease.html"
class ResourceRequestForm(InitialFromFileMixin, VmResourcesForm):
message = CharField(widget=Textarea)
initial_template = "request/initials/resources.html"
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.utils.timezone
from django.conf import settings
import model_utils.fields
import django.core.validators
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0001_initial'),
('vm', '0002_interface_model'),
]
operations = [
migrations.CreateModel(
name='ExtendLeaseAction',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('instance', models.ForeignKey(to='vm.Instance')),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='LeaseType',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=25)),
('lease', models.ForeignKey(to='vm.Lease')),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Request',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)),
('status', models.CharField(default=b'PENDING', max_length=10, choices=[(b'PENDING', 'pending'), (b'ACCEPTED', 'accepted'), (b'DECLINED', 'declined')])),
('type', models.CharField(max_length=10, choices=[(b'resource', 'resource request'), (b'lease', 'lease request'), (b'template', 'template access')])),
('message', models.TextField(verbose_name='Message')),
('reason', models.TextField(verbose_name='Reason')),
('object_id', models.IntegerField()),
('closed_by', models.ForeignKey(related_name='closed_by', to=settings.AUTH_USER_MODEL, null=True)),
('content_type', models.ForeignKey(to='contenttypes.ContentType')),
('user', models.ForeignKey(related_name='user', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='ResourceChangeAction',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('num_cores', models.IntegerField(help_text='Number of virtual CPU cores available to the virtual machine.', verbose_name='number of cores', validators=[django.core.validators.MinValueValidator(0)])),
('ram_size', models.IntegerField(help_text='Mebibytes of memory.', verbose_name='RAM size', validators=[django.core.validators.MinValueValidator(0)])),
('priority', models.IntegerField(help_text='CPU priority.', verbose_name='priority', validators=[django.core.validators.MinValueValidator(0)])),
('instance', models.ForeignKey(to='vm.Instance')),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='TemplateAccessAction',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('level', models.CharField(default=b'user', max_length=10, choices=[(b'user', 'user'), (b'operator', 'operator')])),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='TemplateAccessType',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=25)),
('templates', models.ManyToManyField(to='vm.InstanceTemplate')),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.AddField(
model_name='templateaccessaction',
name='template_type',
field=models.ForeignKey(to='request.TemplateAccessType'),
preserve_default=True,
),
migrations.AddField(
model_name='templateaccessaction',
name='user',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
preserve_default=True,
),
migrations.AddField(
model_name='extendleaseaction',
name='lease_type',
field=models.ForeignKey(to='request.LeaseType'),
preserve_default=True,
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('request', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='leasetype',
name='lease',
field=models.ForeignKey(verbose_name='Lease', to='vm.Lease'),
preserve_default=True,
),
migrations.AlterField(
model_name='leasetype',
name='name',
field=models.CharField(max_length=25, verbose_name='Name'),
preserve_default=True,
),
migrations.AlterField(
model_name='templateaccesstype',
name='name',
field=models.CharField(max_length=25, verbose_name='Name'),
preserve_default=True,
),
migrations.AlterField(
model_name='templateaccesstype',
name='templates',
field=models.ManyToManyField(to='vm.InstanceTemplate', verbose_name='Templates'),
preserve_default=True,
),
]
# Copyright 2014 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 json
import logging
from django.db.models import (
Model, CharField, IntegerField, TextField, ForeignKey, ManyToManyField,
)
from django.db.models.signals import post_save
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey