core: add custom group model with hierarchy , add tree admin

This commit is contained in:
Jens Langhammer 2018-12-27 00:38:42 +01:00
parent ebda84bcaf
commit d4a6e28fe6
No known key found for this signature in database
GPG Key ID: BEBC05297D92821B
17 changed files with 284 additions and 58 deletions

View 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)

View File

@ -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())),
# ]

View File

@ -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()

View 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

View File

@ -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__'

View File

@ -1 +1,3 @@
django-crispy-forms
django-rest-framework
django-rest-swagger

View 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 %}

View File

@ -1,8 +1,12 @@
"""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 = [
path('', overview.AdministrationOverviewView.as_view(), name='overview'),
@ -49,5 +53,9 @@ urlpatterns = [
users.UserDeleteView.as_view(), name='user-delete'),
# 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'))
]

View 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'

View 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')},
),
]

View 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),
),
]

View 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',
),
]

View 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')},
),
]

View File

@ -16,13 +16,28 @@ from passbook.lib.models import CreatedUpdatedModel, UUIDModel
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):
"""Custom User model to allow easier adding o f user-based settings"""
uuid = models.UUIDField(default=uuid4, editable=False)
sources = models.ManyToManyField('Source', through='UserSourceConnection')
applications = models.ManyToManyField('Application')
groups = models.ManyToManyField('Group')
@reversion.register()
class Provider(models.Model):

View File

@ -66,6 +66,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'reversion',
'rest_framework',
'rest_framework_swagger',
'passbook.core.apps.PassbookCoreConfig',
'passbook.admin.apps.PassbookAdminConfig',
'passbook.api.apps.PassbookAPIConfig',

View 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}

File diff suppressed because one or more lines are too long