Commit 6f4a8323 authored by ibuler's avatar ibuler

Add admin user list view

parent dfc628a3
......@@ -11,4 +11,4 @@
"""
from users.utils import AdminUserRequiredMixin
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-05 12:50
# Generated by Django 1.10 on 2016-09-07 15:11
from __future__ import unicode_literals
from django.db import migrations, models
......@@ -24,12 +24,12 @@ class Migration(migrations.Migration):
('private_key', models.CharField(blank=True, max_length=4096, null=True, verbose_name='SSH private key')),
('is_default', models.BooleanField(default=True, verbose_name='As default')),
('auto_update', models.BooleanField(default=True, verbose_name='Auto update pass/key')),
('date_added', models.DateTimeField(auto_now=True, null=True)),
('date_created', models.DateTimeField(auto_now=True, null=True)),
('create_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
],
options={
'db_table': 'adminuser',
'db_table': 'admin_user',
},
),
migrations.CreateModel(
......@@ -55,9 +55,9 @@ class Migration(migrations.Migration):
('sn', models.CharField(blank=True, max_length=128, null=True, unique=True, verbose_name='Serial number')),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('date_added', models.DateTimeField(auto_now=True, null=True, verbose_name='Date added')),
('date_created', models.DateTimeField(auto_now=True, null=True, verbose_name='Date added')),
('comment', models.CharField(blank=True, max_length=128, null=True, verbose_name='Comment')),
('admin_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.AdminUser', verbose_name='Admin user')),
('admin_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.AdminUser', verbose_name='Admin user')),
],
options={
'db_table': 'asset',
......@@ -70,23 +70,24 @@ class Migration(migrations.Migration):
('key', models.CharField(blank=True, max_length=64, null=True, verbose_name='KEY')),
('value', models.CharField(blank=True, max_length=64, null=True, verbose_name='VALUE')),
('created_by', models.CharField(blank=True, max_length=32, verbose_name='Created by')),
('date_added', models.DateTimeField(auto_now=True, null=True)),
('date_created', models.DateTimeField(auto_now=True, null=True)),
('comment', models.TextField(blank=True, verbose_name='Comment')),
],
options={
'db_table': 'assetextend',
'db_table': 'asset_extend',
},
),
migrations.CreateModel(
name='AssetGroup',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, max_length=64, null=True, unique=True, verbose_name='Name')),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('name', models.CharField(max_length=64, unique=True, verbose_name='Name')),
('created_by', models.CharField(blank=True, max_length=32, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now=True, null=True, verbose_name='Date added')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
],
options={
'db_table': 'assetgroup',
'db_table': 'asset_group',
},
),
migrations.CreateModel(
......@@ -99,7 +100,7 @@ class Migration(migrations.Migration):
('phone', models.CharField(blank=True, max_length=32, verbose_name='Phone')),
('address', models.CharField(blank=True, max_length=128, verbose_name='Address')),
('network', models.TextField(blank=True, verbose_name='Network')),
('date_added', models.DateField(auto_now=True, null=True, verbose_name='Date added')),
('date_created', models.DateTimeField(auto_now=True, null=True, verbose_name='Date added')),
('operator', models.CharField(blank=True, max_length=32, verbose_name='Operator')),
('created_by', models.CharField(blank=True, max_length=32, verbose_name='Created by')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
......@@ -115,7 +116,7 @@ class Migration(migrations.Migration):
('key', models.CharField(blank=True, max_length=64, null=True, verbose_name='KEY')),
('value', models.CharField(blank=True, max_length=64, null=True, verbose_name='VALUE')),
('created_by', models.CharField(blank=True, max_length=32, verbose_name='Created by')),
('date_added', models.DateTimeField(auto_now=True, null=True)),
('date_created', models.DateTimeField(auto_now=True, null=True)),
('comment', models.CharField(blank=True, max_length=128, verbose_name='Comment')),
('asset', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.Asset', verbose_name='Asset')),
],
......@@ -124,7 +125,7 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
name='SysUser',
name='SystemUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
......@@ -140,14 +141,19 @@ class Migration(migrations.Migration):
('shell', models.CharField(blank=True, max_length=64, verbose_name='Shell')),
('home', models.CharField(blank=True, max_length=64, verbose_name='Home')),
('uid', models.IntegerField(blank=True, verbose_name='Uid')),
('date_added', models.DateTimeField(auto_now=True, null=True)),
('date_created', models.DateTimeField(auto_now=True, null=True)),
('create_by', models.CharField(blank=True, max_length=32, verbose_name='Created by')),
('comment', models.CharField(blank=True, max_length=128, verbose_name='Comment')),
],
options={
'db_table': 'sysuser',
'db_table': 'system_user',
},
),
migrations.AddField(
model_name='assetgroup',
name='system_users',
field=models.ManyToManyField(blank=True, related_name='asset_groups', to='assets.SystemUser'),
),
migrations.AddField(
model_name='asset',
name='env',
......@@ -156,12 +162,12 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='asset',
name='groups',
field=models.ManyToManyField(blank=True, null=True, to='assets.AssetGroup', verbose_name='Asset groups'),
field=models.ManyToManyField(related_name='assets', to='assets.AssetGroup', verbose_name='Asset groups'),
),
migrations.AddField(
model_name='asset',
name='idc',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.IDC', verbose_name='IDC'),
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.IDC', verbose_name='IDC'),
),
migrations.AddField(
model_name='asset',
......@@ -170,8 +176,8 @@ class Migration(migrations.Migration):
),
migrations.AddField(
model_name='asset',
name='sys_user',
field=models.ManyToManyField(blank=True, null=True, to='assets.SysUser', verbose_name='System User'),
name='system_user',
field=models.ManyToManyField(blank=True, to='assets.SystemUser', verbose_name='System User'),
),
migrations.AddField(
model_name='asset',
......
......@@ -5,6 +5,8 @@ from django.db import models
import logging
from django.utils.translation import ugettext_lazy as _
from common.utils import encrypt, decrypt
logger = logging.getLogger(__name__)
......@@ -68,20 +70,64 @@ class AssetExtend(models.Model):
class AdminUser(models.Model):
name = models.CharField(max_length=128, unique=True, null=True, blank=True, verbose_name=_('Name'))
username = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('Username'))
password = models.CharField(max_length=256, null=True, blank=True, verbose_name=_('Password'))
private_key = models.CharField(max_length=4096, null=True, blank=True, verbose_name=_('SSH private key'))
is_default = models.BooleanField(default=True, verbose_name=_('As default'))
auto_update = models.BooleanField(default=True, verbose_name=_('Auto update pass/key'))
date_created = models.DateTimeField(auto_now=True, null=True, blank=True)
create_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
_password = models.CharField(max_length=256, null=True, blank=True, verbose_name=_('Password'))
_private_key = models.CharField(max_length=4096, null=True, blank=True, verbose_name=_('SSH private key'))
_public_key = models.CharField(max_length=4096, null=True, blank=True, verbose_name=_('SSH public key'))
as_default = models.BooleanField(default=True, verbose_name=_('As default'))
comment = models.TextField(blank=True, verbose_name=_('Comment'))
date_created = models.DateTimeField(auto_now=True, null=True, blank=True)
created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
def __unicode__(self):
return self.name
@property
def password(self):
return decrypt(self._password)
@password.setter
def password(self, password_raw):
self._password = encrypt(password_raw)
@property
def private_key(self):
return decrypt(self._private_key)
@private_key.setter
def private_key(self, private_key_raw):
self._private_key = encrypt(private_key_raw)
@property
def public_key(self):
return decrypt(self._public_key)
@public_key.setter
def public_key(self, public_key_raw):
self._public_key = encrypt(public_key_raw)
class Meta:
db_table = 'admin_user'
@classmethod
def generate_fake(cls, count=100):
from random import seed, choice
import forgery_py
from django.db import IntegrityError
seed()
for i in range(count):
obj = cls(name=forgery_py.name.full_name(),
username=forgery_py.internet.user_name(),
password=forgery_py.lorem_ipsum.word(),
comment=forgery_py.lorem_ipsum.sentence(),
created_by='Fake')
try:
obj.save()
logger.debug('Generate fake asset group: %s' % obj.name)
except IntegrityError:
print('Error continue')
continue
class SystemUser(models.Model):
PROTOCOL_CHOICES = (
......@@ -102,7 +148,7 @@ class SystemUser(models.Model):
home = models.CharField(max_length=64, blank=True, verbose_name=_('Home'))
uid = models.IntegerField(blank=True, verbose_name=_('Uid'))
date_created = models.DateTimeField(auto_now=True, null=True)
create_by = models.CharField(max_length=32, blank=True, verbose_name=_('Created by'))
created_by = models.CharField(max_length=32, blank=True, verbose_name=_('Created by'))
comment = models.CharField(max_length=128, blank=True, verbose_name=_('Comment'))
def __unicode__(self):
......@@ -223,3 +269,7 @@ class Label(models.Model):
class Meta:
db_table = 'label'
def generate_fake():
for cls in (Asset, AssetGroup, IDC):
cls.generate_fake()
{% extends '_list_base.html' %}
{% load i18n %}
{% load common_tags %}
{% block content_left_head %}
<a href="{% url 'assets:admin-user-create' %}" class="btn btn-sm btn-primary "> {% trans "Create admin user" %} </a>
{% endblock %}
{% block table_head %}
<th class="text-center">{% trans 'ID' %}</th>
<th class="text-center"><a href="{% url 'assets:admin-user-list' %}?sort=name">{% trans 'Name' %}</a></th>
<th class="text-center"><a href="{% url 'assets:admin-user-list' %}?sort=username">{% trans 'Username' %}</a></th>
<th class="text-center">{% trans 'Asset num' %}</th>
<th class="text-center">{% trans 'Lost connection' %}</th>
<th class="text-center">{% trans 'Comment' %}</th>
<th class="text-center"></th>
{% endblock %}
{% block table_body %}
{% for admin_user in admin_user_list %}
<tr class="gradeX">
<td class="text-center">{{ admin_user.id }}</td>
<td>
<a href="{% url 'users:user-detail' pk=user.id %}">
{{ admin_user.name }}
</a>
</td>
<td class="text-center">{{ admin_user.username }}</td>
<td class="text-center">{{ admin_user.assets.count }}</td>
<td class="text-center">{{ admin_user.assets.count }}</td>
<td class="text-center">{{ admin_user.comment|truncatewords:8 }}</td>
<td class="text-center">
<!-- Todo: Click script button will paste a url to clipboard like: curl http://url/admin_user_create.sh | bash -->
<a href="{% url 'assets:admin-user-update' pk=admin_user.id %}" class="btn btn-xs btn-primary">{% trans 'Script' %}</a>
<!-- Todo: Click refresh button will run a task to test admin user could connect asset or not immediately -->
<a href="{% url 'assets:admin-user-update' pk=admin_user.id %}" class="btn btn-xs btn-warning">{% trans 'Refresh' %}</a>
<a href="{% url 'assets:admin-user-update' pk=admin_user.id %}" class="btn btn-xs btn-info">{% trans 'Update' %}</a>
<a href="{% url 'assets:admin-user-delete' pk=admin_user.id %}" class="btn btn-xs btn-danger del">{% trans 'Delete' %}</a>
</td>
</tr>
{% endfor %}
{% endblock %}
......@@ -28,5 +28,10 @@ urlpatterns = [
url(r'^idc/(?P<pk>[0-9]+)$', views.IDCDetailView.as_view(), name='idc-detail'),
url(r'^idc/(?P<pk>[0-9]+)/update', views.IDCUpdateView.as_view(), name='idc-update'),
url(r'^idc/(?P<pk>[0-9]+)/delete$', views.IDCDeleteView.as_view(), name='idc-delete'),
url(r'^admin-user$', views.AdminUserListView.as_view(), name='admin-user-list'),
url(r'^admin-user/create$', views.AdminUserCreateView.as_view(), name='admin-user-create'),
url(r'^admin-user/(?P<pk>[0-9]+)$', views.AdminUserDetailView.as_view(), name='admin-user-detail'),
url(r'^admin-user/(?P<pk>[0-9]+)/update', views.AdminUserUpdateView.as_view(), name='admin-user-update'),
url(r'^admin-user/(?P<pk>[0-9]+)/delete$', views.AdminUserDeleteView.as_view(), name='admin-user-delete'),
# url(r'^api/v1.0/', include(router.urls)),
]
# ~*~ coding: utf-8 ~*~
#
from django.contrib.auth.mixins import UserPassesTestMixin
from django.urls import reverse_lazy
from common.tasks import send_mail_async
from common.utils import reverse
from users.models import User
try:
import cStringIO as StringIO
except ImportError:
import StringIO
class AdminUserRequiredMixin(UserPassesTestMixin):
login_url = reverse_lazy('users:login')
def test_func(self):
return self.request.user.is_staff
......@@ -13,9 +13,9 @@ from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateVi
from django.urls import reverse_lazy
from django.views.generic.detail import DetailView, SingleObjectMixin
from .models import Asset, AssetGroup, IDC, AssetExtend
from .models import Asset, AssetGroup, IDC, AssetExtend, AdminUser, SystemUser
from .forms import AssetForm, AssetGroupForm, IDCForm
from .utils import AdminUserRequiredMixin
from .hands import AdminUserRequiredMixin
class AssetCreateView(CreateView):
......@@ -50,7 +50,7 @@ class AssetDetailView(DetailView):
template_name = 'assets/asset_detail.html'
class AssetGroupCreateView(CreateView):
class AssetGroupCreateView(AdminUserRequiredMixin, CreateView):
model = AssetGroup
form_class = AssetGroupForm
template_name = 'assets/asset_group_create.html'
......@@ -72,7 +72,7 @@ class AssetGroupCreateView(CreateView):
return super(AssetGroupCreateView, self).form_valid(form)
class AssetGroupListView(ListView):
class AssetGroupListView(AdminUserRequiredMixin, ListView):
model = AssetGroup
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
context_object_name = 'asset_group_list'
......@@ -101,7 +101,7 @@ class AssetGroupListView(ListView):
return self.queryset
class AssetGroupDetailView(SingleObjectMixin, ListView):
class AssetGroupDetailView(SingleObjectMixin, AdminUserRequiredMixin, ListView):
template_name = 'assets/asset_group_detail.html'
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
......@@ -122,7 +122,7 @@ class AssetGroupDetailView(SingleObjectMixin, ListView):
return super(AssetGroupDetailView, self).get_context_data(**kwargs)
class AssetGroupUpdateView(UpdateView):
class AssetGroupUpdateView(AdminUserRequiredMixin, UpdateView):
model = AssetGroup
form_class = AssetGroupForm
template_name = 'assets/asset_group_create.html'
......@@ -138,13 +138,13 @@ class AssetGroupUpdateView(UpdateView):
return super(AssetGroupUpdateView, self).get_context_data(**kwargs)
class AssetGroupDeleteView(DeleteView):
class AssetGroupDeleteView(AdminUserRequiredMixin, DeleteView):
template_name = 'assets/delete_confirm.html'
model = AssetGroup
success_url = reverse_lazy('assets:asset-group-list')
class IDCListView(ListView):
class IDCListView(AdminUserRequiredMixin, ListView):
model = IDC
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
context_object_name = 'idc_list'
......@@ -173,7 +173,7 @@ class IDCListView(ListView):
return self.queryset
class IDCCreateView(CreateView):
class IDCCreateView(AdminUserRequiredMixin, CreateView):
model = IDC
form_class = IDCForm
template_name = 'assets/idc_create.html'
......@@ -188,13 +188,59 @@ class IDCCreateView(CreateView):
return super(IDCCreateView, self).get_context_data(**kwargs)
class IDCUpdateView(UpdateView):
class IDCUpdateView(AdminUserRequiredMixin, UpdateView):
pass
class IDCDetailView(DetailView):
class IDCDetailView(AdminUserRequiredMixin, DetailView):
pass
class IDCDeleteView(DeleteView):
class IDCDeleteView(AdminUserRequiredMixin, DeleteView):
pass
class AdminUserListView(AdminUserRequiredMixin, ListView):
model = AdminUser
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
context_object_name = 'admin_user_list'
template_name = 'assets/admin_user_list.html'
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Admin user list'),
'keyword': self.request.GET.get('keyword', '')
}
kwargs.update(context)
return super(AdminUserListView, self).get_context_data(**kwargs)
def get_queryset(self):
# Todo: Default group by lose asset connection num
self.queryset = super(AdminUserListView, self).get_queryset()
self.keyword = keyword = self.request.GET.get('keyword', '')
self.sort = sort = self.request.GET.get('sort', '-date_created')
if keyword:
self.queryset = self.queryset.filter(Q(name__icontains=keyword) |
Q(comment__icontains=keyword))
if sort:
self.queryset = self.queryset.order_by(sort)
return self.queryset
class AdminUserCreateView(AdminUserRequiredMixin, CreateView):
pass
class AdminUserUpdateView(AdminUserRequiredMixin, UpdateView):
pass
class AdminUserDetailView(AdminUserRequiredMixin, DetailView):
pass
class AdminUserDeleteView(AdminUserRequiredMixin, DeleteView):
pass
......@@ -5,6 +5,7 @@ from __future__ import unicode_literals
from django.shortcuts import reverse as dj_reverse
from django.conf import settings
from django.core import signing
def reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None, external=False):
......@@ -21,3 +22,12 @@ def get_object_or_none(model, **kwargs):
except model.DoesNotExist:
obj = None
return obj
def encrypt(*args, **kwargs):
return signing.dumps(*args, **kwargs)
def decrypt(*args, **kwargs):
return signing.loads(*args, **kwargs)
......@@ -21,7 +21,7 @@
<li id="asset"><a href="{% url 'assets:asset-list' %}">{% trans 'Asset' %}</a></li>
<li id="asset-group"><a href="{% url 'assets:asset-group-list' %}">{% trans 'Asset group' %}</a></li>
<li id="idc"><a href="{% url 'assets:idc-list' %}">{% trans 'IDC' %}</a></li>
<li id="admin-user"><a href="">{% trans 'Admin user' %}</a></li>
<li id="admin-user"><a href="{% url 'assets:admin-user-list' %}">{% trans 'Admin user' %}</a></li>
<li id="system-user"><a href="">{% trans 'System user' %}</a></li>
<li id=""><a href="">{% trans 'Label' %}</a></li>
</ul>
......@@ -30,10 +30,10 @@
<a href="#"><i class="fa fa-edit"></i> <span class="nav-label">{% trans 'Perms' %}</span><span class="fa arrow"></span></a>
<ul class="nav nav-second-level">
<li id="sudo">
<a class="sudo" href="">{% trans 'Perm' %}</a>
<a class="sudo" href="">{% trans 'User perm' %}</a>
</li>
<li id="role">
<a href="">{% trans 'Create perm' %}</a>
<a href="">{% trans 'User group perm' %}</a>
</li>
</ul>
</li>
......
......@@ -14,46 +14,9 @@ from django.dispatch import receiver
from django.db import IntegrityError
from django.utils.translation import ugettext_lazy as _
from rest_framework.authtoken.models import Token
from django.core import signing
# class Role(models.Model):
# name = models.CharField('name', max_length=80, unique=True)
# permissions = models.ManyToManyField(
# Permission,
# verbose_name='permissions',
# blank=True,
# )
# date_created = models.DateTimeField(auto_now_add=True)
# created_by = models.CharField(max_length=100)
# comment = models.CharField(max_length=80, blank=True)
#
# def __unicode__(self):
# return self.name
#
# def delete(self, using=None, keep_parents=False):
# if self.users.all().count() > 0:
# raise OperationalError('Role %s has some member, should not be delete.' % self.name)
# else:
# return super(Role, self).delete(using=using, keep_parents=keep_parents)
#
# class Meta:
# db_table = 'role'
#
# @classmethod
# def initial(cls):
# roles = {
# 'Administrator': {'permissions': Permission.objects.all(), 'comment': '管理员'},
# 'User': {'permissions': [], 'comment': '用户'},
# 'Auditor': {'permissions': Permission.objects.filter(content_type__app_label='audits'),
# 'comment': '审计员'},
# }
# for role_name, props in roles.items():
# if not cls.objects.filter(name=role_name):
# role = cls.objects.create(name=role_name, comment=props.get('comment', ''), created_by='System')
# if props.get('permissions'):
# role.permissions = props.get('permissions')
from common.utils import encrypt, decrypt
class UserGroup(models.Model):
......@@ -65,6 +28,11 @@ class UserGroup(models.Model):
def __unicode__(self):
return self.name
def has_member(self, user):
if user in self.users.all():
return True
return False
class Meta:
db_table = 'user-group'
......@@ -113,8 +81,8 @@ class User(AbstractUser):
phone = models.CharField(max_length=20, blank=True, verbose_name=_('Phone'))
enable_otp = models.BooleanField(default=False, verbose_name=_('Enable OTP'))
secret_key_otp = models.CharField(max_length=16, blank=True)
private_key = models.CharField(max_length=5000, blank=True, verbose_name=_('ssh private key'))
public_key = models.CharField(max_length=1000, blank=True, verbose_name=_('ssh public key'))
_private_key = models.CharField(max_length=5000, blank=True, verbose_name=_('ssh private key'))
_public_key = models.CharField(max_length=1000, blank=True, verbose_name=_('ssh public key'))
comment = models.TextField(max_length=200, blank=True, verbose_name=_('Comment'))
is_first_login = models.BooleanField(default=False)
date_expired = models.DateTimeField(default=date_expired_default, blank=True, null=True,
......@@ -131,8 +99,8 @@ class User(AbstractUser):
#: user = User(username='example', ...)
#: user.set_password('password')
@password_raw.setter
def password_raw(self, raw_password):
self.set_password(raw_password)
def password_raw(self, password_raw_):
self.set_password(password_raw_)
@property
def is_expired(self):
......@@ -141,6 +109,22 @@ class User(AbstractUser):
else:
return True
@property
def private_key(self):
return decrypt(self._private_key)
@private_key.setter
def private_key(self, private_key_raw):
self._private_key = encrypt(private_key_raw)
@property
def public_key(self):
return decrypt(self._public_key)
@public_key.setter
def public_key(self, public_key_raw):
self._public_key = encrypt(public_key_raw)
@property
def is_superuser(self):
if self.role == 'Admin':
......@@ -198,6 +182,11 @@ class User(AbstractUser):
def generate_reset_token(self):
return signing.dumps({'reset': self.id, 'email': self.email})
def is_member_of(self, user_group):
if user_group in self.groups.all():
return True
return False
@classmethod
def validate_reset_token(cls, token, max_age=3600):
try:
......
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 to comment