├── .gitignore ├── README.md ├── example_apps ├── __init__.py ├── cheetahs │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── frogs │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── herons │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── polliwogs │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── pumas │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── teams │ └── mixins.py ├── tigers │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py └── toads │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py └── templates ├── cheetahs ├── base.html ├── cheetah_confirm_delete.html ├── cheetah_detail.html ├── cheetah_form.html └── cheetah_list.html ├── frogs ├── base.html ├── frog_detail.html ├── frog_form.html └── frog_list.html ├── herons ├── base.html ├── heron_confirm_delete.html ├── heron_detail.html ├── heron_form.html ├── heron_htmx_list.html └── heron_list.html ├── polliwogs ├── base.html ├── polliwog_detail.html ├── polliwog_form.html └── polliwog_list.html ├── pumas ├── base.html ├── puma_confirm_delete.html ├── puma_detail.html ├── puma_form.html └── puma_list.html ├── tigers ├── base.html ├── tiger_confirm_delete.html ├── tiger_detail.html ├── tiger_form.html └── tiger_list.html ├── toads ├── base.html ├── toad_detail.html ├── toad_form.html └── toad_list.html └── web └── components ├── htmx_paginator.html └── paginator.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode/* 3 | *.pyc 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example Models for SaaS Pegasus 2 | 3 | ## Use the New Version Instead! 4 | 5 | An improved and expanded version is now available at . Please see that version instead. 6 | 7 | ## Older version follows here 8 | 9 | Here are example apps for [SaaS Pegasus](https://saaspegasus.com). You can use these to get your own apps, models, and views up and running easily. 10 | 11 | * **Frogs** uses Function-Based Views, and the objects are cross-team 12 | * **Toads** uses Function-Based Views, and the objects are team-specific 13 | * **Polliwogs** are like Toads, but also implement Django-object permissions 14 | * **Cheetahs** uses Class-Based Views, and the objects are cross-team 15 | * **Tigers** uses Class-Based Views, and the objects are team-specific 16 | * **Pumas** are like Tigers, but also implement Django-object permissions 17 | * **Herons** are like Cheetahs, but use htmx to cleanly update the list when moving through pages of objects 18 | 19 | Each app: 20 | 21 | * Implements a model with several sample fields (`Name`, `Number`, and `Notes`) 22 | * Implements views (either Function-Based or Class-Based) for: 23 | * Create 24 | * List (summarize all objects, showing their `Name` and `Number`, including pagination) 25 | * Details (details on one object, showing all their fields) 26 | * Update 27 | * Delete 28 | * Implements a basic CRUD API using django-rest-framework 29 | 30 | I chose names that don't appear anywhere in the Pegasus codebase, to make it easy to search/replace if you use these to build your own apps. I use mnemonics: 31 | 32 | * **Frogs** is mnemonic for **F**unction-Based Views 33 | * **Cheetahs** is mnemonic for **C**lass-Based Views 34 | * **Toads** and **Tigers** are mnemonic for **T**eam-specific, and are of course cousins to Frogs and Cheetahs. 35 | * **Polliwogs** and **Pumas** are mnemonic for **P**ermissions-based. 36 | * **Herons** are mnemonic for "htmx" 37 | 38 | ## Installation 39 | 40 | In the following instructions, replace `project_slug` with your project's name. 41 | 42 | ### Clone this repository 43 | 44 | Clone this **pegasus-example-apps** repository into a folder next to your Pegasus project. 45 | 46 | ```bash 47 | git clone git@github.com:pcherna/pegasus-example-apps.git 48 | ``` 49 | 50 | ### Copy teams mixin into your Pegasus project 51 | 52 | Copy `example_apps/teams/mixins.py` into Pegasus project folder at `apps/teams/mixins.py` 53 | 54 | Note: Pegasus includes a mixins.py that is derived from this project, but for now you need to use the version included with this project. 55 | 56 | ### Copy Paginator into your Pegasus project 57 | 58 | Copy `templates/web/components/paginator.html` into Pegasus project folder at `templates/web/components/paginator.html` 59 | Copy `templates/web/components/htmx_paginator.html` into Pegasus project folder at `templates/web/components/htmx_paginator.html` 60 | 61 | ### Update your Python path 62 | 63 | So that your Pegasus project can find the **pegasus-example-apps**, add the following your Pegasus project's `manage.py`: 64 | 65 | ```python 66 | # Add this after import sys 67 | from pathlib import Path 68 | ... 69 | # Add this after the os.environ.setdefault(...) statement 70 | # You can map this wherever you cloned the repository. 71 | base_dir = Path(__file__).resolve().parent 72 | sys.path.append(str(base_dir / 'pegasus-example-apps')) 73 | ``` 74 | 75 | If you're using Celery you'll also need to do a similar thing in `project_slug/celery.py`: 76 | 77 | ```python 78 | # add these lines after os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project_slug.settings') 79 | base_dir = Path(__file__).resolve().parent.parent 80 | sys.path.append(str(base_dir / 'pegasus-example-apps')) 81 | ``` 82 | 83 | ### Integrate the new apps into your project 84 | 85 | * In `project_slug/settings.py`, to `PROJECT_APPS`, add: 86 | 87 | ```python 88 | 'example_apps.frogs.apps.FrogsConfig', 89 | 'example_apps.toads.apps.ToadsConfig', 90 | 'example_apps.polliwogs.apps.PolliwogsConfig', 91 | 'example_apps.cheetahs.apps.CheetahsConfig', 92 | 'example_apps.tigers.apps.TigersConfig', 93 | 'example_apps.pumas.apps.PumasConfig', 94 | 'example_apps.herons.apps.HeronsConfig', 95 | ``` 96 | 97 | * Also in `project_slug/settings.py`, to `TEMPLATES` `'DIRS'` key add: 98 | 99 | ```python 100 | BASE_DIR / 'pegasus-example-apps' / 'templates', 101 | ``` 102 | 103 | * In `project_slug/urls.py`, to `urlpatterns`, add: 104 | 105 | ```python 106 | path('frogs/', include('example_apps.frogs.urls')), 107 | path('cheetahs/', include('example_apps.cheetahs.urls')), 108 | path('herons/', include('example_apps.herons.urls')), 109 | ``` 110 | 111 | * Also in `project_slug/urls.py`, to `team_urlpatterns`, add: 112 | 113 | ```python 114 | path('toads/', include('example_apps.toads.urls')), 115 | path('polliwogs/', include('example_apps.polliwogs.urls')), 116 | path('tigers/', include('example_apps.tigers.urls')), 117 | path('pumas/', include('example_apps.pumas.urls')), 118 | ``` 119 | 120 | ### Update your database 121 | 122 | ```bash 123 | ./manage.py makemigrations frogs toads polliwogs cheetahs tigers pumas herons 124 | ./manage.py migrate 125 | ``` 126 | 127 | ## Notes and Todos 128 | 129 | Any and all comments and suggestions welcome. [peter@cherna.com](mailto:peter@cherna.com) 130 | 131 | * I have a decent mixin for team-specific apps using Class Based Views (see `apps/teams/mixins`, but not everything is transparently solved 132 | * The API entry points for Team-specific apps (Toads, Tigers, Pumas) handles team-filtering and related logic, but I'd like to build a clean Mixin to handle that 133 | * Only standard (Bulma) templates are currently supplied 134 | * There is an `UnorderedObjectListWarning` I haven't yet looked into, for team-specific CBVs 135 | * For Herons, the `?page=n` query-parameter in the URL drives the initial page, but is not updated when you move through the pages 136 | -------------------------------------------------------------------------------- /example_apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcherna/pegasus-example-apps/328a75c509899717f6939ca54881e824196e0884/example_apps/__init__.py -------------------------------------------------------------------------------- /example_apps/cheetahs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcherna/pegasus-example-apps/328a75c509899717f6939ca54881e824196e0884/example_apps/cheetahs/__init__.py -------------------------------------------------------------------------------- /example_apps/cheetahs/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Cheetah 3 | 4 | @admin.register(Cheetah) 5 | class CheetahAdmin(admin.ModelAdmin): 6 | # Fields to include in admin's list view 7 | list_display = ['name', 'number'] 8 | -------------------------------------------------------------------------------- /example_apps/cheetahs/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CheetahsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'example_apps.cheetahs' 7 | -------------------------------------------------------------------------------- /example_apps/cheetahs/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import Cheetah 4 | 5 | class CheetahForm(forms.ModelForm): 6 | class Meta: 7 | model = Cheetah 8 | fields = [ 9 | 'name', 'number', 'notes', 10 | ] 11 | -------------------------------------------------------------------------------- /example_apps/cheetahs/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | from apps.utils.models import BaseModel 4 | 5 | class Cheetah(BaseModel): 6 | # 'name' and 'number' are just example fields, visible in the List and Detail views 7 | name = models.CharField('Name', max_length=200) 8 | number = models.IntegerField('Number', default=0) 9 | # 'notes' is another example field that we will only show in the Detail view 10 | notes = models.TextField('Notes', max_length=4096, blank=True, default='') 11 | 12 | def __str__(self): 13 | return self.name 14 | 15 | def get_absolute_url(self): 16 | return reverse('cheetahs:cheetah-detailview', kwargs={'pk': self.pk}) 17 | -------------------------------------------------------------------------------- /example_apps/cheetahs/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import Cheetah 4 | 5 | 6 | class CheetahSerializer(serializers.ModelSerializer): 7 | 8 | class Meta: 9 | model = Cheetah 10 | fields = ('id', 'name', 'number', 'notes') 11 | -------------------------------------------------------------------------------- /example_apps/cheetahs/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /example_apps/cheetahs/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework import routers 3 | 4 | from . import views 5 | 6 | 7 | app_name = 'cheetahs' 8 | 9 | urlpatterns = [ 10 | path('', views.CheetahsListView.as_view(), name='cheetah-listview'), 11 | path('/', views.CheetahDetailView.as_view(), name='cheetah-detailview'), 12 | path('new/', views.CheetahCreateView.as_view(), name='cheetah-createview'), 13 | path('/update/', views.CheetahUpdateView.as_view(), name='cheetah-updateview'), 14 | path('/delete/', views.CheetahDeleteView.as_view(), name='cheetah-deleteview'), 15 | ] 16 | 17 | 18 | # drf config 19 | router = routers.DefaultRouter() 20 | router.register('api/cheetahs', views.CheetahViewSet) 21 | 22 | urlpatterns += router.urls 23 | -------------------------------------------------------------------------------- /example_apps/cheetahs/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | from django.contrib.auth.mixins import LoginRequiredMixin 3 | from django.views.generic import ( 4 | ListView, 5 | DetailView, 6 | CreateView, 7 | UpdateView, 8 | DeleteView 9 | ) 10 | from django.urls import reverse_lazy 11 | 12 | 13 | from .models import Cheetah 14 | # from .permissions import CheetahAccessPermissions 15 | from .forms import CheetahForm 16 | # from .admin import CheetahResource 17 | from .serializers import CheetahSerializer 18 | 19 | # Create your views here. 20 | 21 | # List of objects, at http:///cheetahs/ 22 | class CheetahsListView(LoginRequiredMixin, ListView): 23 | model = Cheetah 24 | paginate_by = 20 25 | template_name = 'cheetahs/cheetah_list.html' 26 | 27 | 28 | # One object, at http:///cheetahs/1/ 29 | class CheetahDetailView(LoginRequiredMixin, DetailView): 30 | model = Cheetah 31 | 32 | 33 | # Create a new object, at http:///cheetahs/new/ 34 | class CheetahCreateView(LoginRequiredMixin, CreateView): 35 | model = Cheetah 36 | form_class = CheetahForm 37 | 38 | 39 | # Update object, at http:///cheetahs/1/update/ 40 | class CheetahUpdateView(LoginRequiredMixin, UpdateView): 41 | model = Cheetah 42 | form_class = CheetahForm 43 | 44 | 45 | # Delete object, at http:///cheetahs/1/delete/ 46 | class CheetahDeleteView(LoginRequiredMixin, DeleteView): 47 | model = Cheetah 48 | success_url = reverse_lazy('cheetahs:cheetah-listview') 49 | 50 | 51 | # API at http://localhost:8000/cheetahs/api/cheetahs/ 52 | class CheetahViewSet(viewsets.ModelViewSet): 53 | serializer_class = CheetahSerializer 54 | queryset = Cheetah.objects.all() 55 | # ZZZ: Not sure why yet, but all users seem to be able to Read 56 | # permission_classes = (CheetahAccessPermissions,) 57 | -------------------------------------------------------------------------------- /example_apps/frogs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcherna/pegasus-example-apps/328a75c509899717f6939ca54881e824196e0884/example_apps/frogs/__init__.py -------------------------------------------------------------------------------- /example_apps/frogs/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Frog 3 | 4 | @admin.register(Frog) 5 | class FrogAdmin(admin.ModelAdmin): 6 | # Fields to include in admin's list view 7 | list_display = ['name', 'number'] 8 | -------------------------------------------------------------------------------- /example_apps/frogs/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FrogsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'example_apps.frogs' 7 | -------------------------------------------------------------------------------- /example_apps/frogs/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import Frog 4 | 5 | class FrogForm(forms.ModelForm): 6 | class Meta: 7 | model = Frog 8 | 9 | fields = [ 10 | 'name', 'number', 'notes', 11 | ] 12 | -------------------------------------------------------------------------------- /example_apps/frogs/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | from apps.utils.models import BaseModel 4 | 5 | class Frog(BaseModel): 6 | # 'name' and 'number' are just example fields, visible in the List and Detail views 7 | name = models.CharField('Name', max_length=200) 8 | number = models.IntegerField('Number', default=0) 9 | # 'notes' is another example field that we will only show in the Detail view 10 | notes = models.TextField('Notes', max_length=4096, blank=True, default='') 11 | 12 | def __str__(self): 13 | return self.name 14 | 15 | def get_absolute_url(self): 16 | return reverse('frogs:frog-detailview', kwargs={'pk': self.pk}) 17 | -------------------------------------------------------------------------------- /example_apps/frogs/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import Frog 4 | 5 | 6 | class FrogSerializer(serializers.ModelSerializer): 7 | 8 | class Meta: 9 | model = Frog 10 | fields = ('id', 'name', 'number', 'notes') 11 | -------------------------------------------------------------------------------- /example_apps/frogs/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /example_apps/frogs/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework import routers 3 | 4 | from . import views 5 | 6 | 7 | app_name = 'frogs' 8 | 9 | urlpatterns = [ 10 | path('', views.list_view, name='frog-listview'), 11 | path('/', views.detail_view, name='frog-detailview'), 12 | path('new/', views.create_view, name='frog-createview'), 13 | path('/update/', views.update_view, name='frog-updateview'), 14 | path('/delete/', views.delete_view, name='frog-deleteview'), 15 | ] 16 | 17 | 18 | # drf config 19 | router = routers.DefaultRouter() 20 | router.register('api/frogs', views.FrogViewSet) 21 | 22 | urlpatterns += router.urls 23 | -------------------------------------------------------------------------------- /example_apps/frogs/views.py: -------------------------------------------------------------------------------- 1 | from django.http.response import HttpResponseRedirect 2 | from django.shortcuts import get_object_or_404, render 3 | from django.contrib.auth.decorators import login_required 4 | 5 | from rest_framework import viewsets 6 | from rest_framework.permissions import DjangoModelPermissions 7 | from rest_framework.views import APIView 8 | from rest_framework.response import Response 9 | from django.urls import reverse 10 | 11 | 12 | from .models import Frog 13 | from .forms import FrogForm 14 | from .serializers import FrogSerializer 15 | 16 | # List of objects, at http:///frogs/ 17 | @login_required 18 | def list_view(request): 19 | context = {} 20 | context['objects'] = Frog.objects.all() 21 | return render(request, 'frogs/frog_list.html', context) 22 | 23 | # One object, at http:///frogs/1/ 24 | @login_required 25 | def detail_view(request, pk): 26 | context = {} 27 | context['object'] = Frog.objects.get(id=pk) 28 | return render(request, 'frogs/frog_detail.html', context) 29 | 30 | # Create a new object, at http:///frogs/new/ 31 | @login_required 32 | def create_view(request): 33 | context = {} 34 | form = FrogForm(request.POST or None) 35 | if form.is_valid(): 36 | saved_form = form.save() 37 | return HttpResponseRedirect(reverse('frogs:frog-detailview', kwargs={'pk': saved_form.id})) 38 | context['form'] = form 39 | return render(request, 'frogs/frog_form.html', context) 40 | 41 | # Update object, at http:///frogs/1/update/ 42 | @login_required 43 | def update_view(request, pk): 44 | context = {} 45 | obj = get_object_or_404(Frog, id=pk) 46 | form = FrogForm(request.POST or None, instance=obj) 47 | if form.is_valid(): 48 | form.save() 49 | return HttpResponseRedirect(reverse('frogs:frog-detailview', kwargs={'pk': pk})) 50 | context['form'] = form 51 | context['object'] = obj 52 | return render(request, 'frogs/frog_form.html', context) 53 | 54 | # delete object, at http:///frogs/1/delete/ 55 | @login_required 56 | def delete_view(request, pk): 57 | obj = get_object_or_404(Frog, id=pk) 58 | obj.delete() 59 | return HttpResponseRedirect(reverse('frogs:frog-listview')) 60 | 61 | 62 | # API at http://localhost:8000/frogs/api/frogs/ 63 | class FrogViewSet(viewsets.ModelViewSet): 64 | serializer_class = FrogSerializer 65 | queryset = Frog.objects.all() 66 | # ZZZ: Not sure why yet, but all users seem to be able to Read 67 | permission_classes = (DjangoModelPermissions,) 68 | 69 | # permission_classes = (FrogAccessPermissions,) 70 | 71 | # def get_queryset(self): 72 | # # filter queryset based on logged in user 73 | # return self.request.user.frogs.all() 74 | 75 | # def perform_create(self, serializer): 76 | # serializer.save(user=self.request.user) 77 | -------------------------------------------------------------------------------- /example_apps/herons/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcherna/pegasus-example-apps/328a75c509899717f6939ca54881e824196e0884/example_apps/herons/__init__.py -------------------------------------------------------------------------------- /example_apps/herons/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Heron 3 | 4 | @admin.register(Heron) 5 | class HeronAdmin(admin.ModelAdmin): 6 | # Fields to include in admin's list view 7 | list_display = ['name', 'number'] 8 | -------------------------------------------------------------------------------- /example_apps/herons/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class HeronsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'example_apps.herons' 7 | -------------------------------------------------------------------------------- /example_apps/herons/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import Heron 4 | 5 | class HeronForm(forms.ModelForm): 6 | class Meta: 7 | model = Heron 8 | fields = [ 9 | 'name', 'number', 'notes', 10 | ] 11 | -------------------------------------------------------------------------------- /example_apps/herons/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | from apps.utils.models import BaseModel 4 | 5 | class Heron(BaseModel): 6 | # 'name' and 'number' are just example fields, visible in the List and Detail views 7 | name = models.CharField('Name', max_length=200) 8 | number = models.IntegerField('Number', default=0) 9 | # 'notes' is another example field that we will only show in the Detail view 10 | notes = models.TextField('Notes', max_length=4096, blank=True, default='') 11 | 12 | def __str__(self): 13 | return self.name 14 | 15 | def get_absolute_url(self): 16 | return reverse('herons:heron-detailview', kwargs={'pk': self.pk}) 17 | -------------------------------------------------------------------------------- /example_apps/herons/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import Heron 4 | 5 | 6 | class HeronSerializer(serializers.ModelSerializer): 7 | 8 | class Meta: 9 | model = Heron 10 | fields = ('id', 'name', 'number', 'notes') 11 | -------------------------------------------------------------------------------- /example_apps/herons/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /example_apps/herons/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework import routers 3 | 4 | from . import views 5 | 6 | 7 | app_name = 'herons' 8 | 9 | urlpatterns = [ 10 | path('', views.HeronsListView.as_view(), name='heron-listview'), 11 | path('/', views.HeronDetailView.as_view(), name='heron-detailview'), 12 | path('new/', views.HeronCreateView.as_view(), name='heron-createview'), 13 | path('/update/', views.HeronUpdateView.as_view(), name='heron-updateview'), 14 | path('/delete/', views.HeronDeleteView.as_view(), name='heron-deleteview'), 15 | path('htmx/list/', views.heron_htmx_list, name='heron-htmx-list'), 16 | ] 17 | 18 | 19 | # drf config 20 | router = routers.DefaultRouter() 21 | router.register('api/herons', views.HeronViewSet) 22 | 23 | urlpatterns += router.urls 24 | -------------------------------------------------------------------------------- /example_apps/herons/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseRedirect, HttpResponse 2 | from django.shortcuts import render, get_object_or_404 3 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger 4 | from rest_framework import viewsets 5 | from django.contrib.auth.mixins import LoginRequiredMixin 6 | from django.contrib.auth.decorators import login_required 7 | from django.views.generic import ( 8 | ListView, 9 | DetailView, 10 | CreateView, 11 | UpdateView, 12 | DeleteView 13 | ) 14 | from django.urls import reverse, reverse_lazy 15 | 16 | 17 | from .models import Heron 18 | # from .permissions import HeronAccessPermissions 19 | from .forms import HeronForm 20 | # from .admin import HeronResource 21 | from .serializers import HeronSerializer 22 | 23 | # Create your views here. 24 | 25 | # List of objects, at http:///herons/ 26 | class HeronsListView(LoginRequiredMixin, ListView): 27 | model = Heron 28 | template_name = 'herons/heron_list.html' 29 | # Note: Do not use paginate_by in the ListView class -- see heron_htmx_list() 30 | 31 | def get_context_data(self, *args, **kwargs): 32 | context = super().get_context_data(*args, **kwargs) 33 | # Tell the template how it can find the htmx view that contains/refreshes the list data 34 | context['htmx_list_url'] = reverse('herons:heron-htmx-list') 35 | context['page'] = self.request.GET.get('page', 1) 36 | return context 37 | 38 | # def get_queryset(self): 39 | # return super().get_queryset() 40 | 41 | # htmx view that returns just the object list and paginators 42 | @login_required 43 | def heron_htmx_list(request): 44 | heron_list = Heron.objects.all() 45 | paginate_by = 4 46 | paginator = Paginator(heron_list, paginate_by) 47 | 48 | # Read the ?page= query-parameter 49 | page = request.GET.get('page', 1) 50 | 51 | # Implement pagination ourselves (we need the values within our context, 52 | # but also if we add htmx driven filtering, the values will change within our context) 53 | try: 54 | heron_list = paginator.page(page) 55 | except PageNotAnInteger: 56 | # Default page is first 57 | heron_list = paginator.page(1) 58 | except EmptyPage: 59 | # Do not go past the last page 60 | heron_list = paginator.page(paginator.num_pages) 61 | 62 | return render(request, 'herons/heron_htmx_list.html', { 63 | # Pagination context, just like ListView provides 64 | 'object_list': heron_list, 65 | 'paginator': paginator, 66 | 'page_obj': heron_list, 67 | 'is_paginated': paginator.num_pages > 1, 68 | # Tell the template how it can find the htmx view that contains/refreshes the list data 69 | 'htmx_list_url': reverse('herons:heron-htmx-list'), 70 | }) 71 | 72 | 73 | # One object, at http:///herons/1/ 74 | class HeronDetailView(LoginRequiredMixin, DetailView): 75 | model = Heron 76 | 77 | 78 | # Create a new object, at http:///herons/new/ 79 | class HeronCreateView(LoginRequiredMixin, CreateView): 80 | model = Heron 81 | form_class = HeronForm 82 | 83 | 84 | # Update object, at http:///herons/1/update/ 85 | class HeronUpdateView(LoginRequiredMixin, UpdateView): 86 | model = Heron 87 | form_class = HeronForm 88 | 89 | 90 | # Delete object, at http:///herons/1/delete/ 91 | class HeronDeleteView(LoginRequiredMixin, DeleteView): 92 | model = Heron 93 | success_url = reverse_lazy('herons:heron-listview') 94 | 95 | 96 | # API at http://localhost:8000/herons/api/herons/ 97 | class HeronViewSet(viewsets.ModelViewSet): 98 | serializer_class = HeronSerializer 99 | queryset = Heron.objects.all() 100 | # ZZZ: Not sure why yet, but all users seem to be able to Read 101 | # permission_classes = (HeronAccessPermissions,) 102 | -------------------------------------------------------------------------------- /example_apps/polliwogs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcherna/pegasus-example-apps/328a75c509899717f6939ca54881e824196e0884/example_apps/polliwogs/__init__.py -------------------------------------------------------------------------------- /example_apps/polliwogs/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Polliwog 3 | 4 | @admin.register(Polliwog) 5 | class PolliwogAdmin(admin.ModelAdmin): 6 | # Fields to include in admin's list view 7 | list_display = ['name', 'number'] 8 | -------------------------------------------------------------------------------- /example_apps/polliwogs/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PolliwogsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'example_apps.polliwogs' 7 | -------------------------------------------------------------------------------- /example_apps/polliwogs/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import Polliwog 4 | 5 | class PolliwogForm(forms.ModelForm): 6 | class Meta: 7 | model = Polliwog 8 | 9 | fields = [ 10 | 'name', 'number', 'notes', 11 | ] 12 | -------------------------------------------------------------------------------- /example_apps/polliwogs/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | from apps.utils.models import BaseModel 4 | from apps.teams.models import Team 5 | 6 | class Polliwog(BaseModel): 7 | team = models.ForeignKey(Team, verbose_name='Team', 8 | on_delete=models.DO_NOTHING, blank=False, null=False, editable=True) 9 | 10 | # 'name' and 'number' are just example fields, visible in the List and Detail views 11 | name = models.CharField('Name', max_length=200) 12 | number = models.IntegerField('Number', default=0) 13 | # 'notes' is another example field that we will only show in the Detail view 14 | notes = models.TextField('Notes', max_length=4096, blank=True, default='') 15 | 16 | def __str__(self): 17 | return self.name 18 | 19 | def get_absolute_url(self): 20 | return reverse('polliwogs:polliwog-detailview', kwargs={'team_slug': self.team.slug, 'pk': self.pk}) 21 | -------------------------------------------------------------------------------- /example_apps/polliwogs/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import Polliwog 4 | 5 | 6 | class PolliwogSerializer(serializers.ModelSerializer): 7 | 8 | class Meta: 9 | model = Polliwog 10 | fields = ('id', 'name', 'number', 'notes') 11 | -------------------------------------------------------------------------------- /example_apps/polliwogs/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /example_apps/polliwogs/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework import routers 3 | 4 | from . import views 5 | 6 | 7 | app_name = 'polliwogs' 8 | 9 | urlpatterns = [ 10 | path('', views.list_view, name='polliwog-listview'), 11 | path('/', views.detail_view, name='polliwog-detailview'), 12 | path('new/', views.create_view, name='polliwog-createview'), 13 | path('/update/', views.update_view, name='polliwog-updateview'), 14 | path('/delete/', views.delete_view, name='polliwog-deleteview'), 15 | ] 16 | 17 | 18 | # drf config 19 | router = routers.DefaultRouter() 20 | router.register('api/polliwogs', views.PolliwogViewSet) 21 | 22 | urlpatterns += router.urls 23 | -------------------------------------------------------------------------------- /example_apps/polliwogs/views.py: -------------------------------------------------------------------------------- 1 | from django.http.response import HttpResponseRedirect 2 | from django.shortcuts import get_object_or_404, render 3 | from django.contrib.auth.decorators import permission_required 4 | 5 | from rest_framework import viewsets 6 | from rest_framework.permissions import DjangoModelPermissions 7 | from rest_framework.exceptions import PermissionDenied 8 | from django.urls import reverse 9 | 10 | 11 | from .models import Polliwog 12 | from .forms import PolliwogForm 13 | from .serializers import PolliwogSerializer 14 | 15 | from apps.teams.models import Team 16 | from apps.teams.decorators import login_and_team_required 17 | from apps.teams.permissions import TeamAccessPermissions, TeamModelAccessPermissions 18 | from apps.teams.roles import is_admin, is_member 19 | 20 | # team_slug comes from the url you're trying to access 21 | # request.session['team'].slug and request.team are set for us by the login_and_team_required decorator 22 | 23 | # List of objects, at http:///a//polliwogs/ 24 | @login_and_team_required 25 | @permission_required('polliwogs.view_polliwog', raise_exception=True) 26 | def list_view(request, team_slug): 27 | context = {} 28 | # Only show this team's objects 29 | context['objects'] = Polliwog.objects.filter(team=request.team) 30 | context['team'] = request.team 31 | return render(request, 'polliwogs/polliwog_list.html', context) 32 | 33 | # One object, at http:///a//polliwogs/1/ 34 | @login_and_team_required 35 | @permission_required('polliwogs.view_polliwog', raise_exception=True) 36 | def detail_view(request, team_slug, pk): 37 | context = {} 38 | # Allow only if object belongs to this team 39 | context['object'] = get_object_or_404(Polliwog, id=pk, team=request.team) 40 | context['team'] = request.team 41 | return render(request, 'polliwogs/polliwog_detail.html', context) 42 | 43 | # Create a new object, at http:///a//polliwogs/new/ 44 | @login_and_team_required 45 | @permission_required('polliwogs.add_polliwog', raise_exception=True) 46 | def create_view(request, team_slug): 47 | context = {} 48 | form = PolliwogForm(request.POST or None) 49 | if form.is_valid(): 50 | new_object = form.save(commit=False) 51 | # Add my team to the object 52 | new_object.team = request.team 53 | new_object.save() 54 | return HttpResponseRedirect(reverse('polliwogs:polliwog-detailview', kwargs={'team_slug': team_slug, 'pk': new_object.id})) 55 | context['form'] = form 56 | context['team'] = request.team 57 | return render(request, 'polliwogs/polliwog_form.html', context) 58 | 59 | # Update object, at http:///a//polliwogs/1/update/ 60 | @login_and_team_required 61 | @permission_required('polliwogs.change_polliwog', raise_exception=True) 62 | def update_view(request, team_slug, pk): 63 | context = {} 64 | # Allow only if object belongs to this team 65 | obj = get_object_or_404(Polliwog, id=pk, team=request.team) 66 | form = PolliwogForm(request.POST or None, instance=obj) 67 | if form.is_valid(): 68 | form.save() 69 | return HttpResponseRedirect(reverse('polliwogs:polliwog-detailview', kwargs={'team_slug': team_slug, 'pk': pk})) 70 | context['form'] = form 71 | context['team'] = request.team 72 | context['object'] = obj 73 | return render(request, 'polliwogs/polliwog_form.html', context) 74 | 75 | # delete object, at http:///a//polliwogs/1/delete/ 76 | @login_and_team_required 77 | @permission_required('polliwogs.delete_polliwog', raise_exception=True) 78 | def delete_view(request, team_slug, pk): 79 | # Allow only if object belongs to this team 80 | obj = get_object_or_404(Polliwog, id=pk, team=request.team) 81 | obj.delete() 82 | return HttpResponseRedirect(reverse('polliwogs:polliwog-listview', kwargs={'team_slug': team_slug})) 83 | 84 | # API at http://localhost:8000/a//polliwogs/api/polliwogs/ 85 | class PolliwogViewSet(viewsets.ModelViewSet): 86 | queryset = Polliwog.objects.all() 87 | serializer_class = PolliwogSerializer 88 | permission_classes = (TeamModelAccessPermissions,) 89 | # ZZZ: Not sure why yet, but all users seem to be able to Read 90 | # permission_classes = (DjangoModelPermissions,) 91 | 92 | # permission_classes = (PolliwogAccessPermissions,) 93 | 94 | @property 95 | def team(self): 96 | """Get the team from the URL, and ensure user is a member.""" 97 | team = get_object_or_404(Team, slug=self.kwargs['team_slug']) 98 | if is_member(self.request.user, team): 99 | return team 100 | else: 101 | raise PermissionDenied() 102 | 103 | def get_queryset(self): 104 | """Filter queryset based on logged-in user's team.""" 105 | return self.queryset.filter(team=self.team) 106 | 107 | def perform_create(self, serializer): 108 | """Add team to the model during creation.""" 109 | serializer.save(team=self.team) 110 | -------------------------------------------------------------------------------- /example_apps/pumas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcherna/pegasus-example-apps/328a75c509899717f6939ca54881e824196e0884/example_apps/pumas/__init__.py -------------------------------------------------------------------------------- /example_apps/pumas/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Puma 3 | 4 | @admin.register(Puma) 5 | class PumaAdmin(admin.ModelAdmin): 6 | # Fields to include in admin's list view 7 | list_display = ['name', 'number'] 8 | -------------------------------------------------------------------------------- /example_apps/pumas/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PumasConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'example_apps.pumas' 7 | -------------------------------------------------------------------------------- /example_apps/pumas/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import Puma 4 | 5 | class PumaForm(forms.ModelForm): 6 | class Meta: 7 | model = Puma 8 | fields = [ 9 | 'name', 'number', 'notes', 10 | ] 11 | -------------------------------------------------------------------------------- /example_apps/pumas/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | from apps.utils.models import BaseModel 4 | 5 | from apps.teams.mixins import TeamModelMixin 6 | 7 | class Puma(TeamModelMixin, BaseModel): 8 | # 'name' and 'number' are just example fields, visible in the List and Detail views 9 | name = models.CharField('Name', max_length=200) 10 | number = models.IntegerField('Number', default=0) 11 | # 'notes' is another example field that we will only show in the Detail view 12 | notes = models.TextField('Notes', max_length=4096, blank=True, default='') 13 | 14 | def __str__(self): 15 | return self.name 16 | 17 | def get_absolute_url(self): 18 | return reverse('pumas:puma-detailview', kwargs={'team_slug': self.team.slug, 'pk': self.pk}) 19 | -------------------------------------------------------------------------------- /example_apps/pumas/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import Puma 4 | 5 | 6 | class PumaSerializer(serializers.ModelSerializer): 7 | 8 | class Meta: 9 | model = Puma 10 | fields = ('id', 'name', 'number', 'notes') 11 | -------------------------------------------------------------------------------- /example_apps/pumas/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /example_apps/pumas/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework import routers 3 | 4 | from . import views 5 | 6 | 7 | app_name = 'pumas' 8 | 9 | urlpatterns = [ 10 | path('', views.PumasListView.as_view(), name='puma-listview'), 11 | path('/', views.PumaDetailView.as_view(), name='puma-detailview'), 12 | path('new/', views.PumaCreateView.as_view(), name='puma-createview'), 13 | path('/update/', views.PumaUpdateView.as_view(), name='puma-updateview'), 14 | path('/delete/', views.PumaDeleteView.as_view(), name='puma-deleteview'), 15 | ] 16 | 17 | 18 | # drf config 19 | router = routers.DefaultRouter() 20 | router.register('api/pumas', views.PumaViewSet) 21 | 22 | urlpatterns += router.urls 23 | -------------------------------------------------------------------------------- /example_apps/pumas/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | from rest_framework.permissions import DjangoModelPermissions 3 | from rest_framework.exceptions import PermissionDenied 4 | from django.shortcuts import render, get_object_or_404 5 | from django.contrib.auth.mixins import UserPassesTestMixin 6 | from django.views.generic import ( 7 | ListView, 8 | DetailView, 9 | CreateView, 10 | UpdateView, 11 | DeleteView 12 | ) 13 | from django.urls import reverse_lazy 14 | 15 | 16 | from .models import Puma 17 | from .forms import PumaForm 18 | # from .admin import PumaResource 19 | from .serializers import PumaSerializer 20 | 21 | from apps.teams.models import Team 22 | from apps.teams.mixins import LoginAndTeamRequiredMixin 23 | from apps.teams.roles import is_admin, is_member 24 | from apps.teams.permissions import TeamAccessPermissions, TeamModelAccessPermissions 25 | 26 | # Create your views here. 27 | 28 | # List of objects, at http:///a//pumas/ 29 | class PumasListView(LoginAndTeamRequiredMixin, UserPassesTestMixin, ListView): 30 | model = Puma 31 | paginate_by = 20 32 | template_name = 'pumas/puma_list.html' 33 | 34 | def test_func(self): 35 | return self.request.user.has_perm('pumas.view_puma') 36 | 37 | # One object, at http:///a//pumas/1/ 38 | class PumaDetailView(LoginAndTeamRequiredMixin, UserPassesTestMixin, DetailView): 39 | model = Puma 40 | 41 | def test_func(self): 42 | return self.request.user.has_perm('pumas.view_puma') 43 | 44 | # Create a new object, at http:///a//pumas/new/ 45 | class PumaCreateView(LoginAndTeamRequiredMixin, UserPassesTestMixin, CreateView): 46 | model = Puma 47 | form_class = PumaForm 48 | 49 | def test_func(self): 50 | return self.request.user.has_perm('pumas.add_puma') 51 | 52 | 53 | # Update object, at http:///a//pumas/1/update/ 54 | class PumaUpdateView(LoginAndTeamRequiredMixin, UserPassesTestMixin, UpdateView): 55 | model = Puma 56 | form_class = PumaForm 57 | 58 | def test_func(self): 59 | return self.request.user.has_perm('pumas.change_puma') 60 | 61 | # Delete object, at http:///a//pumas/1/delete/ 62 | class PumaDeleteView(LoginAndTeamRequiredMixin, UserPassesTestMixin, DeleteView): 63 | model = Puma 64 | 65 | def get_success_url(self): 66 | return reverse_lazy('pumas:puma-listview', kwargs={'team_slug': self.request.team.slug}) 67 | 68 | def test_func(self): 69 | return self.request.user.has_perm('pumas.delete_puma') 70 | 71 | # API at http://localhost:8000/a//pumas/api/pumas/ 72 | class PumaViewSet(viewsets.ModelViewSet): 73 | queryset = Puma.objects.all() 74 | serializer_class = PumaSerializer 75 | permission_classes = (TeamModelAccessPermissions,) 76 | # ZZZ: Not sure why yet, but all users seem to be able to Read 77 | # permission_classes = (DjangoModelPermissions,) 78 | 79 | # permission_classes = (PumaAccessPermissions,) 80 | 81 | @property 82 | def team(self): 83 | """Get the team from the URL, and ensure user is a member.""" 84 | team = get_object_or_404(Team, slug=self.kwargs['team_slug']) 85 | if is_member(self.request.user, team): 86 | return team 87 | else: 88 | raise PermissionDenied() 89 | 90 | def get_queryset(self): 91 | """Filter queryset based on logged-in user's team.""" 92 | return self.queryset.filter(team=self.team) 93 | 94 | def perform_create(self, serializer): 95 | """Add team to the model during creation.""" 96 | serializer.save(team=self.team) 97 | -------------------------------------------------------------------------------- /example_apps/teams/mixins.py: -------------------------------------------------------------------------------- 1 | # apps/teams/mixins.py 2 | 3 | # To make a model team-specific: 4 | # - Add the TeamModelMixin to your model class 5 | # - In your model, add a get_absolute_url() that adds the team slug, e.g.: 6 | # def get_absolute_url(self): 7 | # return reverse('appname:model-detailview', kwargs={'team_slug': self.team.slug, 'pk': self.pk}) 8 | # To make the views for a team-specific model: 9 | # - Add the LoginAndTeamRequiredMixin or TeamAdminRequiredMixin to each view class 10 | # - In the delete-view, add a get_success_url() method which includes 11 | # kwargs={'team_slug': self.request.team.slug} 12 | # - Update all references to your views to include the team slug as the first argument 13 | 14 | from django.db import models 15 | from django.contrib.auth.mixins import AccessMixin 16 | from django.utils.decorators import method_decorator 17 | 18 | from apps.teams.decorators import login_and_team_required, team_admin_required 19 | 20 | from .models import Team 21 | 22 | 23 | class TeamObjectViewMixin(AccessMixin): 24 | """ 25 | Abstract model for Django class-based views for a model that belongs to a Team 26 | """ 27 | 28 | def get_context_data(self, *args, **kwargs): 29 | """Add team to the context, for use by templates.""" 30 | context = super().get_context_data(*args, **kwargs) 31 | context['team'] = self.request.team 32 | return context 33 | 34 | def get_queryset(self): 35 | """Narrow queryset to only include objects of this team.""" 36 | return self.model.objects.filter(team=self.request.team) 37 | 38 | def form_valid(self, form): 39 | form.instance.team = self.request.team 40 | return super().form_valid(form) 41 | 42 | # For deletion, it would be nice if we could override get_success_url to include 43 | # kwargs={'team_slug': self.object.team.slug} 44 | # I don't yet know if that can be done with a mixin 45 | 46 | class Meta: 47 | abstract = True 48 | 49 | 50 | class LoginAndTeamRequiredMixin(TeamObjectViewMixin): 51 | """ 52 | Verify that the current user is authenticated and a member of the team. 53 | """ 54 | 55 | @method_decorator(login_and_team_required) 56 | def dispatch(self, request, *args, **kwargs): 57 | return super().dispatch(request, *args, **kwargs) 58 | 59 | 60 | class TeamAdminRequiredMixin(TeamObjectViewMixin): 61 | """ 62 | Verify that the current user is authenticated and admin of the team. 63 | """ 64 | 65 | @method_decorator(team_admin_required) 66 | def dispatch(self, request, *args, **kwargs): 67 | return super().dispatch(request, *args, **kwargs) 68 | 69 | 70 | class TeamModelMixin(models.Model): 71 | """ 72 | Abstract model for objects with a team relationship 73 | """ 74 | team = models.ForeignKey(Team, verbose_name="Team", 75 | on_delete=models.DO_NOTHING, blank=False, null=False, editable=True) 76 | 77 | class Meta: 78 | abstract = True 79 | 80 | 81 | -------------------------------------------------------------------------------- /example_apps/tigers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcherna/pegasus-example-apps/328a75c509899717f6939ca54881e824196e0884/example_apps/tigers/__init__.py -------------------------------------------------------------------------------- /example_apps/tigers/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Tiger 3 | 4 | @admin.register(Tiger) 5 | class TigerAdmin(admin.ModelAdmin): 6 | # Fields to include in admin's list view 7 | list_display = ['name', 'number'] 8 | -------------------------------------------------------------------------------- /example_apps/tigers/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TigersConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'example_apps.tigers' 7 | -------------------------------------------------------------------------------- /example_apps/tigers/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import Tiger 4 | 5 | class TigerForm(forms.ModelForm): 6 | class Meta: 7 | model = Tiger 8 | fields = [ 9 | 'name', 'number', 'notes', 10 | ] 11 | -------------------------------------------------------------------------------- /example_apps/tigers/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | from apps.utils.models import BaseModel 4 | 5 | from apps.teams.mixins import TeamModelMixin 6 | 7 | class Tiger(TeamModelMixin, BaseModel): 8 | # 'name' and 'number' are just example fields, visible in the List and Detail views 9 | name = models.CharField('Name', max_length=200) 10 | number = models.IntegerField('Number', default=0) 11 | # 'notes' is another example field that we will only show in the Detail view 12 | notes = models.TextField('Notes', max_length=4096, blank=True, default='') 13 | 14 | def __str__(self): 15 | return self.name 16 | 17 | def get_absolute_url(self): 18 | return reverse('tigers:tiger-detailview', kwargs={'team_slug': self.team.slug, 'pk': self.pk}) 19 | -------------------------------------------------------------------------------- /example_apps/tigers/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import Tiger 4 | 5 | 6 | class TigerSerializer(serializers.ModelSerializer): 7 | 8 | class Meta: 9 | model = Tiger 10 | fields = ('id', 'name', 'number', 'notes') 11 | -------------------------------------------------------------------------------- /example_apps/tigers/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /example_apps/tigers/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework import routers 3 | 4 | from . import views 5 | 6 | 7 | app_name = 'tigers' 8 | 9 | urlpatterns = [ 10 | path('', views.TigersListView.as_view(), name='tiger-listview'), 11 | path('/', views.TigerDetailView.as_view(), name='tiger-detailview'), 12 | path('new/', views.TigerCreateView.as_view(), name='tiger-createview'), 13 | path('/update/', views.TigerUpdateView.as_view(), name='tiger-updateview'), 14 | path('/delete/', views.TigerDeleteView.as_view(), name='tiger-deleteview'), 15 | ] 16 | 17 | 18 | # drf config 19 | router = routers.DefaultRouter() 20 | router.register('api/tigers', views.TigerViewSet) 21 | 22 | urlpatterns += router.urls 23 | -------------------------------------------------------------------------------- /example_apps/tigers/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | from rest_framework.exceptions import PermissionDenied 3 | from django.shortcuts import render, get_object_or_404 4 | from django.views.generic import ( 5 | ListView, 6 | DetailView, 7 | CreateView, 8 | UpdateView, 9 | DeleteView 10 | ) 11 | from django.urls import reverse_lazy 12 | 13 | 14 | from .models import Tiger 15 | from .forms import TigerForm 16 | # from .admin import TigerResource 17 | from .serializers import TigerSerializer 18 | 19 | from apps.teams.models import Team 20 | from apps.teams.mixins import LoginAndTeamRequiredMixin 21 | from apps.teams.roles import is_admin, is_member 22 | from apps.teams.permissions import TeamAccessPermissions, TeamModelAccessPermissions 23 | 24 | # Create your views here. 25 | 26 | # List of objects, at http:///a//tigers/ 27 | class TigersListView(LoginAndTeamRequiredMixin, ListView): 28 | model = Tiger 29 | paginate_by = 20 30 | template_name = 'tigers/tiger_list.html' 31 | 32 | 33 | # One object, at http:///a//tigers/1/ 34 | class TigerDetailView(LoginAndTeamRequiredMixin, DetailView): 35 | model = Tiger 36 | 37 | 38 | # Create a new object, at http:///a//tigers/new/ 39 | class TigerCreateView(LoginAndTeamRequiredMixin, CreateView): 40 | model = Tiger 41 | form_class = TigerForm 42 | 43 | 44 | # Update object, at http:///a//tigers/1/update/ 45 | class TigerUpdateView(LoginAndTeamRequiredMixin, UpdateView): 46 | model = Tiger 47 | form_class = TigerForm 48 | 49 | 50 | # Delete object, at http:///a//tigers/1/delete/ 51 | class TigerDeleteView(LoginAndTeamRequiredMixin, DeleteView): 52 | model = Tiger 53 | 54 | def get_success_url(self): 55 | return reverse_lazy('tigers:tiger-listview', kwargs={'team_slug': self.request.team.slug}) 56 | 57 | 58 | # API at http://localhost:8000/a//tigers/api/tigers/ 59 | class TigerViewSet(viewsets.ModelViewSet): 60 | queryset = Tiger.objects.all() 61 | serializer_class = TigerSerializer 62 | permission_classes = (TeamModelAccessPermissions,) 63 | 64 | # permission_classes = (TigerAccessPermissions,) 65 | 66 | @property 67 | def team(self): 68 | """Get the team from the URL, and ensure user is a member.""" 69 | team = get_object_or_404(Team, slug=self.kwargs['team_slug']) 70 | if is_member(self.request.user, team): 71 | return team 72 | else: 73 | raise PermissionDenied() 74 | 75 | def get_queryset(self): 76 | """Filter queryset based on logged-in user's team.""" 77 | return self.queryset.filter(team=self.team) 78 | 79 | def perform_create(self, serializer): 80 | """Add team to the model during creation.""" 81 | serializer.save(team=self.team) 82 | -------------------------------------------------------------------------------- /example_apps/toads/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcherna/pegasus-example-apps/328a75c509899717f6939ca54881e824196e0884/example_apps/toads/__init__.py -------------------------------------------------------------------------------- /example_apps/toads/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Toad 3 | 4 | @admin.register(Toad) 5 | class ToadAdmin(admin.ModelAdmin): 6 | # Fields to include in admin's list view 7 | list_display = ['name', 'number'] 8 | -------------------------------------------------------------------------------- /example_apps/toads/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ToadsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'example_apps.toads' 7 | -------------------------------------------------------------------------------- /example_apps/toads/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import Toad 4 | 5 | class ToadForm(forms.ModelForm): 6 | class Meta: 7 | model = Toad 8 | 9 | fields = [ 10 | 'name', 'number', 'notes', 11 | ] 12 | -------------------------------------------------------------------------------- /example_apps/toads/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | from apps.utils.models import BaseModel 4 | from apps.teams.models import Team 5 | 6 | class Toad(BaseModel): 7 | team = models.ForeignKey(Team, verbose_name='Team', 8 | on_delete=models.DO_NOTHING, blank=False, null=False, editable=True) 9 | 10 | # 'name' and 'number' are just example fields, visible in the List and Detail views 11 | name = models.CharField('Name', max_length=200) 12 | number = models.IntegerField('Number', default=0) 13 | # 'notes' is another example field that we will only show in the Detail view 14 | notes = models.TextField('Notes', max_length=4096, blank=True, default='') 15 | 16 | def __str__(self): 17 | return self.name 18 | 19 | def get_absolute_url(self): 20 | return reverse('toads:toad-detailview', kwargs={'team_slug': self.team.slug, 'pk': self.pk}) 21 | -------------------------------------------------------------------------------- /example_apps/toads/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import Toad 4 | 5 | 6 | class ToadSerializer(serializers.ModelSerializer): 7 | 8 | class Meta: 9 | model = Toad 10 | fields = ('id', 'name', 'number', 'notes') 11 | -------------------------------------------------------------------------------- /example_apps/toads/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /example_apps/toads/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework import routers 3 | 4 | from . import views 5 | 6 | 7 | app_name = 'toads' 8 | 9 | urlpatterns = [ 10 | path('', views.list_view, name='toad-listview'), 11 | path('/', views.detail_view, name='toad-detailview'), 12 | path('new/', views.create_view, name='toad-createview'), 13 | path('/update/', views.update_view, name='toad-updateview'), 14 | path('/delete/', views.delete_view, name='toad-deleteview'), 15 | ] 16 | 17 | 18 | # drf config 19 | router = routers.DefaultRouter() 20 | router.register('api/toads', views.ToadViewSet) 21 | 22 | urlpatterns += router.urls 23 | -------------------------------------------------------------------------------- /example_apps/toads/views.py: -------------------------------------------------------------------------------- 1 | from django.http.response import HttpResponseRedirect 2 | from django.shortcuts import get_object_or_404, render 3 | 4 | from rest_framework import viewsets 5 | from rest_framework.permissions import DjangoModelPermissions 6 | from rest_framework.exceptions import PermissionDenied 7 | from django.urls import reverse 8 | 9 | 10 | from .models import Toad 11 | from .forms import ToadForm 12 | from .serializers import ToadSerializer 13 | 14 | from apps.teams.models import Team 15 | from apps.teams.decorators import login_and_team_required 16 | from apps.teams.permissions import TeamAccessPermissions, TeamModelAccessPermissions 17 | from apps.teams.roles import is_admin, is_member 18 | 19 | # team_slug comes from the url you're trying to access 20 | # request.session['team'].slug and request.team are set for us by the login_and_team_required decorator 21 | 22 | # List of objects, at http:///a//toads/ 23 | @login_and_team_required 24 | def list_view(request, team_slug): 25 | context = {} 26 | # Only show this team's objects 27 | context['objects'] = Toad.objects.filter(team=request.team) 28 | context['team'] = request.team 29 | return render(request, 'toads/toad_list.html', context) 30 | 31 | # One object, at http:///a//toads/1/ 32 | @login_and_team_required 33 | def detail_view(request, team_slug, pk): 34 | context = {} 35 | # Allow only if object belongs to this team 36 | context['object'] = get_object_or_404(Toad, id=pk, team=request.team) 37 | context['team'] = request.team 38 | return render(request, 'toads/toad_detail.html', context) 39 | 40 | # Create a new object, at http:///a//toads/new/ 41 | @login_and_team_required 42 | def create_view(request, team_slug): 43 | context = {} 44 | form = ToadForm(request.POST or None) 45 | if form.is_valid(): 46 | new_object = form.save(commit=False) 47 | # Add my team to the object 48 | new_object.team = request.team 49 | new_object.save() 50 | return HttpResponseRedirect(reverse('toads:toad-detailview', kwargs={'team_slug': team_slug, 'pk': new_object.id})) 51 | context['form'] = form 52 | context['team'] = request.team 53 | return render(request, 'toads/toad_form.html', context) 54 | 55 | # Update object, at http:///a//toads/1/update/ 56 | @login_and_team_required 57 | def update_view(request, team_slug, pk): 58 | context = {} 59 | # Allow only if object belongs to this team 60 | obj = get_object_or_404(Toad, id=pk, team=request.team) 61 | form = ToadForm(request.POST or None, instance=obj) 62 | if form.is_valid(): 63 | form.save() 64 | return HttpResponseRedirect(reverse('toads:toad-detailview', kwargs={'team_slug': team_slug, 'pk': pk})) 65 | context['form'] = form 66 | context['team'] = request.team 67 | context['object'] = obj 68 | return render(request, 'toads/toad_form.html', context) 69 | 70 | # delete object, at http:///a//toads/1/delete/ 71 | @login_and_team_required 72 | def delete_view(request, team_slug, pk): 73 | # Allow only if object belongs to this team 74 | obj = get_object_or_404(Toad, id=pk, team=request.team) 75 | obj.delete() 76 | return HttpResponseRedirect(reverse('toads:toad-listview', kwargs={'team_slug': team_slug})) 77 | 78 | # API at http://localhost:8000/a//toads/api/toads/ 79 | class ToadViewSet(viewsets.ModelViewSet): 80 | queryset = Toad.objects.all() 81 | serializer_class = ToadSerializer 82 | permission_classes = (TeamModelAccessPermissions,) 83 | # ZZZ: Not sure why yet, but all users seem to be able to Read 84 | # permission_classes = (DjangoModelPermissions,) 85 | 86 | # permission_classes = (ToadAccessPermissions,) 87 | 88 | @property 89 | def team(self): 90 | """Get the team from the URL, and ensure user is a member.""" 91 | team = get_object_or_404(Team, slug=self.kwargs['team_slug']) 92 | if is_member(self.request.user, team): 93 | return team 94 | else: 95 | raise PermissionDenied() 96 | 97 | def get_queryset(self): 98 | """Filter queryset based on logged-in user's team.""" 99 | return self.queryset.filter(team=self.team) 100 | 101 | def perform_create(self, serializer): 102 | """Add team to the model during creation.""" 103 | serializer.save(team=self.team) 104 | -------------------------------------------------------------------------------- /templates/cheetahs/base.html: -------------------------------------------------------------------------------- 1 | {% extends "web/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block body %} 5 |
6 | {% block content %}{% endblock %} 7 |
8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/cheetahs/cheetah_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "cheetahs/base.html" %} 2 | {% load static %} 3 | {% load socialaccount %} 4 | {% load bulma_form_tags %} 5 | {% block body %} 6 |
7 |
8 |

Delete Cheetah

9 |
10 | {% csrf_token %} 11 | {{ form.non_field_errors }} 12 | 13 |

Are you sure you want to delete the cheetah "{{ object.name }}"?

14 | 15 |
16 | Cancel 17 | 18 |
19 | 20 |
21 |
22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /templates/cheetahs/cheetah_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "cheetahs/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block content %} 5 |
6 | < Cheetah List 7 | + New 8 | Edit 9 | Delete 10 |
11 |
12 |
{{ object.name }}
13 |
14 |
15 |
Id: {{ object.id }}
16 |
Number: {{ object.number }}
17 |
Notes: {{ object.notes }}
18 | 19 |
20 |
Created at: {{ object.created_at }}
21 | {% if object.modified_at %} 22 |
Modified at: {{ object.modified_at }}
23 | {% endif %} 24 |
25 |
26 |
27 |
28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /templates/cheetahs/cheetah_form.html: -------------------------------------------------------------------------------- 1 | {% extends "cheetahs/base.html" %} 2 | {% load static %} 3 | {% load socialaccount %} 4 | {% load bulma_form_tags %} 5 | {% block body %} 6 | {% if object.id %} 7 | Cancel 8 | {% else %} 9 | Cancel 10 | {% endif %} 11 |
12 |
13 | {% if object.id %} 14 |

Update Cheetah

15 | {% else %} 16 |

New Cheetah

17 | {% endif %} 18 |
19 | {% csrf_token %} 20 | {{ form.non_field_errors }} 21 | {% render_text_input form.name %} 22 | {% render_text_input form.number %} 23 | {% render_text_input form.notes %} 24 | 25 | 26 | 27 |
28 |
29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/cheetahs/cheetah_list.html: -------------------------------------------------------------------------------- 1 | {% extends "cheetahs/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block content %} 5 |
Cheetahs
6 |
Cheetahs are example cross-team objects using Class-Based Views
7 | + New Cheetah 8 | 9 | {% include "web/components/paginator.html" %} 10 | 11 | {% for object in object_list %} 12 |
13 |
14 | 15 |
16 |
17 |
Id: {{ object.id }}
18 |
Number: {{ object.number }}
19 |
20 |
21 | {% endfor %} 22 | 23 | {% include "web/components/paginator.html" %} 24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /templates/frogs/base.html: -------------------------------------------------------------------------------- 1 | {% extends "web/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block body %} 5 |
6 | {% block content %}{% endblock %} 7 |
8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/frogs/frog_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "frogs/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block content %} 5 |
6 | < Frog List 7 | + New 8 | Edit 9 | Delete 10 |
11 |
12 |
{{ object.name }}
13 |
14 |
15 |
Id: {{ object.id }}
16 |
Number: {{ object.number }}
17 |
Notes: {{ object.notes }}
18 | 19 |
20 |
Created at: {{ object.created_at }}
21 | {% if object.modified_at %} 22 |
Modified at: {{ object.modified_at }}
23 | {% endif %} 24 |
25 |
26 |
27 |
28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /templates/frogs/frog_form.html: -------------------------------------------------------------------------------- 1 | {% extends "frogs/base.html" %} 2 | {% load static %} 3 | {% load socialaccount %} 4 | {% load bulma_form_tags %} 5 | {% block body %} 6 | {% if object.id %} 7 | Cancel 8 | {% else %} 9 | Cancel 10 | {% endif %} 11 |
12 |
13 | {% if object.id %} 14 |

Update Frog

15 | {% else %} 16 |

New Frog

17 | {% endif %} 18 |
19 | {% csrf_token %} 20 | {{ form.non_field_errors }} 21 | {% render_text_input form.name %} 22 | {% render_text_input form.number %} 23 | {% render_text_input form.notes %} 24 | 25 | 26 | 27 |
28 |
29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/frogs/frog_list.html: -------------------------------------------------------------------------------- 1 | {% extends "frogs/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block content %} 5 |
Frogs
6 |
Frogs are example cross-team objects using Function-Based Views
7 | + New Frog 8 | 9 | {% include "web/components/paginator.html" %} 10 | 11 | {% for object in object_list %} 12 |
13 |
14 | 15 |
16 |
17 |
Id: {{ object.id }}
18 |
Number: {{ object.number }}
19 |
20 |
21 | {% endfor %} 22 | 23 | {% include "web/components/paginator.html" %} 24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /templates/herons/base.html: -------------------------------------------------------------------------------- 1 | {% extends "web/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | 5 | {% block page_head %} 6 | 7 | {% endblock page_head %} 8 | 9 | {% block body %} 10 |
11 | {% block content %}{% endblock %} 12 |
13 | {% endblock %} 14 | 15 | {% block page_js %} 16 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /templates/herons/heron_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "herons/base.html" %} 2 | {% load static %} 3 | {% load socialaccount %} 4 | {% load bulma_form_tags %} 5 | {% block body %} 6 |
7 |
8 |

Delete Heron

9 |
10 | {% csrf_token %} 11 | {{ form.non_field_errors }} 12 | 13 |

Are you sure you want to delete the heron "{{ object.name }}"?

14 | 15 |
16 | Cancel 17 | 18 |
19 | 20 |
21 |
22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /templates/herons/heron_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "herons/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block content %} 5 |
6 | < Heron List 7 | + New 8 | Edit 9 | Delete 10 |
11 |
12 |
{{ object.name }}
13 |
14 |
15 |
Id: {{ object.id }}
16 |
Number: {{ object.number }}
17 |
Notes: {{ object.notes }}
18 | 19 |
20 |
Created at: {{ object.created_at }}
21 | {% if object.modified_at %} 22 |
Modified at: {{ object.modified_at }}
23 | {% endif %} 24 |
25 |
26 |
27 |
28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /templates/herons/heron_form.html: -------------------------------------------------------------------------------- 1 | {% extends "herons/base.html" %} 2 | {% load static %} 3 | {% load socialaccount %} 4 | {% load bulma_form_tags %} 5 | {% block body %} 6 | {% if object.id %} 7 | Cancel 8 | {% else %} 9 | Cancel 10 | {% endif %} 11 |
12 |
13 | {% if object.id %} 14 |

Update Heron

15 | {% else %} 16 |

New Heron

17 | {% endif %} 18 |
19 | {% csrf_token %} 20 | {{ form.non_field_errors }} 21 | {% render_text_input form.name %} 22 | {% render_text_input form.number %} 23 | {% render_text_input form.notes %} 24 | 25 | 26 | 27 |
28 |
29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/herons/heron_htmx_list.html: -------------------------------------------------------------------------------- 1 | 2 | {% include "web/components/htmx_paginator.html" %} 3 | 4 | {% for object in object_list %} 5 |
6 | 9 |
10 |
Id: {{ object.id }}
11 |
Number: {{ object.number }}
12 |
13 |
14 | {% endfor %} 15 | 16 | {% include "web/components/htmx_paginator.html" %} 17 | -------------------------------------------------------------------------------- /templates/herons/heron_list.html: -------------------------------------------------------------------------------- 1 | {% extends "herons/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | 5 | {% block content %} 6 |
Herons
7 |
Herons are example objects (CBV) that show off some htmx
8 | 9 | + New Heron 10 | 11 | 12 | 13 |
14 | 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /templates/polliwogs/base.html: -------------------------------------------------------------------------------- 1 | {% extends "web/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block body %} 5 |
6 | {% block content %}{% endblock %} 7 |
8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/polliwogs/polliwog_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "polliwogs/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block content %} 5 |
6 | < Polliwog List 7 | {% if perms.polliwogs.add_polliwog %} 8 | + New 9 | {% endif %} 10 | {% if perms.polliwogs.change_polliwog %} 11 | Edit 12 | {% endif %} 13 | {% if perms.polliwogs.delete_polliwog %} 14 | Delete 15 | {% endif %} 16 |
17 |
18 |
{{ object.name }}
19 |
20 |
21 |
Id: {{ object.id }}
22 |
Number: {{ object.number }}
23 |
Notes: {{ object.notes }}
24 | 25 |
26 |
Created at: {{ object.created_at }}
27 | {% if object.modified_at %} 28 |
Modified at: {{ object.modified_at }}
29 | {% endif %} 30 |
31 |
32 |
33 |
34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /templates/polliwogs/polliwog_form.html: -------------------------------------------------------------------------------- 1 | {% extends "polliwogs/base.html" %} 2 | {% load static %} 3 | {% load socialaccount %} 4 | {% load bulma_form_tags %} 5 | {% block body %} 6 | {% if object.id %} 7 | Cancel 8 | {% else %} 9 | Cancel 10 | {% endif %} 11 |
12 |
13 | {% if object.id %} 14 |

Update Polliwog

15 | {% else %} 16 |

New Polliwog

17 | {% endif %} 18 |
19 | {% csrf_token %} 20 | {{ form.non_field_errors }} 21 | {% render_text_input form.name %} 22 | {% render_text_input form.number %} 23 | {% render_text_input form.notes %} 24 | 25 | 26 | 27 |
28 |
29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/polliwogs/polliwog_list.html: -------------------------------------------------------------------------------- 1 | {% extends "polliwogs/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block content %} 5 |
Polliwogs
6 |
Polliwogs are example team-specific objects using Function-Based Views, with permissions
7 | {% if perms.polliwogs.add_polliwog %} 8 | + New Polliwog 9 | {% endif %} 10 | 11 | {% include "web/components/paginator.html" %} 12 | 13 | {% for object in object_list %} 14 |
15 |
16 | 17 |
18 |
19 |
Id: {{ object.id }}
20 |
Number: {{ object.number }}
21 |
22 |
23 | {% endfor %} 24 | 25 | {% include "web/components/paginator.html" %} 26 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /templates/pumas/base.html: -------------------------------------------------------------------------------- 1 | {% extends "web/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block body %} 5 |
6 | {% block content %}{% endblock %} 7 |
8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/pumas/puma_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "pumas/base.html" %} 2 | {% load static %} 3 | {% load socialaccount %} 4 | {% load bulma_form_tags %} 5 | {% block body %} 6 |
7 |
8 |

Delete Puma

9 |
10 | {% csrf_token %} 11 | {{ form.non_field_errors }} 12 | 13 |

Are you sure you want to delete the puma "{{ object.name }}"?

14 | 15 |
16 | Cancel 17 | 18 |
19 | 20 |
21 |
22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /templates/pumas/puma_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "pumas/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block content %} 5 |
6 | < Puma List 7 | {% if perms.pumas.add_puma %} 8 | + New 9 | {% endif %} 10 | {% if perms.pumas.change_puma %} 11 | Edit 12 | {% endif %} 13 | {% if perms.pumas.delete_puma %} 14 | Delete 15 | {% endif %} 16 |
17 |
18 |
{{ object.name }}
19 |
20 |
21 |
Id: {{ object.id }}
22 |
Number: {{ object.number }}
23 |
Notes: {{ object.notes }}
24 | 25 |
26 |
Created at: {{ object.created_at }}
27 | {% if object.modified_at %} 28 |
Modified at: {{ object.modified_at }}
29 | {% endif %} 30 |
31 |
32 |
33 |
34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /templates/pumas/puma_form.html: -------------------------------------------------------------------------------- 1 | {% extends "pumas/base.html" %} 2 | {% load static %} 3 | {% load socialaccount %} 4 | {% load bulma_form_tags %} 5 | {% block body %} 6 | {% if object.id %} 7 | Cancel 8 | {% else %} 9 | Cancel 10 | {% endif %} 11 |
12 |
13 | {% if object.id %} 14 |

Update Puma

15 | {% else %} 16 |

New Puma

17 | {% endif %} 18 |
19 | {% csrf_token %} 20 | {{ form.non_field_errors }} 21 | {% render_text_input form.name %} 22 | {% render_text_input form.number %} 23 | {% render_text_input form.notes %} 24 | 25 | 26 | 27 |
28 |
29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/pumas/puma_list.html: -------------------------------------------------------------------------------- 1 | {% extends "pumas/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block content %} 5 |
Pumas
6 |
Pumas are example team-specific objects using Class-Based Views, with permissions
7 | {% if perms.pumas.add_puma %} 8 | + New Puma 9 | {% endif %} 10 | 11 | {% include "web/components/paginator.html" %} 12 | 13 | {% for object in object_list %} 14 |
15 |
16 | 17 |
18 |
19 |
Id: {{ object.id }}
20 |
Number: {{ object.number }}
21 |
22 |
23 | {% endfor %} 24 | 25 | {% include "web/components/paginator.html" %} 26 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /templates/tigers/base.html: -------------------------------------------------------------------------------- 1 | {% extends "web/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block body %} 5 |
6 | {% block content %}{% endblock %} 7 |
8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/tigers/tiger_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "tigers/base.html" %} 2 | {% load static %} 3 | {% load socialaccount %} 4 | {% load bulma_form_tags %} 5 | {% block body %} 6 |
7 |
8 |

Delete Tiger

9 |
10 | {% csrf_token %} 11 | {{ form.non_field_errors }} 12 | 13 |

Are you sure you want to delete the tiger "{{ object.name }}"?

14 | 15 |
16 | Cancel 17 | 18 |
19 | 20 |
21 |
22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /templates/tigers/tiger_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "tigers/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block content %} 5 |
6 | < Tiger List 7 | + New 8 | Edit 9 | Delete 10 |
11 |
12 |
{{ object.name }}
13 |
14 |
15 |
Id: {{ object.id }}
16 |
Number: {{ object.number }}
17 |
Notes: {{ object.notes }}
18 | 19 |
20 |
Created at: {{ object.created_at }}
21 | {% if object.modified_at %} 22 |
Modified at: {{ object.modified_at }}
23 | {% endif %} 24 |
25 |
26 |
27 |
28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /templates/tigers/tiger_form.html: -------------------------------------------------------------------------------- 1 | {% extends "tigers/base.html" %} 2 | {% load static %} 3 | {% load socialaccount %} 4 | {% load bulma_form_tags %} 5 | {% block body %} 6 | {% if object.id %} 7 | Cancel 8 | {% else %} 9 | Cancel 10 | {% endif %} 11 |
12 |
13 | {% if object.id %} 14 |

Update Tiger

15 | {% else %} 16 |

New Tiger

17 | {% endif %} 18 |
19 | {% csrf_token %} 20 | {{ form.non_field_errors }} 21 | {% render_text_input form.name %} 22 | {% render_text_input form.number %} 23 | {% render_text_input form.notes %} 24 | 25 | 26 | 27 |
28 |
29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/tigers/tiger_list.html: -------------------------------------------------------------------------------- 1 | {% extends "tigers/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block content %} 5 |
Tigers
6 |
Tigers are example team-specific objects using Class-Based Views
7 | + New Tiger 8 | 9 | {% include "web/components/paginator.html" %} 10 | 11 | {% for object in object_list %} 12 |
13 |
14 | 15 |
16 |
17 |
Id: {{ object.id }}
18 |
Number: {{ object.number }}
19 |
20 |
21 | {% endfor %} 22 | 23 | {% include "web/components/paginator.html" %} 24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /templates/toads/base.html: -------------------------------------------------------------------------------- 1 | {% extends "web/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block body %} 5 |
6 | {% block content %}{% endblock %} 7 |
8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/toads/toad_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "toads/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block content %} 5 |
6 | < Toad List 7 | + New 8 | Edit 9 | Delete 10 |
11 |
12 |
{{ object.name }}
13 |
14 |
15 |
Id: {{ object.id }}
16 |
Number: {{ object.number }}
17 |
Notes: {{ object.notes }}
18 | 19 |
20 |
Created at: {{ object.created_at }}
21 | {% if object.modified_at %} 22 |
Modified at: {{ object.modified_at }}
23 | {% endif %} 24 |
25 |
26 |
27 |
28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /templates/toads/toad_form.html: -------------------------------------------------------------------------------- 1 | {% extends "toads/base.html" %} 2 | {% load static %} 3 | {% load socialaccount %} 4 | {% load bulma_form_tags %} 5 | {% block body %} 6 | {% if object.id %} 7 | Cancel 8 | {% else %} 9 | Cancel 10 | {% endif %} 11 |
12 |
13 | {% if object.id %} 14 |

Update Toad

15 | {% else %} 16 |

New Toad

17 | {% endif %} 18 |
19 | {% csrf_token %} 20 | {{ form.non_field_errors }} 21 | {% render_text_input form.name %} 22 | {% render_text_input form.number %} 23 | {% render_text_input form.notes %} 24 | 25 | 26 | 27 |
28 |
29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/toads/toad_list.html: -------------------------------------------------------------------------------- 1 | {% extends "toads/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block content %} 5 |
Toads
6 |
Toads are example team-specific objects using Function-Based Views
7 | + New Toad 8 | 9 | {% include "web/components/paginator.html" %} 10 | 11 | {% for object in object_list %} 12 |
13 |
14 | 15 |
16 |
17 |
Id: {{ object.id }}
18 |
Number: {{ object.number }}
19 |
20 |
21 | {% endfor %} 22 | 23 | {% include "web/components/paginator.html" %} 24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /templates/web/components/htmx_paginator.html: -------------------------------------------------------------------------------- 1 | {% if is_paginated %} 2 |
3 | {% if page_obj.has_previous %} 4 | First 5 | 6 | 7 | {% else %} 8 | First 9 | 10 | {% endif %} 11 | {% for num in page_obj.paginator.page_range %} 12 | {% if page_obj.number == num %} 13 | {{ num }} 14 | {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %} 15 | {{ num }} 16 | {% elif num > page_obj.number|add:'-4' and num < page_obj.number|add:'4' %} 17 | 18 | {% endif %} 19 | {% endfor %} 20 | {% if page_obj.has_next %} 21 | 22 | Last 23 | {% else %} 24 | 25 | Last 26 | {% endif %} 27 |
28 | {% endif %} 29 | -------------------------------------------------------------------------------- /templates/web/components/paginator.html: -------------------------------------------------------------------------------- 1 | {% if is_paginated %} 2 |
3 | {% if page_obj.has_previous %} 4 | First 5 | 6 | {% else %} 7 | First 8 | 9 | {% endif %} 10 | {% for num in page_obj.paginator.page_range %} 11 | {% if page_obj.number == num %} 12 | {{ num }} 13 | {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %} 14 | {{ num }} 15 | {% elif num > page_obj.number|add:'-4' and num < page_obj.number|add:'4' %} 16 | 17 | {% endif %} 18 | {% endfor %} 19 | {% if page_obj.has_next %} 20 | 21 | Last 22 | {% else %} 23 | 24 | Last 25 | {% endif %} 26 |
27 | {% endif %} 28 | --------------------------------------------------------------------------------