stash
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
j3d1 2023-07-07 18:45:05 +02:00
parent b3bae6f5ad
commit bd59c40ac6
20 changed files with 431 additions and 25 deletions

View file

@ -13,5 +13,4 @@ steps:
- apk add --no-cache gcc musl-dev python3-dev - apk add --no-cache gcc musl-dev python3-dev
- pip install --upgrade pip && pip install -r requirements.txt - pip install --upgrade pip && pip install -r requirements.txt
- python3 configure.py - python3 configure.py
- coverage run manage.py test - coverage run manage.py test && coverage report
- coverage report

3
.gitignore vendored
View file

@ -130,4 +130,5 @@ dmypy.json
staticfiles/ staticfiles/
userfiles/ userfiles/
testdata.py backend/templates/
backend/testdata.py

View file

@ -5,7 +5,9 @@
``` bash ``` bash
git clone https://github.com/gr4yj3d1/toolshed.git git clone https://github.com/gr4yj3d1/toolshed.git
``` ```
or or
``` bash ``` bash
git clone https://git.neulandlabor.de/j3d1/toolshed.git git clone https://git.neulandlabor.de/j3d1/toolshed.git
``` ```
@ -20,7 +22,9 @@ pip install -r requirements.txt
python configure.py python configure.py
python manage.py runserver 0.0.0.0:8000 --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.
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.
### Frontend ### Frontend
@ -37,8 +41,6 @@ cd toolshed/docs
mkdocs serve mkdocs serve
``` ```
## CLI Client ## CLI Client
### Requirements ### Requirements

View file

@ -22,7 +22,8 @@ class KnownIdentity(models.Model):
def __str__(self): def __str__(self):
return f"{self.username}@{self.domain}" return f"{self.username}@{self.domain}"
def is_authenticated(self): @staticmethod
def is_authenticated():
return True return True
def friends_or_self(self): def friends_or_self(self):
@ -30,9 +31,9 @@ class KnownIdentity(models.Model):
public_identity=self) public_identity=self)
def verify(self, message, signature): def verify(self, message, signature):
if len(signature) != 128 or type(signature) != str: if len(signature) != 128 or not isinstance(signature, str):
raise TypeError('Signature must be 128 characters long and a string') raise TypeError('Signature must be 128 characters long and a string')
if type(message) != str: if not isinstance(message, str):
raise TypeError('Message must be a string') raise TypeError('Message must be a string')
try: try:
VerifyKey(bytes.fromhex(self.public_key)).verify(message.encode('utf-8'), bytes.fromhex(signature)) VerifyKey(bytes.fromhex(self.public_key)).verify(message.encode('utf-8'), bytes.fromhex(signature))
@ -44,14 +45,17 @@ class KnownIdentity(models.Model):
class ToolshedUserManager(auth.models.BaseUserManager): class ToolshedUserManager(auth.models.BaseUserManager):
def create_user(self, username, email, password, **extra_fields): def create_user(self, username, email, password, **extra_fields):
domain = extra_fields.pop('domain', 'localhost') domain = extra_fields.pop('domain', 'localhost')
private_key_hex = extra_fields.pop('private_key', None) private_key_hex: str | None = extra_fields.pop('private_key', None)
if private_key_hex and type(private_key_hex) != str: if private_key_hex is not None:
if not isinstance(private_key_hex, str):
raise TypeError('Private key must be a string or no private key must be provided') raise TypeError('Private key must be a string or no private key must be provided')
if private_key_hex and len(private_key_hex) != 64: if len(private_key_hex) != 64:
raise ValueError('Private key must be 64 characters long or no private key must be provided') raise ValueError('Private key must be 64 characters long or no private key must be provided')
if private_key_hex and not all(c in '0123456789abcdef' for c in private_key_hex): if not all(c in '0123456789abcdef' for c in private_key_hex):
raise ValueError('Private key must be a hexadecimal string or no private key must be provided') raise ValueError('Private key must be a hexadecimal string or no private key must be provided')
private_key = SigningKey(bytes.fromhex(private_key_hex)) if private_key_hex else SigningKey.generate() private_key = SigningKey(bytes.fromhex(private_key_hex))
else:
private_key = SigningKey.generate()
public_key = SigningKey(private_key.encode()).verify_key public_key = SigningKey(private_key.encode()).verify_key
extra_fields['private_key'] = private_key.encode(encoder=HexEncoder).decode('utf-8') extra_fields['private_key'] = private_key.encode(encoder=HexEncoder).decode('utf-8')
try: try:

View file

@ -1,5 +1,6 @@
import json import json
from django.core.exceptions import ValidationError
from django.test import Client, RequestFactory from django.test import Client, RequestFactory
from nacl.encoding import HexEncoder from nacl.encoding import HexEncoder
from nacl.signing import SigningKey from nacl.signing import SigningKey

View file

@ -13,11 +13,14 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from drf_yasg import openapi from drf_yasg import openapi
from drf_yasg.views import get_schema_view from drf_yasg.views import get_schema_view
from backend import settings
openapi_info = openapi.Info( openapi_info = openapi.Info(
title="Toolshed API", title="Toolshed API",
default_version='v1', default_version='v1',
@ -35,9 +38,10 @@ urlpatterns = [
path('auth/', include('authentication.api')), path('auth/', include('authentication.api')),
path('admin/', include('hostadmin.api')), path('admin/', include('hostadmin.api')),
path('api/', include('toolshed.api.friend')), path('api/', include('toolshed.api.friend')),
path('api/', include('toolshed.api.social')),
path('api/', include('toolshed.api.inventory')), path('api/', include('toolshed.api.inventory')),
path('api/', include('toolshed.api.info')), path('api/', include('toolshed.api.info')),
path('api/', include('toolshed.api.files')), path('api/', include('toolshed.api.files')),
path('media/', include('files.media_urls')), path('media/', include('files.media_urls')),
path('docs/', schema_view.with_ui('swagger', cache_timeout=0), name='api-docs'), path('docs/', schema_view.with_ui('swagger', cache_timeout=0), name='api-docs'),
] ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -74,10 +74,18 @@ def configure():
from django.core.management import call_command from django.core.management import call_command
call_command('migrate') call_command('migrate')
# if yesno("Do you want to create initial domains?"):
# domains = input("Enter a comma-separated list of allowed hosts: ")
# from hostadmin import Domain
#
# Domain.objects.
# TODO check if superuser exists
if yesno("Do you want to create a superuser?"): if yesno("Do you want to create a superuser?"):
from django.core.management import call_command from django.core.management import call_command
call_command('createsuperuser') call_command('createsuperuser')
# TODO ask for which static directory to use and save it in .env
call_command('collectstatic', '--no-input') call_command('collectstatic', '--no-input')
if yesno("Do you want to import all categories, properties and tags contained in this repository?", default=True): if yesno("Do you want to import all categories, properties and tags contained in this repository?", default=True):

View file

@ -0,0 +1,59 @@
{
"categories": [
{ "name": "hardware"},
{ "name": "material"},
{ "name": "tools"}
],
"properties": [
{ "name": "angle", "unit_symbol": "°", "unit_name": "degree", "unit_name_plural": "degrees" },
{ "name": "area", "unit_symbol": "m²", "unit_name": "square meter", "unit_name_plural": "square meters" },
{ "name": "current", "unit_symbol": "A", "unit_name": "ampere", "unit_name_plural": "amperes" },
{ "name": "diameter", "unit_symbol": "m", "unit_name": "meter", "unit_name_plural": "meters" },
{ "name": "energy", "unit_symbol": "J", "unit_name": "joule", "unit_name_plural": "joules" },
{ "name": "frequency", "unit_symbol": "Hz", "unit_name": "hertz", "unit_name_plural": "hertz" },
{ "name": "height", "unit_symbol": "m", "unit_name": "meter", "unit_name_plural": "meters" },
{ "name": "length", "unit_symbol": "m", "unit_name": "meter", "unit_name_plural": "meters" },
{ "name": "memory", "unit_symbol": "B", "unit_name": "byte", "unit_name_plural": "bytes", "base2_prefix": true },
{ "name": "power", "unit_symbol": "W", "unit_name": "watt", "unit_name_plural": "watts" },
{ "name": "price", "unit_symbol": "€", "unit_name": "euro", "unit_name_plural": "euros" },
{ "name": "speed", "unit_symbol": "m/s", "unit_name": "meter per second", "unit_name_plural": "meters per second" },
{ "name": "temperature", "unit_symbol": "°C", "unit_name": "degree Celsius", "unit_name_plural": "degrees Celsius" },
{ "name": "time", "unit_symbol": "s", "unit_name": "second", "unit_name_plural": "seconds" },
{ "name": "voltage", "unit_symbol": "V", "unit_name": "volt", "unit_name_plural": "volts" },
{ "name": "volume", "unit_symbol": "l", "unit_name": "liter", "unit_name_plural": "liters" },
{ "name": "weight", "unit_symbol": "g", "unit_name": "gram", "unit_name_plural": "grams" },
{ "name": "width", "unit_symbol": "m", "unit_name": "meter", "unit_name_plural": "meters" }
],
"tags": [
{"name": "bolt", "category": "hardware"},
{"name": "chisel", "category": "tools"},
{"name": "clamp", "category": "tools"},
{"name": "drill", "category": "tools"},
{"name": "ear plugs", "category": "tools"},
{"name": "extension cord", "category": "tools"},
{"name": "flashlight", "category": "tools"},
{"name": "gloves", "category": "tools"},
{"name": "goggles", "category": "tools"},
{"name": "hammer", "category": "tools"},
{"name": "level", "category": "tools"},
{"name": "mask", "category": "tools"},
{"name": "nail", "category": "hardware"},
{"name": "nut", "category": "hardware"},
{"name": "paint brush", "category": "tools"},
{"name": "paint roller", "category": "tools"},
{"name": "paint tray", "category": "tools"},
{"name": "pliers", "category": "tools"},
{"name": "power strip", "category": "tools"},
{"name": "sander", "category": "tools"},
{"name": "saw", "category": "tools"},
{"name": "screw", "category": "hardware"},
{"name": "screwdriver", "category": "tools"},
{"name": "soldering iron", "category": "tools"},
{"name": "stapler", "category": "tools"},
{"name": "tape measure", "category": "tools"},
{"name": "tool"},
{"name": "vise", "category": "tools"},
{"name": "washer", "category": "hardware"},
{"name": "wrench", "category": "tools"}
]
}

View file

@ -0,0 +1,30 @@
{
"depends": [ "git:base" ],
"categories": [
{ "name": "screws", "parent": "hardware"}
],
"tags": [
{"name": "m1", "category": "screws"},
{"name": "m2", "category": "screws"},
{"name": "m2.5", "category": "screws"},
{"name": "m3", "category": "screws"},
{"name": "m4", "category": "screws"},
{"name": "m5", "category": "screws"},
{"name": "m6", "category": "screws"},
{"name": "m8", "category": "screws"},
{"name": "m10", "category": "screws"},
{"name": "m12", "category": "screws"},
{"name": "m16", "category": "screws"},
{"name": "torx", "category": "screws"},
{"name": "hex", "category": "screws"},
{"name": "phillips", "category": "screws"},
{"name": "pozidriv", "category": "screws"},
{"name": "slotted", "category": "screws"},
{"name": "socket", "category": "screws"},
{"name": "flat", "category": "screws"},
{"name": "pan", "category": "screws"},
{"name": "button", "category": "screws"},
{"name": "countersunk", "category": "screws"},
{"name": "round", "category": "screws"}
]
}

View file

@ -1,6 +1,14 @@
from django.contrib import admin from django.contrib import admin
from toolshed.models import InventoryItem, Property, Tag, ItemProperty, ItemTag from toolshed.models import Profile, InventoryItem, Property, Tag, ItemProperty, ItemTag
class ProfileAdmin(admin.ModelAdmin):
list_display = ('user', 'bio', 'location')
search_fields = ('user', 'bio', 'location')
admin.site.register(Profile, ProfileAdmin)
class InventoryItemAdmin(admin.ModelAdmin): class InventoryItemAdmin(admin.ModelAdmin):
@ -25,3 +33,26 @@ class TagAdmin(admin.ModelAdmin):
admin.site.register(Tag, TagAdmin) admin.site.register(Tag, TagAdmin)
# class ItemPropertyAdmin(admin.ModelAdmin):
# list_display = ('item', 'property', 'value')
# search_fields = ('item', 'property', 'value')
#
#
# admin.site.register(ItemProperty, ItemPropertyAdmin)
#
#
# class ItemTagAdmin(admin.ModelAdmin):
# list_display = ('item', 'tag')
# search_fields = ('item', 'tag')
#
#
# admin.site.register(ItemTag, ItemTagAdmin)
# class LendingPeriodAdmin(admin.ModelAdmin):
# list_display = ('item', 'start_date', 'end_date')
# search_fields = ('item', 'start_date', 'end_date')
#
#
# admin.site.register(LendingPeriod, LendingPeriodAdmin)

View file

@ -0,0 +1,16 @@
from toolshed.models import Event, Message
def timeline_notifications(user):
"""Return a list of notifications that the user is interested in."""
for evt in Event.objects.all():
if evt.user == user:
yield evt
for tool in user.inventory.all():
if evt.tool == tool:
yield evt
def unread_messages(user):
"""Return a list of unread messages."""
return Message.objects.filter(recipient=user, read=False)

View file

@ -61,9 +61,11 @@ def combined_info(request, format=None): # /info/
tags = [tag.name for tag in Tag.objects.all()] tags = [tag.name for tag in Tag.objects.all()]
properties = PropertySerializer(Property.objects.all(), many=True).data properties = PropertySerializer(Property.objects.all(), many=True).data
categories = [str(category) for category in Category.objects.all()] categories = [str(category) for category in Category.objects.all()]
policies = ['private', 'friends', 'internal', 'public'] policies = InventoryItem.AVAILABILITY_POLICY_CHOICES
domains = [domain.name for domain in Domain.objects.filter(open_registration=True)] domains = [domain.name for domain in Domain.objects.filter(open_registration=True)]
return Response({'tags': tags, 'properties': properties, 'availability_policies': policies, 'categories': categories, 'domains': domains}) return Response(
{'tags': tags, 'properties': properties, 'availability_policies': policies, 'categories': categories,
'domains': domains})
urlpatterns = [ urlpatterns = [

View file

@ -1,12 +1,13 @@
from django.db import transaction from django.db import transaction
from django.urls import path from django.urls import path
from rest_framework import routers, viewsets from rest_framework import routers, viewsets, serializers
from rest_framework.decorators import authentication_classes, api_view, permission_classes from rest_framework.decorators import authentication_classes, api_view, permission_classes
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from authentication.models import ToolshedUser, KnownIdentity from authentication.models import ToolshedUser, KnownIdentity
from authentication.signature_auth import SignatureAuthentication from authentication.signature_auth import SignatureAuthentication
from files.models import File
from toolshed.models import InventoryItem, StorageLocation from toolshed.models import InventoryItem, StorageLocation
from toolshed.serializers import InventoryItemSerializer, StorageLocationSerializer from toolshed.serializers import InventoryItemSerializer, StorageLocationSerializer

View file

@ -0,0 +1,40 @@
from django.urls import path
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from authentication.signature_auth import SignatureAuthenticationLocal
from toolshed.models import Message, Profile
from toolshed.serializers import MessageSerializer, ProfileSerializer
from toolshed.aggregators import unread_messages, timeline_notifications
@api_view(['GET'])
@authentication_classes([SignatureAuthenticationLocal])
@permission_classes([IsAuthenticated])
def get_messages(request):
messages = Message.objects.filter(recipient=request.user)
return Response(MessageSerializer(messages, many=True).data)
@api_view(['GET'])
@authentication_classes([SignatureAuthenticationLocal])
@permission_classes([IsAuthenticated])
def get_profile(request):
profile = Profile.objects.get(user=request.user)
return Response(ProfileSerializer(profile).data)
@api_view(['GET'])
@authentication_classes([SignatureAuthenticationLocal])
@permission_classes([IsAuthenticated])
def get_notifications(request):
notifications = timeline_notifications(request.user)
return Response(notifications)
urlpatterns = [
path('messages/', get_messages),
path('profile/', get_profile),
path('notifications/', get_notifications),
]

View file

@ -0,0 +1,67 @@
# Generated by Django 4.2.2 on 2024-02-23 15:30
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('files', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('toolshed', '0005_alter_inventoryitem_availability_policy'),
]
operations = [
migrations.CreateModel(
name='Event',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField()),
('location', models.CharField(max_length=255)),
('date', models.DateField()),
('time', models.TimeField()),
('host_username', models.CharField(max_length=255)),
('host_domain', models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name='Transaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('pending', 'Pending'), ('accepted', 'Accepted'), ('rejected', 'Rejected')], default='pending', max_length=20)),
('message', models.TextField(blank=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('item_offered', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='offered_transactions', to='toolshed.inventoryitem')),
('item_requested', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requested_transactions', to='toolshed.inventoryitem')),
('offerer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='offered_transactions', to=settings.AUTH_USER_MODEL)),
('requester', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requested_transactions', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Profile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('bio', models.TextField(blank=True)),
('location', models.CharField(blank=True, max_length=255)),
('profile_picture', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='files.file')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Message',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('subject', models.CharField(max_length=255)),
('body', models.TextField()),
('read', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL)),
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
],
),
]

View file

@ -1,4 +1,6 @@
from django.db import models from django.db import models
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django_softdelete.models import SoftDeleteModel from django_softdelete.models import SoftDeleteModel
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
@ -7,6 +9,52 @@ from authentication.models import ToolshedUser, KnownIdentity
from files.models import File from files.models import File
# @receiver(pre_save)
# def pre_save_handler(sender, instance, *args, **kwargs):
# instance.full_clean()
class Event(models.Model):
name = models.CharField(max_length=255)
description = models.TextField()
location = models.CharField(max_length=255)
date = models.DateField()
time = models.TimeField()
# host = models.ForeignKey(User, on_delete=models.CASCADE, related_name='events')
host_username = models.CharField(max_length=255)
host_domain = models.CharField(max_length=255)
# def __str__(self):
# return self.name
class Message(models.Model):
sender = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='sent_messages')
recipient = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='received_messages')
subject = models.CharField(max_length=255)
body = models.TextField()
read = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Profile(models.Model):
user = models.OneToOneField(ToolshedUser, on_delete=models.CASCADE)
profile_picture = models.ForeignKey(File, on_delete=models.SET_NULL, null=True, blank=True)
bio = models.TextField(blank=True)
location = models.CharField(max_length=255, blank=True)
@receiver(post_save, sender=ToolshedUser)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=ToolshedUser)
def save_user_profile(sender, instance, **kwargs):
instance.profile.save()
class Category(SoftDeleteModel): class Category(SoftDeleteModel):
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
description = models.TextField(null=True, blank=True) description = models.TextField(null=True, blank=True)
@ -29,6 +77,7 @@ class Property(models.Model):
unit_name = models.CharField(max_length=255, 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) unit_name_plural = models.CharField(max_length=255, null=True, blank=True)
base2_prefix = models.BooleanField(default=False) base2_prefix = models.BooleanField(default=False)
# sort_lexicographically = models.BooleanField(default=False)
dimensions = models.IntegerField(null=False, blank=False, default=1, validators=[MinValueValidator(1)]) dimensions = models.IntegerField(null=False, blank=False, default=1, validators=[MinValueValidator(1)])
origin = models.CharField(max_length=255, null=False, blank=False) origin = models.CharField(max_length=255, null=False, blank=False)
@ -89,6 +138,23 @@ class ItemTag(models.Model):
inventory_item = models.ForeignKey(InventoryItem, on_delete=models.CASCADE) inventory_item = models.ForeignKey(InventoryItem, on_delete=models.CASCADE)
class Transaction(models.Model):
STATUS_CHOICES = (
('pending', 'Pending'),
('accepted', 'Accepted'),
('rejected', 'Rejected'),
)
item_requested = models.ForeignKey(InventoryItem, on_delete=models.CASCADE, related_name='requested_transactions')
item_offered = models.ForeignKey(InventoryItem, on_delete=models.CASCADE, related_name='offered_transactions')
requester = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='requested_transactions')
offerer = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='offered_transactions')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
message = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class StorageLocation(models.Model): class StorageLocation(models.Model):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True) description = models.TextField(null=True, blank=True)

View file

@ -1,9 +1,10 @@
from rest_framework import serializers from rest_framework import serializers
from authentication.models import KnownIdentity, ToolshedUser, FriendRequestIncoming from authentication.models import KnownIdentity, ToolshedUser, FriendRequestIncoming
from authentication.serializers import OwnerSerializer from authentication.serializers import OwnerSerializer
from files.models import File from files.models import File
from files.serializers import FileSerializer from files.serializers import FileSerializer
from toolshed.models import Category, Property, ItemProperty, InventoryItem, Tag, StorageLocation from toolshed.models import Category, Property, ItemProperty, InventoryItem, Tag, Profile, Message, StorageLocation
class FriendSerializer(serializers.ModelSerializer): class FriendSerializer(serializers.ModelSerializer):
@ -28,6 +29,18 @@ class FriendRequestSerializer(serializers.ModelSerializer):
return obj.befriender_username + '@' + obj.befriender_domain return obj.befriender_username + '@' + obj.befriender_domain
class ProfileSerializer(serializers.Serializer):
class Meta:
model = Profile
fields = '__all__'
class MessageSerializer(serializers.Serializer):
class Meta:
model = Message
fields = '__all__'
class PropertySerializer(serializers.ModelSerializer): class PropertySerializer(serializers.ModelSerializer):
category = serializers.SlugRelatedField(queryset=Category.objects.all(), slug_field='name') category = serializers.SlugRelatedField(queryset=Category.objects.all(), slug_field='name')
@ -103,6 +116,8 @@ class InventoryItemSerializer(serializers.ModelSerializer):
tags = validated_data.pop('tags', []) tags = validated_data.pop('tags', [])
props = validated_data.pop('itemproperty_set', []) props = validated_data.pop('itemproperty_set', [])
files = validated_data.pop('files', []) files = validated_data.pop('files', [])
# if 'category' in validated_data and validated_data['category'] == '':
# validated_data.pop('category')
item = InventoryItem.objects.create(**validated_data) item = InventoryItem.objects.create(**validated_data)
for tag in tags: for tag in tags:
item.tags.add(tag, through_defaults={}) item.tags.add(tag, through_defaults={})
@ -127,6 +142,8 @@ class InventoryItemSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
tags = validated_data.pop('tags', []) tags = validated_data.pop('tags', [])
props = validated_data.pop('itemproperty_set', []) props = validated_data.pop('itemproperty_set', [])
# if 'category' in validated_data and validated_data['category'] == '':
# validated_data.pop('category')
item = super().update(instance, validated_data) item = super().update(instance, validated_data)
item.tags.clear() item.tags.clear()
item.properties.clear() item.properties.clear()

View file

@ -53,7 +53,8 @@ class CombinedApiTestCase(UserTestMixin, CategoryTestMixin, TagTestMixin, Proper
def test_combined_api(self): def test_combined_api(self):
response = client.get('/api/info/', self.f['local_user1']) response = client.get('/api/info/', self.f['local_user1'])
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['availability_policies'], ['private', 'friends', 'internal', 'public']) self.assertEqual(response.json()['availability_policies'], [['sell', 'Sell'], ['rent', 'Rent'], ['lend', 'Lend'],
['share', 'Share'], ['private', 'Private']])
self.assertEqual(response.json()['categories'], self.assertEqual(response.json()['categories'],
['cat1', 'cat2', 'cat3', 'cat1/subcat1', 'cat1/subcat2', 'cat1/subcat1/subcat3']) ['cat1', 'cat2', 'cat3', 'cat1/subcat1', 'cat1/subcat2', 'cat1/subcat1/subcat3'])
self.assertEqual(response.json()['tags'], ['tag1', 'tag2', 'tag3']) self.assertEqual(response.json()['tags'], ['tag1', 'tag2', 'tag3'])

View file

@ -369,13 +369,29 @@ class FriendRequestOutgoingTestCase(UserTestMixin, ToolshedTestCase):
self.assertEqual(befriendee.friends.first().username, befriender.username) self.assertEqual(befriendee.friends.first().username, befriender.username)
self.assertEqual(befriendee.friends.first().domain, befriender.domain) self.assertEqual(befriendee.friends.first().domain, befriender.domain)
# TODO:
# - test that the friend request is deleted after a certain amount of time
# - decline friend request Endpoint ('reject'?)
# - cancel friend request Endpoint ('retract'?)
# - drop friend Endpoint
# Szenarios: (all also with broken signature, wrong key, wrong secret, wrong author and without authorisation header)
# (plus: friend request exists already, already friends)
# - local1 requests local2, local2 accepts
# - local1 requests local2, local2 declines
# - local1 requests local2, local1 cancels
# - ext requests local, local accepts
# - ext requests local, local declines
# - ext requests local, ext cancels
# - local requests ext, ext accepts
# - local requests ext, ext declines
# - local requests ext, local cancels
class FriendRequestCombinedTestCase(UserTestMixin, ToolshedTestCase): class FriendRequestCombinedTestCase(UserTestMixin, ToolshedTestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.prepare_users() self.prepare_users()
print(self.f)
def test_friend_request_combined(self): def test_friend_request_combined(self):
befriender = self.f['local_user1'] befriender = self.f['local_user1']

View file

@ -0,0 +1,41 @@
from django.test import Client
from authentication.tests import SignatureAuthClient, UserTestMixin, ToolshedTestCase
anonymous_client = Client()
client = SignatureAuthClient()
class MessageApiTestCase(UserTestMixin, ToolshedTestCase):
def setUp(self):
super().setUp()
self.prepare_users()
def test_get_messages(self):
response = client.get('/api/messages/', self.f['local_user1'])
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
class ProfileApiTestCase(UserTestMixin, ToolshedTestCase):
def setUp(self):
super().setUp()
self.prepare_users()
def test_get_profile(self):
response = client.get('/api/profile/', self.f['local_user1'])
self.assertEqual(response.status_code, 200)
class NotificationApiTestCase(UserTestMixin, ToolshedTestCase):
def setUp(self):
super().setUp()
self.prepare_users()
def test_get_notifications(self):
response = client.get('/api/notifications/', self.f['local_user1'])
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])