core: add custom group model with hierarchy , add tree admin
This commit is contained in:
parent
ebda84bcaf
commit
d4a6e28fe6
36
passbook/admin/api/v1/groups.py
Normal file
36
passbook/admin/api/v1/groups.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
"""passbook admin gorup API"""
|
||||||
|
from rest_framework.permissions import IsAdminUser
|
||||||
|
from rest_framework.serializers import ModelSerializer, Serializer
|
||||||
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from passbook.core.models import Group
|
||||||
|
|
||||||
|
|
||||||
|
class RecursiveField(Serializer):
|
||||||
|
"""Recursive field for manytomanyfield"""
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
serializer = self.parent.parent.__class__(value, context=self.context)
|
||||||
|
return serializer.data
|
||||||
|
|
||||||
|
def create(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
class GroupSerializer(ModelSerializer):
|
||||||
|
"""Group Serializer"""
|
||||||
|
|
||||||
|
children = RecursiveField(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Group
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class GroupViewSet(ModelViewSet):
|
||||||
|
"""Group Viewset"""
|
||||||
|
|
||||||
|
permission_classes = [IsAdminUser]
|
||||||
|
serializer_class = GroupSerializer
|
||||||
|
queryset = Group.objects.filter(parent__isnull=True)
|
|
@ -1,8 +0,0 @@
|
||||||
# from django.conf.urls import url, include
|
|
||||||
|
|
||||||
# # Add this!
|
|
||||||
# from passbook.admin.api.v1.source import SourceResource
|
|
||||||
|
|
||||||
# urlpatterns = [
|
|
||||||
# url(r'source/', include(SourceResource.urls())),
|
|
||||||
# ]
|
|
|
@ -1,26 +0,0 @@
|
||||||
# from rest_framework.serializers import HyperlinkedModelSerializer
|
|
||||||
# from passbook.admin.api.v1.utils import LookupSerializer
|
|
||||||
# from passbook.core.models import Source
|
|
||||||
# from passbook.oauth_client.models import OAuthSource
|
|
||||||
|
|
||||||
# from rest_framework.viewsets import ModelViewSet
|
|
||||||
|
|
||||||
# class LookupSourceSerializer(HyperlinkedModelSerializer):
|
|
||||||
|
|
||||||
# def to_representation(self, instance):
|
|
||||||
# if isinstance(instance, Source):
|
|
||||||
# return SourceSerializer(instance=instance).data
|
|
||||||
# elif isinstance(instance, OAuthSource):
|
|
||||||
# return OAuthSourceSerializer(instance=instance).data
|
|
||||||
# else:
|
|
||||||
# return LookupSourceSerializer(instance=instance).data
|
|
||||||
|
|
||||||
# class Meta:
|
|
||||||
# model = Source
|
|
||||||
# fields = '__all__'
|
|
||||||
|
|
||||||
|
|
||||||
# class SourceViewSet(ModelViewSet):
|
|
||||||
|
|
||||||
# serializer_class = LookupSourceSerializer
|
|
||||||
# queryset = Source.objects.select_subclasses()
|
|
9
passbook/admin/api/v1/urls.py
Normal file
9
passbook/admin/api/v1/urls.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
"""passbook admin API URLs"""
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
|
from passbook.admin.api.v1.groups import GroupViewSet
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register(r'groups', GroupViewSet)
|
||||||
|
|
||||||
|
urlpatterns = router.urls
|
|
@ -1,18 +0,0 @@
|
||||||
"""passbook admin api utils"""
|
|
||||||
# from django.db.models import Model
|
|
||||||
# from rest_framework.serializers import ModelSerializer
|
|
||||||
|
|
||||||
|
|
||||||
# class LookupSerializer(ModelSerializer):
|
|
||||||
|
|
||||||
# mapping = {}
|
|
||||||
|
|
||||||
# def to_representation(self, instance):
|
|
||||||
# for __model, __serializer in self.mapping.items():
|
|
||||||
# if isinstance(instance, __model):
|
|
||||||
# return __serializer(instance=instance).to_representation(instance)
|
|
||||||
# raise KeyError(instance.__class__.__name__)
|
|
||||||
|
|
||||||
# class Meta:
|
|
||||||
# model = Model
|
|
||||||
# fields = '__all__'
|
|
|
@ -1 +1,3 @@
|
||||||
django-crispy-forms
|
django-crispy-forms
|
||||||
|
django-rest-framework
|
||||||
|
django-rest-swagger
|
||||||
|
|
83
passbook/admin/templates/administration/groups/list.html
Normal file
83
passbook/admin/templates/administration/groups/list.html
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
{% extends "administration/base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
{% load utils %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
{{ block.super }}
|
||||||
|
<link rel="stylesheet" href="{% static 'css/bootstrap-treeview.min.css'%}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script src="{% static 'js/bootstrap-treeview.min.js' %}"></script>
|
||||||
|
<script>
|
||||||
|
var cleanupData = function (obj) {
|
||||||
|
return {
|
||||||
|
text: obj.name,
|
||||||
|
href: '?group=' + obj.uuid,
|
||||||
|
nodes: obj.children.map(cleanupData),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
$(function() {
|
||||||
|
var apiUrl = "{% url 'passbook_admin:group-list' %}?format=json";
|
||||||
|
$.ajax({
|
||||||
|
url: apiUrl,
|
||||||
|
}).done(function(data) {
|
||||||
|
$('#treeview1').treeview({
|
||||||
|
collapseIcon: "fa fa-angle-down",
|
||||||
|
data: data.map(cleanupData),
|
||||||
|
expandIcon: "fa fa-angle-right",
|
||||||
|
nodeIcon: "fa pficon-users",
|
||||||
|
showBorder: true,
|
||||||
|
enableLinks: true,
|
||||||
|
onNodeSelected: function (event, node) {
|
||||||
|
window.location.href = node.href;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% title %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div id="treeview1" class="treeview">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<h1>{% trans "Invitations" %}</h1>
|
||||||
|
<a href="{% url 'passbook_admin:invitation-create' %}" class="btn btn-primary">
|
||||||
|
{% trans 'Create...' %}
|
||||||
|
</a>
|
||||||
|
<hr>
|
||||||
|
<table class="table table-striped table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans 'Expiry' %}</th>
|
||||||
|
<th>{% trans 'Link' %}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for invitation in object_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ invitation.expires|default:"Never" }}</td>
|
||||||
|
<td>
|
||||||
|
<pre>{{ invitation.link }}</pre>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:invitation-delete' pk=invitation.uuid %}?back={{ request.get_full_path }}">{%
|
||||||
|
trans 'Delete' %}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -1,8 +1,12 @@
|
||||||
"""passbook URL Configuration"""
|
"""passbook URL Configuration"""
|
||||||
from django.urls import path
|
from django.urls import include, path
|
||||||
|
from rest_framework_swagger.views import get_swagger_view
|
||||||
|
|
||||||
|
from passbook.admin.views import (applications, audit, groups, invitations,
|
||||||
|
overview, providers, rules, sources, users)
|
||||||
|
|
||||||
|
schema_view = get_swagger_view(title='passbook Admin Internal API')
|
||||||
|
|
||||||
from passbook.admin.views import (applications, audit, invitations, overview,
|
|
||||||
providers, rules, sources, users)
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', overview.AdministrationOverviewView.as_view(), name='overview'),
|
path('', overview.AdministrationOverviewView.as_view(), name='overview'),
|
||||||
|
@ -49,5 +53,9 @@ urlpatterns = [
|
||||||
users.UserDeleteView.as_view(), name='user-delete'),
|
users.UserDeleteView.as_view(), name='user-delete'),
|
||||||
# Audit Log
|
# Audit Log
|
||||||
path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'),
|
path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'),
|
||||||
# path('api/v1/', include('passbook.admin.api.v1.urls'))
|
# Groups
|
||||||
|
path('groups/', groups.GroupListView.as_view(), name='groups'),
|
||||||
|
# API
|
||||||
|
path('api/', schema_view),
|
||||||
|
path('api/v1/', include('passbook.admin.api.v1.urls'))
|
||||||
]
|
]
|
||||||
|
|
12
passbook/admin/views/groups.py
Normal file
12
passbook/admin/views/groups.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
"""passbook Group administration"""
|
||||||
|
from django.views.generic import ListView
|
||||||
|
|
||||||
|
from passbook.admin.mixins import AdminRequiredMixin
|
||||||
|
from passbook.core.models import Group
|
||||||
|
|
||||||
|
|
||||||
|
class GroupListView(AdminRequiredMixin, ListView):
|
||||||
|
"""Show list of all invitations"""
|
||||||
|
|
||||||
|
model = Group
|
||||||
|
template_name = 'administration/groups/list.html'
|
35
passbook/core/migrations/0005_auto_20181226_2115.py
Normal file
35
passbook/core/migrations/0005_auto_20181226_2115.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 2.1.4 on 2018-12-26 21:15
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0009_alter_user_last_name_max_length'),
|
||||||
|
('passbook_core', '0004_application_slug'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Group',
|
||||||
|
fields=[
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=80, verbose_name='name')),
|
||||||
|
('parent_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='passbook_core.Group')),
|
||||||
|
('permissions', models.ManyToManyField(blank=True, related_name='_group_permissions_+', to='auth.Permission')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='groups',
|
||||||
|
field=models.ManyToManyField(to='passbook_core.Group'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='group',
|
||||||
|
unique_together={('name', 'parent_group')},
|
||||||
|
),
|
||||||
|
]
|
18
passbook/core/migrations/0006_group_extra_data.py
Normal file
18
passbook/core/migrations/0006_group_extra_data.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.1.4 on 2018-12-26 21:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('passbook_core', '0005_auto_20181226_2115'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='group',
|
||||||
|
name='extra_data',
|
||||||
|
field=models.TextField(blank=True),
|
||||||
|
),
|
||||||
|
]
|
30
passbook/core/migrations/0007_auto_20181226_2142.py
Normal file
30
passbook/core/migrations/0007_auto_20181226_2142.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 2.1.4 on 2018-12-26 21:42
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('passbook_core', '0006_group_extra_data'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='group',
|
||||||
|
name='children',
|
||||||
|
field=models.ManyToManyField(blank=True, to='passbook_core.Group'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='group',
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='group',
|
||||||
|
name='parent_group',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='group',
|
||||||
|
name='permissions',
|
||||||
|
),
|
||||||
|
]
|
27
passbook/core/migrations/0008_auto_20181226_2200.py
Normal file
27
passbook/core/migrations/0008_auto_20181226_2200.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 2.1.4 on 2018-12-26 22:00
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('passbook_core', '0007_auto_20181226_2142'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='group',
|
||||||
|
name='parent',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='passbook_core.Group'),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='group',
|
||||||
|
name='children',
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='group',
|
||||||
|
unique_together={('name', 'parent')},
|
||||||
|
),
|
||||||
|
]
|
|
@ -16,13 +16,28 @@ from passbook.lib.models import CreatedUpdatedModel, UUIDModel
|
||||||
|
|
||||||
LOGGER = getLogger(__name__)
|
LOGGER = getLogger(__name__)
|
||||||
|
|
||||||
@reversion.register()
|
class Group(UUIDModel):
|
||||||
|
"""Custom Group model which supports a basic hierarchy"""
|
||||||
|
|
||||||
|
name = models.CharField(_('name'), max_length=80)
|
||||||
|
parent = models.ForeignKey('Group', blank=True, null=True,
|
||||||
|
on_delete=models.SET_NULL, related_name='children')
|
||||||
|
extra_data = models.TextField(blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Group %s" % self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
unique_together = (('name', 'parent',),)
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
"""Custom User model to allow easier adding o f user-based settings"""
|
"""Custom User model to allow easier adding o f user-based settings"""
|
||||||
|
|
||||||
uuid = models.UUIDField(default=uuid4, editable=False)
|
uuid = models.UUIDField(default=uuid4, editable=False)
|
||||||
sources = models.ManyToManyField('Source', through='UserSourceConnection')
|
sources = models.ManyToManyField('Source', through='UserSourceConnection')
|
||||||
applications = models.ManyToManyField('Application')
|
applications = models.ManyToManyField('Application')
|
||||||
|
groups = models.ManyToManyField('Group')
|
||||||
|
|
||||||
@reversion.register()
|
@reversion.register()
|
||||||
class Provider(models.Model):
|
class Provider(models.Model):
|
||||||
|
|
|
@ -66,6 +66,7 @@ INSTALLED_APPS = [
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'reversion',
|
'reversion',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
|
'rest_framework_swagger',
|
||||||
'passbook.core.apps.PassbookCoreConfig',
|
'passbook.core.apps.PassbookCoreConfig',
|
||||||
'passbook.admin.apps.PassbookAdminConfig',
|
'passbook.admin.apps.PassbookAdminConfig',
|
||||||
'passbook.api.apps.PassbookAPIConfig',
|
'passbook.api.apps.PassbookAPIConfig',
|
||||||
|
|
1
passbook/core/static/css/bootstrap-treeview.min.css
vendored
Normal file
1
passbook/core/static/css/bootstrap-treeview.min.css
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon,.treeview span.image{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed}.treeview .node-hidden{display:none}.treeview span.image{display:inline-block;height:1.19em;vertical-align:middle;background-size:contain;background-repeat:no-repeat;line-height:1em}.treeview span.icon.node-icon-background{padding:2px;width:calc(1em + 4px);height:calc(1em + 4px);line-height:1em}
|
1
passbook/core/static/js/bootstrap-treeview.min.js
vendored
Normal file
1
passbook/core/static/js/bootstrap-treeview.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in a new issue