core: add custom group model with hierarchy , add tree admin
This commit is contained in:
parent
ebda84bcaf
commit
d4a6e28fe6
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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"""
|
||||
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'))
|
||||
]
|
||||
|
|
|
@ -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'
|
|
@ -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')},
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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__)
|
||||
|
||||
@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):
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
Reference in New Issue