Commit 1c48024b by Őry Máté

Update to master

Merge branch 'master' into feature-vm-gc

Conflicts:
	circle/dashboard/views.py
parents a24f503e 05e64bb7
============= ============
cirecle-cloud circle-cloud
============= ============
This is the Django based controller and web portal of the CIRCLE Cloud. This is the Django based controller and web portal of the CIRCLE Cloud.
\ No newline at end of file
...@@ -339,7 +339,6 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': ...@@ -339,7 +339,6 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
'djangosaml2.backends.Saml2Backend', 'djangosaml2.backends.Saml2Backend',
) )
LOGIN_URL = '/saml2/login/'
remote_metadata = join(SITE_ROOT, 'remote_metadata.xml') remote_metadata = join(SITE_ROOT, 'remote_metadata.xml')
if not isfile(remote_metadata): if not isfile(remote_metadata):
...@@ -388,6 +387,8 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': ...@@ -388,6 +387,8 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
'DJANGO_SAML_GROUP_OWNER_ATTRIBUTES', '').split(',') 'DJANGO_SAML_GROUP_OWNER_ATTRIBUTES', '').split(',')
SAML_CREATE_UNKNOWN_USER = True SAML_CREATE_UNKNOWN_USER = True
if get_env_variable('DJANGO_SAML_ORG_ID_ATTRIBUTE', False) != False: if get_env_variable('DJANGO_SAML_ORG_ID_ATTRIBUTE', False) is not False:
SAML_ORG_ID_ATTRIBUTE = get_env_variable( SAML_ORG_ID_ATTRIBUTE = get_env_variable(
'DJANGO_SAML_ORG_ID_ATTRIBUTE') 'DJANGO_SAML_ORG_ID_ATTRIBUTE')
LOGIN_REDIRECT_URL = "/"
...@@ -6,6 +6,8 @@ from django.shortcuts import redirect ...@@ -6,6 +6,8 @@ from django.shortcuts import redirect
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from circle.settings.base import get_env_variable from circle.settings.base import get_env_variable
from dashboard.views import circle_login
from dashboard.forms import CirclePasswordResetForm, CircleSetPasswordForm
admin.autodiscover() admin.autodiscover()
...@@ -23,6 +25,19 @@ urlpatterns = patterns( ...@@ -23,6 +25,19 @@ urlpatterns = patterns(
url(r'^admin/', include(admin.site.urls)), url(r'^admin/', include(admin.site.urls)),
url(r'^network/', include('network.urls')), url(r'^network/', include('network.urls')),
url(r'^dashboard/', include('dashboard.urls')), url(r'^dashboard/', include('dashboard.urls')),
url((r'^accounts/reset/(?P<uidb36>[0-9A-Za-z]{1,13})-'
'(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$'),
'django.contrib.auth.views.password_reset_confirm',
{'set_password_form': CircleSetPasswordForm},
name='accounts.password_reset_confirm'
),
url(r'^accounts/password/reset/$', ("django.contrib.auth.views."
"password_reset"),
{'password_reset_form': CirclePasswordResetForm},
name="accounts.password-reset",
),
url(r'^accounts/login/?$', circle_login, name="accounts.login"),
url(r'^accounts/', include('django.contrib.auth.urls')), url(r'^accounts/', include('django.contrib.auth.urls')),
) )
......
...@@ -2,6 +2,9 @@ from datetime import timedelta ...@@ -2,6 +2,9 @@ from datetime import timedelta
import uuid import uuid
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.forms import (
AuthenticationForm, PasswordResetForm, SetPasswordForm,
)
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import ( from crispy_forms.layout import (
...@@ -484,6 +487,16 @@ class TemplateForm(forms.ModelForm): ...@@ -484,6 +487,16 @@ class TemplateForm(forms.ModelForm):
return User.objects.get(pk=self.instance.owner.pk) return User.objects.get(pk=self.instance.owner.pk)
return self.user return self.user
def clean(self):
cleaned_data = self.cleaned_data
# if raw_data has changed and the user is not superuser
if "raw_data" in self.changed_data and not self.user.is_superuser:
old_raw_data = InstanceTemplate.objects.get(
pk=self.instance.pk).raw_data
cleaned_data['raw_data'] = old_raw_data
return cleaned_data
def save(self, commit=True): def save(self, commit=True):
data = self.cleaned_data data = self.cleaned_data
self.instance.max_ram_size = data.get('ram_size') self.instance.max_ram_size = data.get('ram_size')
...@@ -509,6 +522,9 @@ class TemplateForm(forms.ModelForm): ...@@ -509,6 +522,9 @@ class TemplateForm(forms.ModelForm):
@property @property
def helper(self): def helper(self):
kwargs_raw_data = {}
if not self.user.is_superuser:
kwargs_raw_data['readonly'] = None
helper = FormHelper() helper = FormHelper()
helper.layout = Layout( helper.layout = Layout(
Field("name"), Field("name"),
...@@ -560,7 +576,7 @@ class TemplateForm(forms.ModelForm): ...@@ -560,7 +576,7 @@ class TemplateForm(forms.ModelForm):
"stuff", "stuff",
Field('access_method'), Field('access_method'),
Field('boot_menu'), Field('boot_menu'),
Field('raw_data'), Field('raw_data', **kwargs_raw_data),
Field('req_traits'), Field('req_traits'),
Field('description'), Field('description'),
Field("parent", type="hidden"), Field("parent", type="hidden"),
...@@ -762,6 +778,93 @@ class DiskAddForm(forms.Form): ...@@ -762,6 +778,93 @@ class DiskAddForm(forms.Form):
return helper return helper
class CircleAuthenticationForm(AuthenticationForm):
# fields: username, password
@property
def helper(self):
helper = FormHelper()
helper.form_show_labels = False
helper.layout = Layout(
AnyTag(
"div",
AnyTag(
"span",
AnyTag(
"i",
css_class="icon-user",
),
css_class="input-group-addon",
),
Field("username", placeholder=_("Username"),
css_class="form-control"),
css_class="input-group",
),
AnyTag(
"div",
AnyTag(
"span",
AnyTag(
"i",
css_class="icon-lock",
),
css_class="input-group-addon",
),
Field("password", placeholder=_("Password"),
css_class="form-control"),
css_class="input-group",
),
)
helper.add_input(Submit("submit", _("Sign in"),
css_class="btn btn-success"))
return helper
class CirclePasswordResetForm(PasswordResetForm):
# fields: email
@property
def helper(self):
helper = FormHelper()
helper.form_show_labels = False
helper.layout = Layout(
AnyTag(
"div",
AnyTag(
"span",
AnyTag(
"i",
css_class="icon-envelope",
),
css_class="input-group-addon",
),
Field("email", placeholder=_("Email address"),
css_class="form-control"),
Div(
AnyTag(
"button",
HTML(_("Reset password")),
css_class="btn btn-success",
),
css_class="input-group-btn",
),
css_class="input-group",
),
)
return helper
class CircleSetPasswordForm(SetPasswordForm):
@property
def helper(self):
helper = FormHelper()
helper.add_input(Submit("submit", _("Change password"),
css_class="btn btn-success change-password",
css_id="submit-password-button"))
return helper
class LinkButton(BaseInput): class LinkButton(BaseInput):
""" """
......
...@@ -332,8 +332,6 @@ a.hover-black { ...@@ -332,8 +332,6 @@ a.hover-black {
} }
<<<<<<< HEAD
.notification-messages { .notification-messages {
padding: 10px 8px; padding: 10px 8px;
width: 350px; width: 350px;
......
...@@ -18,6 +18,15 @@ $(function() { ...@@ -18,6 +18,15 @@ $(function() {
$('#create-modal').on('hidden.bs.modal', function() { $('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove(); $('#create-modal').remove();
}); });
$('#vm-migrate-node-list li').click(function(e) {
var li = $(this).closest('li');
if (li.find('input').attr('disabled'))
return true;
$('#vm-migrate-node-list li').removeClass('panel-primary');
li.addClass('panel-primary').find('input').attr('checked', true);
return false;
});
$('#vm-migrate-node-list li input:checked').closest('li').addClass('panel-primary');
} }
}); });
return false; return false;
......
...@@ -131,7 +131,11 @@ $(function() { ...@@ -131,7 +131,11 @@ $(function() {
location.reload(); location.reload();
}, },
error: function(xhr, textStatus, error) { error: function(xhr, textStatus, error) {
if (xhr.status == 500) {
addMessage("Internal Server Error", "danger");
} else {
addMessage(xhr.status + " Unknown Error", "danger");
}
} }
}); });
} else { } else {
......
{% load i18n %} {% load i18n %}
{% load sizefieldtags %} {% load sizefieldtags %}
<form method="POST" action="{% url "dashboard.views.vm-migrate" pk=vm %}"> <form method="POST" action="{% url "dashboard.views.vm-migrate" pk=vm.pk %}">
{% csrf_token %} {% csrf_token %}
<ul id="vm-migrate-node-list"> <ul id="vm-migrate-node-list">
{% for n in nodes %} {% with current=vm.node.pk selected=vm.select_node.pk %}
<li> {% for n in nodes %}
<strong>{{ n }}</strong> <li class="panel panel-default"><div class="panel-body">
<input type="radio" name="node" value="{{ n.pk }}" style="float: right;"/> <label for="migrate-to-{{n.pk}}">
<span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span> <strong>{{ n }}</strong>
<span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span> {% if current == n.pk %}<div class="label label-info">{% trans "current" %}</div>{% endif %}
<div style="clear: both;"></div> {% if selected == n.pk %}<div class="label label-success">{% trans "recommended" %}</div>{% endif %}
</li> </label>
{% endfor %} <input id="migrate-to-{{n.pk}}" type="radio" name="node" value="{{ n.pk }}" style="float: right;"
{% if current == n.pk %}disabled="disabled"{% endif %}
{% if selected == n.pk %}checked="checked"{% endif %} />
<span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span>
<span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span>
<div style="clear: both;"></div>
</div></li>
{% endfor %}
{% endwith %}
</ul> </ul>
<button type="submit" class="btn btn-primary btn-sm"><i class="icon-truck"></i> Migrate</button> <button type="submit" class="btn btn-primary btn-sm"><i class="icon-truck"></i> Migrate</button>
</form> </form>
...@@ -113,8 +113,11 @@ ...@@ -113,8 +113,11 @@
<i class="icon-tasks icon-2x"></i><br> <i class="icon-tasks icon-2x"></i><br>
{% trans "Resources" %}</a> {% trans "Resources" %}</a>
</li> </li>
<li {% if not instance.is_console_available %}class="disabled"{% endif %}> <li {% if not instance.is_console_available %}class="disabled">
<a href="#" data-toggle="pill_" data-target="#_console" class="text-center">
{% else %}>
<a href="#console" data-toggle="pill" data-target="#_console" class="text-center"> <a href="#console" data-toggle="pill" data-target="#_console" class="text-center">
{% endif %}
<i class="icon-desktop icon-2x"></i><br> <i class="icon-desktop icon-2x"></i><br>
{% trans "Console" %}</a></li> {% trans "Console" %}</a></li>
<li> <li>
......
...@@ -5,8 +5,10 @@ import re ...@@ -5,8 +5,10 @@ import re
from datetime import datetime from datetime import datetime
import requests import requests
from django.conf import settings
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.contrib.auth.views import redirect_to_login from django.contrib.auth.views import login, redirect_to_login
from django.contrib.auth.views import login
from django.contrib.messages import warning from django.contrib.messages import warning
from django.core.exceptions import ( from django.core.exceptions import (
PermissionDenied, SuspiciousOperation, PermissionDenied, SuspiciousOperation,
...@@ -32,7 +34,7 @@ from braces.views import ( ...@@ -32,7 +34,7 @@ from braces.views import (
from .forms import ( from .forms import (
VmCustomizeForm, TemplateForm, LeaseForm, NodeForm, HostForm, VmCustomizeForm, TemplateForm, LeaseForm, NodeForm, HostForm,
DiskAddForm, DiskAddForm, CircleAuthenticationForm,
) )
from .tables import (VmListTable, NodeListTable, NodeVmListTable, from .tables import (VmListTable, NodeListTable, NodeVmListTable,
TemplateListTable, LeaseListTable, GroupListTable,) TemplateListTable, LeaseListTable, GroupListTable,)
...@@ -45,6 +47,15 @@ from dashboard.models import Favourite, Profile ...@@ -45,6 +47,15 @@ from dashboard.models import Favourite, Profile
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def search_user(keyword):
try:
return User.objects.get(username=keyword)
except User.DoesNotExist:
return User.objects.get(email=keyword)
except User.DoesNotExist:
return User.objects.get(profile__org_id=keyword)
# github.com/django/django/blob/stable/1.6.x/django/contrib/messages/views.py # github.com/django/django/blob/stable/1.6.x/django/contrib/messages/views.py
class SuccessMessageMixin(object): class SuccessMessageMixin(object):
""" """
...@@ -1491,11 +1502,7 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView): ...@@ -1491,11 +1502,7 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
try: try:
new_owner = User.objects.get(username=request.POST['name']) new_owner = search_user(request.POST['name'])
except User.DoesNotExist:
new_owner = User.objects.get(email=request.POST['name'])
except User.DoesNotExist:
new_owner = User.objects.get(profile__org_id=request.POST['name'])
except User.DoesNotExist: except User.DoesNotExist:
messages.error(request, _('Can not find specified user.')) messages.error(request, _('Can not find specified user.'))
return self.get(request, *args, **kwargs) return self.get(request, *args, **kwargs)
...@@ -1873,7 +1880,7 @@ class VmMigrateView(SuperuserRequiredMixin, TemplateView): ...@@ -1873,7 +1880,7 @@ class VmMigrateView(SuperuserRequiredMixin, TemplateView):
'template': 'dashboard/_vm-migrate.html', 'template': 'dashboard/_vm-migrate.html',
'box_title': _('Migrate %(name)s' % {'name': vm.name}), 'box_title': _('Migrate %(name)s' % {'name': vm.name}),
'ajax_title': True, 'ajax_title': True,
'vm': kwargs['pk'], 'vm': vm,
'nodes': [n for n in Node.objects.filter(enabled=True) 'nodes': [n for n in Node.objects.filter(enabled=True)
if n.state == "ONLINE"] if n.state == "ONLINE"]
}) })
...@@ -1890,3 +1897,12 @@ class VmMigrateView(SuperuserRequiredMixin, TemplateView): ...@@ -1890,3 +1897,12 @@ class VmMigrateView(SuperuserRequiredMixin, TemplateView):
messages.error(self.request, _("You didn't select a node!")) messages.error(self.request, _("You didn't select a node!"))
return redirect("%s#activity" % vm.get_absolute_url()) return redirect("%s#activity" % vm.get_absolute_url())
def circle_login(request):
authentication_form = CircleAuthenticationForm
extra_context = {
'saml2': hasattr(settings, "SAML_CONFIG")
}
return login(request, authentication_form=authentication_form,
extra_context=extra_context)
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
from itertools import islice, chain from itertools import islice, chain
import logging import logging
from netaddr import IPSet from netaddr import IPSet, EUI
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
...@@ -631,6 +631,8 @@ class Host(models.Model): ...@@ -631,6 +631,8 @@ class Host(models.Model):
if self.shared_ip and public: if self.shared_ip and public:
res = Record.objects.filter(type='A', res = Record.objects.filter(type='A',
address=self.pub_ipv4) address=self.pub_ipv4)
if res.count() < 1:
return unicode(self.pub_ipv4)
else: else:
res = self.record_set.filter(type='A', res = self.record_set.filter(type='A',
address=self.ipv4) address=self.ipv4)
...@@ -706,6 +708,17 @@ class Host(models.Model): ...@@ -706,6 +708,17 @@ class Host(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return ('network.host', None, {'pk': self.pk}) return ('network.host', None, {'pk': self.pk})
@property
def eui(self):
return EUI(self.mac)
@property
def hw_vendor(self):
try:
return self.eui.oui.registration().org
except:
return None
class Firewall(models.Model): class Firewall(models.Model):
name = models.CharField(max_length=20, unique=True, name = models.CharField(max_length=20, unique=True,
......
from django.test import TestCase from django.test import TestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from ..admin import HostAdmin from ..admin import HostAdmin
from firewall.models import Vlan, Domain, Host from firewall.models import Vlan, Domain, Record, Host
from django.forms import ValidationError from django.forms import ValidationError
...@@ -82,3 +82,32 @@ class GetNewAddressTestCase(TestCase): ...@@ -82,3 +82,32 @@ class GetNewAddressTestCase(TestCase):
Host(hostname='h-6', mac='01:02:03:04:05:06', Host(hostname='h-6', mac='01:02:03:04:05:06',
ipv4='10.0.0.6', vlan=self.vlan, owner=self.u1).save() ipv4='10.0.0.6', vlan=self.vlan, owner=self.u1).save()
self.assertEqual(self.vlan.get_new_address()['ipv4'], '10.0.0.2') self.assertEqual(self.vlan.get_new_address()['ipv4'], '10.0.0.2')
class HostGetHostnameTestCase(TestCase):
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.save()
self.d = Domain(name='example.org', owner=self.u1)
self.d.save()
Record.objects.all().delete()
self.vlan = Vlan(vid=1, name='test', network4='10.0.0.0/24',
network6='2001:738:2001:4031::/80', domain=self.d,
owner=self.u1, network_type='portforward',
snat_ip='10.1.1.1')
self.vlan.save()
self.h = Host(hostname='h', mac='01:02:03:04:05:00', ipv4='10.0.0.1',
vlan=self.vlan, owner=self.u1, shared_ip=True,
pub_ipv4=self.vlan.snat_ip)
self.h.save()
def test_issue_93_wo_record(self):
self.assertEqual(self.h.get_hostname(proto='ipv4', public=True),
unicode(self.h.pub_ipv4))
def test_issue_93_w_record(self):
self.r = Record(name='vm', type='A', domain=self.d, owner=self.u1,
address=self.vlan.snat_ip)
self.r.save()
self.assertEqual(self.h.get_hostname(proto='ipv4', public=True),
self.r.fqdn)
...@@ -36,6 +36,9 @@ class GroupTable(Table): ...@@ -36,6 +36,9 @@ class GroupTable(Table):
class HostTable(Table): class HostTable(Table):
hostname = LinkColumn('network.host', args=[A('pk')]) hostname = LinkColumn('network.host', args=[A('pk')])
mac = TemplateColumn(
template_name="network/columns/mac.html"
)
class Meta: class Meta:
model = Host model = Host
......
{% load i18n %}
<span title="{% blocktrans with vendor=record.hw_vendor|default:"n/a" %}Vendor: {{vendor}}{% endblocktrans %}">{{ record.mac }}</span>
{% load i18n %}
{% load staticfiles %}
{% get_current_language as LANGUAGE_CODE %}
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}{% endblock %} | CIRCLE</title>
<meta name="description" content="">
<meta name="author" content="">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
<link href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css" rel="stylesheet">
<style type="text/css">
html, body {
background-color: #eee;
}
body {
margin-top: 40px;
}
.container {
width: 600px;
}
ul, li {
list-style: none;
margin: 0;
padding: 0;
}
.container > .content {
background-color: #fff;
padding: 20px;
-webkit-border-radius: 10px 10px 10px 10px;
-moz-border-radius: 10px 10px 10px 10px;
border-radius: 10px 10px 10px 10px;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15);
-moz-box-shadow: 0 1px 2px rgba(0,0,0,.15);
box-shadow: 0 1px 2px rgba(0,0,0,.15);
}
.login-form-errors .alert {
margin-right: 30px;
margin-left: 30px;
}
.login-form {
margin-top: 40px;
padding: 0 10px;
}
.login-form form {
padding: 0 20px;
}
.input-group {
margin-bottom: 10px;
}
.input-group-addon {
width: 38px;
}
.form-group label {
margin-top: 20px;
}
#submit-password-button {
margin-top: 15px;
}
/* fix for crispy-forms' html */
.form-group {
margin-bottom: 0px;
}
.help-block {
display: none;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
{% block content %}{% endblock %}
</div>
</div> <!-- /container -->
</body>
</html>
{% extends "base.html" %} {% extends "registration/base.html" %}
{% load i18n %} {% load i18n %}
{% load staticfiles %} {% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
{% block title %}{% trans "Login" %}{% endblock %}
{% block content %} {% block content %}
<form action="" method="POST"> <div class="row">
{% csrf_token %} {% if form.password.errors or form.username.errors %}
{{ form }} <div class="login-form-errors">
<input type="submit" value="LOGIN" /> {% include "display-form-errors.html" %}
</form> </div>
{% endif %}
<div class="col-sm-{% if saml2 %}6{% else %}12{% endif %}">
<div class="login-form">
<form action="" method="POST">
{% csrf_token %}
{% crispy form %}
</form>
</div>
</div>
{% if saml2 %}
<div class="col-sm-6">
<h4 style="padding-top: 0; margin-top: 0;">{% trans "Login with SSO" %}</h4>
<a href="{% url "saml2_login" %}">{% trans "Click here!" %}</a>
</div>
{% endif %}
</div>
<div class="row">
<div class="col-sm-12">
<a class="pull-right" href="{% url "accounts.password-reset" %}">{% trans "Forgot your password?" %}</a>
</div>
</div>
{% endblock %} {% endblock %}
{% extends "registration/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% block title %}{% trans "Password reset complete" %}{% endblock %}
{% block content %}
<div class="row">
<div class="login-form-errors">
{% include "display-form-errors.html" %}
</div>
<div class="col-sm-12">
<div class="alert alert-success">
{% trans "Password change successful!" %}
<a href="{% url "accounts.login" %}">{% trans "Click here to login" %}</a>
</div>
</div>
</div>
{% endblock %}
{% extends "registration/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% block title %}{% trans "Password reset confirm" %}{% endblock %}
{% block content %}
<div class="row">
<div class="login-form-errors">
{% include "display-form-errors.html" %}
</div>
<div class="col-sm-12">
<div style="margin: 0 0 25px 0;">
{% blocktrans %}Please enter your new password twice so we can verify you typed it in correctly!{% endblocktrans %}
</div>
{% if form %}
{% crispy form %}
{% else %}
<div class="alert alert-warning">
{% url "accounts.password-reset" as url %}
{% blocktrans with url=url %}This token is expired, please <a href="{{ url }}">request</a> a new password reset link again!{% endblocktrans %}
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% extends "registration/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% block title %}{% trans "Password reset done" %}{% endblock %}
{% block content %}
<div class="row">
<div class="login-form-errors">