add Tags, Properties and Categories
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
j3d1 2023-06-22 04:08:52 +02:00
parent 91cd5c57b3
commit 7e1b92a162
10 changed files with 414 additions and 1 deletions

View file

@ -18,7 +18,7 @@ python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python configure.py
python manage.py runserver 0.0.0.0:800 --insecure
python manage.py runserver 0.0.0.0:8000 --insecure
```
to run this in properly in production, you need to configure a webserver to serve the static files and proxy the requests to the backend, then run the backend with just `python manage.py runserver` without the `--insecure` flag.

View file

@ -33,5 +33,6 @@ urlpatterns = [
path('auth/', include('authentication.api')),
path('admin/', include('hostadmin.api')),
path('api/', include('toolshed.api.friend')),
path('api/', include('toolshed.api.info')),
path('docs/', schema_view.with_ui('swagger', cache_timeout=0), name='api-docs'),
]

View file

@ -0,0 +1,68 @@
from django.urls import path
from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from hostadmin.models import Domain
from authentication.signature_auth import SignatureAuthentication
from toolshed.models import Tag, Property, Category
from toolshed.serializers import CategorySerializer, PropertySerializer
@api_view(['GET'])
@permission_classes([])
@authentication_classes([])
def list_domains(request, format=None): # /domains/
domains = [domain.name for domain in Domain.objects.filter(open_registration=True)]
return Response(domains)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
@authentication_classes([SignatureAuthentication])
def list_tags(format=None): # /tags/
tags = [tag.name for tag in Tag.objects.all()]
return Response(tags)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
@authentication_classes([SignatureAuthentication])
def list_properties(request, format=None): # /properties/
return Response(PropertySerializer(Property.objects.all(), many=True).data)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
@authentication_classes([SignatureAuthentication])
def list_categories(request, format=None): # /categories/
return Response(CategorySerializer(Category.objects.all(), many=True).data)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
@authentication_classes([SignatureAuthentication])
def list_availability_policies(request, format=None): # /availability_policies/
policies = ['private', 'friends', 'internal', 'public']
return Response(policies)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
@authentication_classes([SignatureAuthentication])
def combined_info(request, format=None): # /info/
tags = [tag.name for tag in Tag.objects.all()]
properties = [property.name for property in Property.objects.all()]
categories = [category.name for category in Category.objects.all()]
policies = ['private', 'friends', 'internal', 'public']
return Response({'tags': tags, 'properties': properties, 'policies': policies, 'categories': categories})
urlpatterns = [
path('availability_policies/', list_availability_policies, name='availability_policies'),
path('properties/', list_properties, name='propertylist'),
path('categories/', list_categories, name='categorylist'),
path('domains/', list_domains, name='domainlist'),
path('tags/', list_tags, name='taglist'),
path('info/', combined_info, name='info'),
]

View file

@ -0,0 +1,59 @@
# Generated by Django 4.2.2 on 2023-06-22 02:03
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_deleted', models.BooleanField(default=False)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('name', models.CharField(max_length=255, unique=True)),
('description', models.TextField(blank=True, null=True)),
('origin', models.CharField(max_length=255)),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='toolshed.category')),
],
options={
'verbose_name_plural': 'categories',
},
),
migrations.CreateModel(
name='Tag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True, null=True)),
('origin', models.CharField(max_length=255)),
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tags', to='toolshed.category')),
],
),
migrations.CreateModel(
name='Property',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True, null=True)),
('unit_symbol', models.CharField(blank=True, max_length=16, null=True)),
('unit_name', models.CharField(blank=True, max_length=255, null=True)),
('unit_name_plural', models.CharField(blank=True, max_length=255, null=True)),
('base2_prefix', models.BooleanField(default=False)),
('dimensions', models.IntegerField(default=1, validators=[django.core.validators.MinValueValidator(1)])),
('origin', models.CharField(max_length=255)),
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='properties', to='toolshed.category')),
],
options={
'verbose_name_plural': 'properties',
},
),
]

View file

@ -0,0 +1,47 @@
from django.db import models
from django.core.validators import MinValueValidator
from django_softdelete.models import SoftDeleteModel
from authentication.models import ToolshedUser, KnownIdentity
class Category(SoftDeleteModel):
name = models.CharField(max_length=255, unique=True)
description = models.TextField(null=True, blank=True)
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')
origin = models.CharField(max_length=255, null=False, blank=False)
class Meta:
verbose_name_plural = 'categories'
def __str__(self):
parent = str(self.parent) + "/" if self.parent else ""
return parent + self.name
class Property(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)
category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True, related_name='properties')
unit_symbol = models.CharField(max_length=16, null=True, blank=True)
unit_name = models.CharField(max_length=255, null=True, blank=True)
unit_name_plural = models.CharField(max_length=255, null=True, blank=True)
base2_prefix = models.BooleanField(default=False)
dimensions = models.IntegerField(null=False, blank=False, default=1, validators=[MinValueValidator(1)])
origin = models.CharField(max_length=255)
class Meta:
verbose_name_plural = 'properties'
def __str__(self):
return self.name
class Tag(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)
category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True, related_name='tags')
origin = models.CharField(max_length=255)
def __str__(self):
return self.name

View file

@ -1,5 +1,6 @@
from rest_framework import serializers
from authentication.models import KnownIdentity
from toolshed.models import Category, Property
class FriendSerializer(serializers.ModelSerializer):
@ -11,3 +12,20 @@ class FriendSerializer(serializers.ModelSerializer):
def get_username(self, obj):
return obj.username + '@' + obj.domain
class PropertySerializer(serializers.ModelSerializer):
category = serializers.SlugRelatedField(queryset=Category.objects.all(), slug_field='name')
class Meta:
model = Property
fields = ['name', 'description', 'category', 'unit_symbol', 'unit_name', 'unit_name_plural', 'base2_prefix']
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ['name']
def to_representation(self, instance):
return str(instance)

View file

@ -0,0 +1,54 @@
from django.test import TestCase, Client
from authentication.tests import UserTestCase, SignatureAuthClient
from toolshed.models import Category, Tag, Property
anonymous_client = Client()
client = SignatureAuthClient()
class CombinedApiTestCase(UserTestCase):
def setUp(self):
super().setUp()
self.cat1 = Category.objects.create(name='cat1')
self.cat2 = Category.objects.create(name='cat2')
self.cat3 = Category.objects.create(name='cat3')
self.tag1 = Tag.objects.create(name='tag1')
self.tag2 = Tag.objects.create(name='tag2')
self.tag3 = Tag.objects.create(name='tag3')
self.prop1 = Property.objects.create(name='prop1')
self.prop2 = Property.objects.create(name='prop2')
self.prop3 = Property.objects.create(name='prop3')
def test_domains_anonymous(self):
response = anonymous_client.get('/api/domains/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), ['example.com'])
def test_domains_authenticated(self):
response = client.get('/api/domains/', self.local_user1)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), ['example.com'])
def test_combined_api_anonymous(self):
response = anonymous_client.get('/api/info/')
self.assertEqual(response.status_code, 403)
def test_combined_api(self):
response = client.get('/api/info/', self.local_user1)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['policies'], ['private', 'friends', 'internal', 'public'])
self.assertEqual(response.json()['categories'], ['cat1', 'cat2', 'cat3'])
self.assertEqual(response.json()['tags'], ['tag1', 'tag2', 'tag3'])
self.assertEqual(response.json()['properties'], ['prop1', 'prop2', 'prop3'])
def test_policy_api_anonymous(self):
response = anonymous_client.get('/api/availability_policies/')
self.assertEqual(response.status_code, 403)
def test_policy_api(self):
response = client.get('/api/availability_policies/', self.local_user1)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), ['private', 'friends', 'internal', 'public'])

View file

@ -0,0 +1,53 @@
from authentication.tests import UserTestCase, SignatureAuthClient
from toolshed.models import Category
client = SignatureAuthClient()
class CategoryTestCase(UserTestCase):
def setUp(self):
super().setUp()
self.cat1 = Category.objects.create(name='cat1')
self.cat1.save()
self.cat2 = Category.objects.create(name='cat2')
self.cat2.save()
self.cat3 = Category.objects.create(name='cat3')
self.cat3.save()
def test_get_categories(self):
subcat1 = Category.objects.create(name='subcat1', parent=self.cat1)
subcat1.save()
subcat2 = Category.objects.create(name='subcat2', parent=self.cat1)
subcat2.save()
subcat3 = Category.objects.create(name='subcat3', parent=subcat1)
subcat3.save()
self.assertEqual(self.cat1.children.count(), 2)
self.assertEqual(self.cat1.children.first(), subcat1)
self.assertEqual(self.cat1.children.last(), subcat2)
self.assertEqual(subcat1.parent, self.cat1)
self.assertEqual(subcat2.parent, self.cat1)
self.assertEqual(subcat3.parent, subcat1)
self.assertEqual(str(subcat1), 'cat1/subcat1')
self.assertEqual(str(subcat2), 'cat1/subcat2')
self.assertEqual(str(subcat3), 'cat1/subcat1/subcat3')
class CategoryApiTestCase(UserTestCase):
def setUp(self):
super().setUp()
self.cat1 = Category.objects.create(name='cat1')
self.cat1.save()
self.cat2 = Category.objects.create(name='cat2')
self.cat2.save()
self.cat3 = Category.objects.create(name='cat3')
self.cat3.save()
def test_get_categories(self):
reply = client.get('/api/categories/', self.local_user1)
self.assertEqual(reply.status_code, 200)
self.assertEqual(len(reply.json()), 3)
self.assertEqual(reply.json()[0], 'cat1')
self.assertEqual(reply.json()[1], 'cat2')
self.assertEqual(reply.json()[2], 'cat3')

View file

@ -0,0 +1,55 @@
from authentication.tests import UserTestCase, SignatureAuthClient
from toolshed.models import Property, Category
client = SignatureAuthClient()
class PropertyTestCase(UserTestCase):
def setUp(self):
super().setUp()
self.cat1 = Category.objects.create(name='cat1')
self.cat1.save()
self.prop1 = Property.objects.create(name='prop1')
self.prop1.save()
self.prop2 = Property.objects.create(name='prop2', description='prop2 description', category=self.cat1)
self.prop2.save()
self.prop3 = Property.objects.create(name='prop3', description='prop3 description', category=self.cat1)
self.prop3.save()
def test_properties(self):
self.assertEqual(len(Property.objects.all()), 3)
self.assertEqual(self.prop1.name, 'prop1')
self.assertEqual(self.prop1.description, None)
self.assertEqual(self.prop1.category, None)
self.assertEqual(self.prop2.name, 'prop2')
self.assertEqual(self.prop2.description, 'prop2 description')
self.assertEqual(self.prop2.category, self.cat1)
self.assertEqual(self.prop3.name, 'prop3')
self.assertEqual(self.prop3.description, 'prop3 description')
self.assertEqual(self.prop3.category, self.cat1)
self.assertEqual(str(self.prop1), 'prop1')
self.assertEqual(str(self.prop2), 'prop2')
self.assertEqual(str(self.prop3), 'prop3')
self.assertEqual(self.cat1.properties.count(), 2)
self.assertEqual(self.cat1.properties.first(), self.prop2)
self.assertEqual(self.cat1.properties.last(), self.prop3)
class PropertyApiTestCase(UserTestCase):
def setUp(self):
super().setUp()
self.cat1 = Category.objects.create(name='cat1')
self.cat1.save()
self.cat2 = Category.objects.create(name='cat2')
self.cat2.save()
self.cat3 = Category.objects.create(name='cat3')
self.cat3.save()
def test_get_properties(self):
reply = client.get('/api/properties/', self.local_user1)
self.assertEqual(reply.status_code, 200)
self.assertEqual(len(reply.json()), 0)
prop1 = Property.objects.create(name='prop1')
prop1.save()

View file

@ -0,0 +1,58 @@
from authentication.tests import UserTestCase, SignatureAuthClient
from toolshed.models import Tag, Category
client = SignatureAuthClient()
class TagTestCase(UserTestCase):
def setUp(self):
super().setUp()
self.cat1 = Category.objects.create(name='cat1')
self.cat1.save()
self.tag1 = Tag.objects.create(name='tag1', description='tag1 description', category=self.cat1)
self.tag1.save()
self.tag2 = Tag.objects.create(name='tag2', description='tag2 description', category=self.cat1)
self.tag2.save()
self.tag3 = Tag.objects.create(name='tag3')
self.tag3.save()
def test_tags(self):
self.assertEqual(len(Tag.objects.all()), 3)
self.assertEqual(self.tag1.name, 'tag1')
self.assertEqual(self.tag1.description, 'tag1 description')
self.assertEqual(self.tag1.category, self.cat1)
self.assertEqual(self.tag2.name, 'tag2')
self.assertEqual(self.tag2.description, 'tag2 description')
self.assertEqual(self.tag2.category, self.cat1)
self.assertEqual(self.tag3.name, 'tag3')
self.assertEqual(self.tag3.description, None)
self.assertEqual(self.tag3.category, None)
self.assertEqual(str(self.tag1), 'tag1')
self.assertEqual(str(self.tag2), 'tag2')
self.assertEqual(str(self.tag3), 'tag3')
self.assertEqual(self.cat1.tags.count(), 2)
self.assertEqual(self.cat1.tags.first(), self.tag1)
self.assertEqual(self.cat1.tags.last(), self.tag2)
class TagApiTestCase(UserTestCase):
def setUp(self):
super().setUp()
self.cat1 = Category.objects.create(name='cat1')
self.cat1.save()
self.tag1 = Tag.objects.create(name='tag1', description='tag1 description', category=self.cat1)
self.tag1.save()
self.tag2 = Tag.objects.create(name='tag2', description='tag2 description', category=self.cat1)
self.tag2.save()
self.tag3 = Tag.objects.create(name='tag3')
self.tag3.save()
def test_get_tags(self):
reply = client.get('/api/tags/', self.local_user1)
self.assertEqual(reply.status_code, 200)
self.assertEqual(len(reply.json()), 3)
self.assertEqual(reply.json()[0], 'tag1')
self.assertEqual(reply.json()[1], 'tag2')
self.assertEqual(reply.json()[2], 'tag3')