core: add custom group model with hierarchy , add tree admin
This commit is contained in:
		
							
								
								
									
										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""" | ||||
| 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')) | ||||
| ] | ||||
|  | ||||
							
								
								
									
										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__) | ||||
|  | ||||
| @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', | ||||
|  | ||||
							
								
								
									
										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 New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer