Browse Source

Noot! Domains working

Sebastian Lohff 2 years ago
parent
commit
aa6313b464

+ 45
- 46
dnmgmt/settings.py View File

@@ -32,12 +32,12 @@ ALLOWED_HOSTS = []
32 32
 # Application definition
33 33
 
34 34
 INSTALLED_APPS = [
35
-    'django.contrib.admin',
36
-    'django.contrib.auth',
37
-    'django.contrib.contenttypes',
38
-    'django.contrib.sessions',
39
-    'django.contrib.messages',
40
-    'django.contrib.staticfiles',
35
+	'django.contrib.admin',
36
+	'django.contrib.auth',
37
+	'django.contrib.contenttypes',
38
+	'django.contrib.sessions',
39
+	'django.contrib.messages',
40
+	'django.contrib.staticfiles',
41 41
 	'crispy_forms',
42 42
 	'dncore',
43 43
 	'whoisdb',
@@ -46,31 +46,31 @@ INSTALLED_APPS = [
46 46
 ]
47 47
 
48 48
 MIDDLEWARE = [
49
-    'django.middleware.security.SecurityMiddleware',
50
-    'django.contrib.sessions.middleware.SessionMiddleware',
51
-    'django.middleware.common.CommonMiddleware',
52
-    'django.middleware.csrf.CsrfViewMiddleware',
53
-    'django.contrib.auth.middleware.AuthenticationMiddleware',
54
-    'django.contrib.messages.middleware.MessageMiddleware',
55
-    'django.middleware.clickjacking.XFrameOptionsMiddleware',
49
+	'django.middleware.security.SecurityMiddleware',
50
+	'django.contrib.sessions.middleware.SessionMiddleware',
51
+	'django.middleware.common.CommonMiddleware',
52
+	'django.middleware.csrf.CsrfViewMiddleware',
53
+	'django.contrib.auth.middleware.AuthenticationMiddleware',
54
+	'django.contrib.messages.middleware.MessageMiddleware',
55
+	'django.middleware.clickjacking.XFrameOptionsMiddleware',
56 56
 ]
57 57
 
58 58
 ROOT_URLCONF = 'dnmgmt.urls'
59 59
 
60 60
 TEMPLATES = [
61
-    {
62
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
63
-        'DIRS': [os.path.join(BASE_DIR, "templates/")],
64
-        'APP_DIRS': True,
65
-        'OPTIONS': {
66
-            'context_processors': [
67
-                'django.template.context_processors.debug',
68
-                'django.template.context_processors.request',
69
-                'django.contrib.auth.context_processors.auth',
70
-                'django.contrib.messages.context_processors.messages',
71
-            ],
72
-        },
73
-    },
61
+	{
62
+		'BACKEND': 'django.template.backends.django.DjangoTemplates',
63
+		'DIRS': [os.path.join(BASE_DIR, "templates/")],
64
+		'APP_DIRS': True,
65
+		'OPTIONS': {
66
+			'context_processors': [
67
+				'django.template.context_processors.debug',
68
+				'django.template.context_processors.request',
69
+				'django.contrib.auth.context_processors.auth',
70
+				'django.contrib.messages.context_processors.messages',
71
+			],
72
+		},
73
+	},
74 74
 ]
75 75
 
76 76
 WSGI_APPLICATION = 'dnmgmt.wsgi.application'
@@ -80,10 +80,10 @@ WSGI_APPLICATION = 'dnmgmt.wsgi.application'
80 80
 # https://docs.djangoproject.com/en/1.10/ref/settings/#databases
81 81
 
82 82
 DATABASES = {
83
-    'default': {
84
-        'ENGINE': 'django.db.backends.sqlite3',
85
-        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
86
-    }
83
+	'default': {
84
+		'ENGINE': 'django.db.backends.sqlite3',
85
+		'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
86
+	}
87 87
 }
88 88
 
89 89
 
@@ -91,18 +91,18 @@ DATABASES = {
91 91
 # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
92 92
 
93 93
 AUTH_PASSWORD_VALIDATORS = [
94
-    {
95
-        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
96
-    },
97
-    {
98
-        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
99
-    },
100
-    {
101
-        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
102
-    },
103
-    {
104
-        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
105
-    },
94
+	{
95
+		'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
96
+	},
97
+	{
98
+		'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
99
+	},
100
+	{
101
+		'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
102
+	},
103
+	{
104
+		'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
105
+	},
106 106
 ]
107 107
 
108 108
 
@@ -126,14 +126,13 @@ USE_TZ = True
126 126
 STATIC_URL = '/static/'
127 127
 STATIC_ROOT = os.path.join(BASE_DIR, "cstatic")
128 128
 STATICFILES_DIRS = [
129
-    os.path.join(BASE_DIR, "static"),
129
+	os.path.join(BASE_DIR, "static"),
130 130
 ]
131 131
 CRISPY_TEMPLATE_PACK = 'bootstrap3'
132 132
 MESSAGE_TAGS = {
133
-    messages.ERROR: 'danger',
133
+	messages.ERROR: 'danger',
134 134
 }
135 135
 
136 136
 AUTH_USER_MODEL = 'dncore.User'
137 137
 LOGIN_REDIRECT_URL = '/'
138
-LOGIN_URL = '/login/'
139
-
138
+LOGIN_URL = '/user/login/'

+ 31
- 15
domains/forms.py View File

@@ -17,28 +17,35 @@ class DomainForm(MntFormMixin, forms.ModelForm):
17 17
 
18 18
 		super(DomainForm, self).__init__(*args, **kwargs)
19 19
 
20
+		instance = getattr(self, "instance", None)
21
+		self._create = not (instance and instance.pk)
22
+
23
+		if not self._create:
24
+			self.fields['name'].disabled = True
25
+
20 26
 	def clean_name(self):
21 27
 		name = self.cleaned_data['name'].lower()
22
-		if not name.endswith("."):
23
-			name += "."
28
+		if self._create:
29
+			if not name.endswith("."):
30
+				name += "."
24 31
 
25
-		if not name.endswith("dn."):
26
-			raise forms.ValidationError("Only .dn domains can be registered at this point")
32
+			if not name.endswith("dn."):
33
+				raise forms.ValidationError("Only .dn domains can be registered at this point")
27 34
 
28
-		if name.count(".") > 2:
29
-			raise forms.ValidationError("No subdomains can be registered")
35
+			if name.count(".") > 2:
36
+				raise forms.ValidationError("No subdomains can be registered")
30 37
 
31
-		if not re.match("^[a-z0-9.-]+$", name):
32
-			raise forms.ValidationError("Only a-z, 0-9 and - are allowed inside the domain name")
38
+			if not re.match("^[a-z0-9.-]+$", name):
39
+				raise forms.ValidationError("Only a-z, 0-9 and - are allowed inside the domain name")
33 40
 
34
-		try:
35
-			Domain.objects.get(name=name)
36
-			raise forms.ValidationError("Domain already exists")
37
-		except Domain.DoesNotExist:
38
-			pass
41
+			try:
42
+				Domain.objects.get(name=name)
43
+				raise forms.ValidationError("Domain already exists")
44
+			except Domain.DoesNotExist:
45
+				pass
39 46
 
40 47
 		return name
41
-		
48
+
42 49
 
43 50
 class NameserverForm(MntFormMixin, forms.ModelForm):
44 51
 	class Meta:
@@ -50,6 +57,9 @@ class NameserverForm(MntFormMixin, forms.ModelForm):
50 57
 
51 58
 		super(NameserverForm, self).__init__(*args, **kwargs)
52 59
 
60
+		instance = getattr(self, "instance", None)
61
+		self._create = not (instance and instance.pk)
62
+
53 63
 	def clean_name(self):
54 64
 		name = self.cleaned_data['name'].lower().strip()
55 65
 		if not name.endswith("."):
@@ -62,8 +72,14 @@ class NameserverForm(MntFormMixin, forms.ModelForm):
62 72
 
63 73
 		mnts = self._user.maintainer_set.all()
64 74
 		domains = Domain.objects.filter(mnt_by__in=mnts)
75
+		found = False
65 76
 		for domain in domains:
66
-			if domain.endswith(name)
77
+			if domain.name == zone:
78
+				found = True
79
+				break
80
+
81
+		if not found:
82
+			raise forms.ValidationError("This nameserver is not under a domain you control.")
67 83
 
68 84
 		return name
69 85
 

+ 3
- 25
domains/migrations/0001_initial.py View File

@@ -1,5 +1,5 @@
1 1
 # -*- coding: utf-8 -*-
2
-# Generated by Django 1.10.5 on 2017-03-13 11:18
2
+# Generated by Django 1.10.5 on 2017-03-14 21:04
3 3
 from __future__ import unicode_literals
4 4
 
5 5
 from django.db import migrations, models
@@ -35,7 +35,7 @@ class Migration(migrations.Migration):
35 35
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
36 36
                 ('created', models.DateTimeField(auto_now_add=True)),
37 37
                 ('last_modified', models.DateTimeField(auto_now_add=True)),
38
-                ('name', models.CharField(max_length=256)),
38
+                ('name', models.CharField(max_length=256, unique=True)),
39 39
                 ('glueIPv4', models.GenericIPAddressField(blank=True, null=True, protocol='IPv4')),
40 40
                 ('glueIPv6', models.GenericIPAddressField(blank=True, null=True, protocol='IPv6')),
41 41
                 ('admin_c', models.ManyToManyField(to='whoisdb.Contact')),
@@ -45,38 +45,16 @@ class Migration(migrations.Migration):
45 45
                 'abstract': False,
46 46
             },
47 47
         ),
48
-        migrations.CreateModel(
49
-            name='NameserverDomainAssignment',
50
-            fields=[
51
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
52
-                ('order', models.PositiveSmallIntegerField(default=0)),
53
-                ('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='domains.Domain')),
54
-                ('nameserver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='domains.Nameserver')),
55
-            ],
56
-        ),
57
-        migrations.CreateModel(
58
-            name='NameserverReverseZoneAssignment',
59
-            fields=[
60
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
61
-                ('order', models.PositiveSmallIntegerField()),
62
-                ('nameserver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='domains.Nameserver')),
63
-            ],
64
-        ),
65 48
         migrations.CreateModel(
66 49
             name='ReverseZone',
67 50
             fields=[
68 51
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
69 52
                 ('address', models.GenericIPAddressField(db_index=True)),
70 53
                 ('netmask', models.PositiveIntegerField()),
71
-                ('nameservers', models.ManyToManyField(through='domains.NameserverReverseZoneAssignment', to='domains.Nameserver')),
54
+                ('nameservers', models.ManyToManyField(to='domains.Nameserver')),
72 55
                 ('parentNet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='whoisdb.InetNum')),
73 56
             ],
74 57
         ),
75
-        migrations.AddField(
76
-            model_name='nameserverreversezoneassignment',
77
-            name='reversezone',
78
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='domains.ReverseZone'),
79
-        ),
80 58
         migrations.AddField(
81 59
             model_name='domain',
82 60
             name='nameservers',

+ 14
- 1
domains/models.py View File

@@ -6,6 +6,7 @@ from whoisdb.models import MntdObject, Contact, InetNum
6 6
 # generally allow domains for .dn to be created
7 7
 # allow owners of a subnet to create reverse dns record?
8 8
 
9
+
9 10
 class Nameserver(MntdObject):
10 11
 	handleSuffix = "NS"
11 12
 	# dns name
@@ -24,6 +25,7 @@ class Nameserver(MntdObject):
24 25
 	def __str__(self):
25 26
 		return self.name
26 27
 
28
+
27 29
 class Domain(MntdObject):
28 30
 	handle = None
29 31
 	handleSuffix = "DOM"
@@ -38,18 +40,29 @@ class Domain(MntdObject):
38 40
 	def __str__(self):
39 41
 		return self.name
40 42
 
43
+	def getNoDeleteReasons(self):
44
+		reasons = []
45
+
46
+		nameservers = Nameserver.objects.filter(name__endswith="." + self.name)
47
+		for ns in nameservers:
48
+			reasons.append("Nameserver %s depends on this domain" % ns.name)
49
+
50
+		return reasons
51
+
41 52
 #class NameserverDomainAssignment(models.Model):
42 53
 #	domain = models.ForeignKey(Domain)
43 54
 #	nameserver = models.ForeignKey(Nameserver)
44 55
 #
45 56
 #	order = models.PositiveSmallIntegerField(default=0)
46 57
 
58
+
47 59
 class ReverseZone(models.Model):
48 60
 	parentNet = models.ForeignKey(InetNum)
49 61
 	address = models.GenericIPAddressField(db_index=True)
50 62
 	netmask = models.PositiveIntegerField()
51 63
 
52
-	nameservers = models.ManyToManyField(Nameserver, through='NameserverReverseZoneAssignment')
64
+	nameservers = models.ManyToManyField(Nameserver)
65
+
53 66
 
54 67
 #class NameserverReverseZoneAssignment(models.Model):
55 68
 #	reversezone = models.ForeignKey(ReverseZone)

+ 6
- 2
domains/urls.py View File

@@ -6,8 +6,12 @@ urlpatterns = [
6 6
 	url(r'^$', domains_views.overview, name='overview'),
7 7
 
8 8
 	url(r'domain/create/$', domains_views.DomainCreate.as_view(), name='domain-create'),
9
-	url(r'show/(?P<domain>[a-z0-9.-]+)/$', domains_views.DomainDetail, name='domain-show'),
9
+	url(r'domain/show/(?P<domain>[a-z0-9.-]+)/$', domains_views.DomainDetail.as_view(), name='domain-show'),
10
+	url(r'domain/edit/(?P<domain>[a-z0-9.-]+)/$', domains_views.DomainEdit.as_view(), name='domain-edit'),
11
+	url(r'domain/delete/(?P<domain>[a-z0-9.-]+)/$', domains_views.DomainDelete.as_view(), name='domain-delete'),
10 12
 
11 13
 	url(r'nameserver/create/$', domains_views.NameserverCreate.as_view(), name='nameserver-create'),
12
-	url(r'nameserver/show/(?P<pk>\d+)/$', domains_views.NameserverDetail, name='nameserver-show'),
14
+	url(r'nameserver/show/(?P<domain>[a-z0-9.-]+)/$', domains_views.NameserverDetail.as_view(), name='nameserver-show'),
15
+	url(r'nameserver/edit/(?P<domain>[a-z0-9.-]+)/$', domains_views.NameserverEdit.as_view(), name='nameserver-edit'),
16
+	url(r'nameserver/delete/(?P<domain>[a-z0-9.-]+)/$', domains_views.NameserverDelete.as_view(), name='nameserver-delete'),
13 17
 ]

+ 49
- 4
domains/views.py View File

@@ -1,13 +1,15 @@
1 1
 from django.shortcuts import render
2
+from django.urls import reverse_lazy
2 3
 from django.contrib.auth.decorators import login_required
3 4
 from django.views.generic import DetailView, CreateView, UpdateView
4 5
 from django.contrib.auth.mixins import LoginRequiredMixin
5 6
 
6
-from whoisdb.generic import MntGenericMixin
7
+from whoisdb.generic import MntGenericMixin, DeleteCheckView
7 8
 
8 9
 from .models import Domain, Nameserver
9 10
 from .forms import DomainForm, NameserverForm
10 11
 
12
+
11 13
 @login_required
12 14
 def overview(request):
13 15
 	mnts = request.user.maintainer_set.all()
@@ -18,6 +20,7 @@ def overview(request):
18 20
 
19 21
 	return render(request, "domains/overview.html", {"domains": domains, "nameservers": nameservers})
20 22
 
23
+
21 24
 class DomainCreate(LoginRequiredMixin, CreateView):
22 25
 	template_name = "domains/obj_create.html"
23 26
 	form_class = DomainForm
@@ -28,13 +31,33 @@ class DomainCreate(LoginRequiredMixin, CreateView):
28 31
 
29 32
 		return kwargs
30 33
 
34
+
31 35
 class DomainDetail(LoginRequiredMixin, DetailView):
32 36
 	model = Domain
33 37
 	slug_field = "name"
34 38
 	slug_url_kwarg = "domain"
39
+	context_object_name = "domain"
40
+
35 41
 
36 42
 class DomainEdit(MntGenericMixin, LoginRequiredMixin, UpdateView):
37
-	template_name = "domain"
43
+	model = Domain
44
+	form_class = DomainForm
45
+	slug_field = "name"
46
+	slug_url_kwarg = "domain"
47
+	template_name = "domains/obj_edit.html"
48
+
49
+	def get_form_kwargs(self, *args, **kwargs):
50
+		kwargs = super(DomainEdit, self).get_form_kwargs(*args, **kwargs)
51
+		kwargs["user"] = self.request.user
52
+		return kwargs
53
+
54
+
55
+class DomainDelete(MntGenericMixin, LoginRequiredMixin, DeleteCheckView):
56
+	template_name = "domains/obj_delete.html"
57
+	model = Domain
58
+	slug_field = "name"
59
+	slug_url_kwarg = "domain"
60
+	success_url = reverse_lazy("domains:overview")
38 61
 
39 62
 
40 63
 class NameserverCreate(LoginRequiredMixin, CreateView):
@@ -47,8 +70,30 @@ class NameserverCreate(LoginRequiredMixin, CreateView):
47 70
 
48 71
 		return kwargs
49 72
 
73
+
50 74
 class NameserverDetail(LoginRequiredMixin, DetailView):
51 75
 	model = Nameserver
52
-	#slug_field = "name"
53
-	#slug_url_kwarg = "domain"
76
+	slug_field = "name"
77
+	slug_url_kwarg = "domain"
78
+	context_object_name = "nameserver"
79
+
80
+
81
+class NameserverEdit(MntGenericMixin, LoginRequiredMixin, UpdateView):
82
+	model = Nameserver
83
+	form_class = NameserverForm
84
+	slug_field = "name"
85
+	slug_url_kwarg = "domain"
86
+	template_name = "domains/obj_edit.html"
54 87
 
88
+	def get_form_kwargs(self, *args, **kwargs):
89
+		kwargs = super(NameserverEdit, self).get_form_kwargs(*args, **kwargs)
90
+		kwargs["user"] = self.request.user
91
+		return kwargs
92
+
93
+
94
+class NameserverDelete(MntGenericMixin, LoginRequiredMixin, DeleteCheckView):
95
+	template_name = "domains/obj_delete.html"
96
+	model = Nameserver
97
+	slug_field = "name"
98
+	slug_url_kwarg = "domain"
99
+	success_url = reverse_lazy("domains:overview")

+ 15
- 0
templates/domains/domain_detail.html View File

@@ -0,0 +1,15 @@
1
+{% extends "base.html" %}
2
+
3
+{% block content %}
4
+<div class="row">
5
+	<div class="col-sm-12">
6
+		<div class="panel panel-default">
7
+			<div class="panel-heading">Header</div>
8
+			<div class="panel-body">
9
+				{{ domain.name }}
10
+			</div>
11
+		</div>
12
+	</div>
13
+</div>
14
+{% endblock %}
15
+

+ 15
- 0
templates/domains/nameserver_detail.html View File

@@ -0,0 +1,15 @@
1
+{% extends "base.html" %}
2
+
3
+{% block content %}
4
+<div class="row">
5
+	<div class="col-sm-12">
6
+		<div class="panel panel-default">
7
+			<div class="panel-heading">Header</div>
8
+			<div class="panel-body">
9
+				{{ nameserver.name }}
10
+			</div>
11
+		</div>
12
+	</div>
13
+</div>
14
+{% endblock %}
15
+

+ 33
- 0
templates/domains/obj_delete.html View File

@@ -0,0 +1,33 @@
1
+{% extends "base.html" %}
2
+
3
+{% load crispy_forms_tags %}
4
+
5
+{% block content %}
6
+<div class="row">
7
+	<div class="col-sm-12">
8
+		<div class="panel panel-{% if reasons %}danger{%else%}default{%endif%}">
9
+			<div class="panel-heading">Header</div>
10
+			<div class="panel-body">
11
+				{% if reasons %}
12
+					<p>
13
+					You cannot delete this object, as other objects in the database depend on it!
14
+					</p>
15
+					<p>
16
+					<ul>
17
+					{% for reason in reasons %}
18
+						<li>{{ reason }}</li>
19
+					{% endfor %}
20
+					</ul>
21
+				{% else %}
22
+					{{ obj }}
23
+				<form method="post" action="#">
24
+					{% csrf_token %}
25
+					<button type="submit" class="btn btn-primary">Delete</button>
26
+				</form>
27
+				{% endif %}
28
+			</div>
29
+		</div>
30
+	</div>
31
+</div>
32
+{% endblock %}
33
+

+ 21
- 0
templates/domains/obj_edit.html View File

@@ -0,0 +1,21 @@
1
+{% extends "base.html" %}
2
+
3
+{% load crispy_forms_tags %}
4
+
5
+{% block content %}
6
+<div class="row">
7
+	<div class="col-sm-12">
8
+		<div class="panel panel-default">
9
+			<div class="panel-heading">Header</div>
10
+			<div class="panel-body">
11
+				<form method="post" action="#">
12
+					{% csrf_token %}
13
+					{{ form | crispy }}
14
+					<button type="submit" class="btn btn-primary">Update</button>
15
+				</form>
16
+			</div>
17
+		</div>
18
+	</div>
19
+</div>
20
+{% endblock %}
21
+

+ 32
- 1
templates/domains/overview.html View File

@@ -8,15 +8,46 @@
8 8
 			<div class="panel-body">
9 9
 			<p>
10 10
 			Your nameservers (<a href="{% url "domains:nameserver-create" %}">New nameserver</a>)
11
+			<table class="table">
12
+				<tr>
13
+					<th>Nameserver</th>
14
+					<th>Glue IPv4</th>
15
+					<th>Glue IPv6</th>
16
+					<th>MNTs</th>
17
+					<th></th>
18
+				</tr>
11 19
 			{% for nameserver in nameservers %}
12
-				{{ nameserver }}<br />
20
+				<tr>
21
+					<td><a href="{% url "domains:nameserver-show" nameserver.name %}">{{ nameserver.name }}</a></td>
22
+					<td></td>
23
+					<td></td>
24
+					<td></td>
25
+					<td><a href="{% url "domains:nameserver-edit" nameserver.name %}">Edit</a> <a href="{% url "domains:nameserver-delete" nameserver.name %}">Delete</a></td>
26
+				</tr>
13 27
 			{% endfor %}
28
+			</table>
14 29
 			</p>
15 30
 			<p>
16 31
 			Your domains (<a href="{% url "domains:domain-create" %}">New domain</a>)
32
+			<table class="table">
33
+				<tr>
34
+					<th>Domain</th>
35
+					<th>Nameserver</th>
36
+					<th>Glue IPv6</th>
37
+					<th>MNTs</th>
38
+					<th></th>
39
+				</tr>
17 40
 			{% for domain in domains %}
41
+				<tr>
42
+					<td><a href="{% url "domains:domain-show" domain.name %}">{{ domain.name }}</a></td>
43
+					<td></td>
44
+					<td></td>
45
+					<td></td>
46
+					<td><a href="{% url "domains:domain-edit" domain.name %}">Edit</a> <a href="{% url "domains:domain-delete" domain.name %}">Delete</a></td>
47
+				</tr>
18 48
 				{{ domain }}<br />
19 49
 			{% endfor %}
50
+			</table>
20 51
 			</div>
21 52
 		</div>
22 53
 	</div>

Loading…
Cancel
Save