diff --git a/.build.yml b/.build.yml index 0061d73..a97f4ca 100644 --- a/.build.yml +++ b/.build.yml @@ -13,5 +13,4 @@ steps: - apk add --no-cache gcc musl-dev python3-dev - pip install --upgrade pip && pip install -r requirements.txt - python3 configure.py - - coverage run manage.py test - - coverage report + - coverage run manage.py test && coverage report diff --git a/.gitignore b/.gitignore index 2e85c98..b338100 100644 --- a/.gitignore +++ b/.gitignore @@ -130,4 +130,5 @@ dmypy.json staticfiles/ userfiles/ -testdata.py \ No newline at end of file +backend/templates/ +backend/testdata.py \ No newline at end of file diff --git a/README.md b/README.md index 854b54b..a2c0135 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ ``` bash git clone https://github.com/gr4yj3d1/toolshed.git ``` + or + ``` bash git clone https://git.neulandlabor.de/j3d1/toolshed.git ``` @@ -20,7 +22,9 @@ pip install -r requirements.txt python configure.py 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 @@ -37,8 +41,6 @@ cd toolshed/docs mkdocs serve ``` - - ## CLI Client ### Requirements diff --git a/backend/authentication/models.py b/backend/authentication/models.py index 5ef3890..8b01aa0 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -22,7 +22,8 @@ class KnownIdentity(models.Model): def __str__(self): return f"{self.username}@{self.domain}" - def is_authenticated(self): + @staticmethod + def is_authenticated(): return True def friends_or_self(self): @@ -30,9 +31,9 @@ class KnownIdentity(models.Model): public_identity=self) 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') - if type(message) != str: + if not isinstance(message, str): raise TypeError('Message must be a string') try: 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): def create_user(self, username, email, password, **extra_fields): domain = extra_fields.pop('domain', 'localhost') - private_key_hex = extra_fields.pop('private_key', None) - if private_key_hex and type(private_key_hex) != str: - 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: - 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): - 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_hex: str | None = extra_fields.pop('private_key', None) + 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') + if len(private_key_hex) != 64: + raise ValueError('Private key must be 64 characters long or no private key must be provided') + 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') + private_key = SigningKey(bytes.fromhex(private_key_hex)) + else: + private_key = SigningKey.generate() public_key = SigningKey(private_key.encode()).verify_key extra_fields['private_key'] = private_key.encode(encoder=HexEncoder).decode('utf-8') try: diff --git a/backend/authentication/tests/test_auth.py b/backend/authentication/tests/test_auth.py index 0a3caf1..480be20 100644 --- a/backend/authentication/tests/test_auth.py +++ b/backend/authentication/tests/test_auth.py @@ -1,5 +1,6 @@ import json +from django.core.exceptions import ValidationError from django.test import Client, RequestFactory from nacl.encoding import HexEncoder from nacl.signing import SigningKey diff --git a/backend/backend/urls.py b/backend/backend/urls.py index ab9c3a2..0a27070 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -13,11 +13,14 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 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.urls import path, include from drf_yasg import openapi from drf_yasg.views import get_schema_view +from backend import settings + openapi_info = openapi.Info( title="Toolshed API", default_version='v1', @@ -35,9 +38,10 @@ urlpatterns = [ path('auth/', include('authentication.api')), path('admin/', include('hostadmin.api')), path('api/', include('toolshed.api.friend')), + path('api/', include('toolshed.api.social')), path('api/', include('toolshed.api.inventory')), path('api/', include('toolshed.api.info')), path('api/', include('toolshed.api.files')), path('media/', include('files.media_urls')), path('docs/', schema_view.with_ui('swagger', cache_timeout=0), name='api-docs'), -] +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/backend/configure.py b/backend/configure.py index 67b87b5..4229da2 100755 --- a/backend/configure.py +++ b/backend/configure.py @@ -74,10 +74,18 @@ def configure(): from django.core.management import call_command 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?"): from django.core.management import call_command call_command('createsuperuser') + # TODO ask for which static directory to use and save it in .env call_command('collectstatic', '--no-input') if yesno("Do you want to import all categories, properties and tags contained in this repository?", default=True): diff --git a/backend/shared_data/base.json b/backend/shared_data/base.json new file mode 100644 index 0000000..d961bb8 --- /dev/null +++ b/backend/shared_data/base.json @@ -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"} + ] +} \ No newline at end of file diff --git a/backend/shared_data/screws.json b/backend/shared_data/screws.json new file mode 100644 index 0000000..20c8b93 --- /dev/null +++ b/backend/shared_data/screws.json @@ -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"} + ] +} \ No newline at end of file diff --git a/backend/toolshed/admin.py b/backend/toolshed/admin.py index 4b3174e..40aac35 100644 --- a/backend/toolshed/admin.py +++ b/backend/toolshed/admin.py @@ -1,6 +1,14 @@ 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): @@ -25,3 +33,26 @@ class TagAdmin(admin.ModelAdmin): 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) diff --git a/backend/toolshed/aggregators.py b/backend/toolshed/aggregators.py new file mode 100644 index 0000000..4b20c53 --- /dev/null +++ b/backend/toolshed/aggregators.py @@ -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) diff --git a/backend/toolshed/api/info.py b/backend/toolshed/api/info.py index 3c54412..7395c03 100644 --- a/backend/toolshed/api/info.py +++ b/backend/toolshed/api/info.py @@ -61,9 +61,11 @@ def combined_info(request, format=None): # /info/ tags = [tag.name for tag in Tag.objects.all()] properties = PropertySerializer(Property.objects.all(), many=True).data 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)] - 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 = [ diff --git a/backend/toolshed/api/inventory.py b/backend/toolshed/api/inventory.py index c39a5c8..d500c45 100644 --- a/backend/toolshed/api/inventory.py +++ b/backend/toolshed/api/inventory.py @@ -1,12 +1,13 @@ from django.db import transaction 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.permissions import IsAuthenticated from rest_framework.response import Response from authentication.models import ToolshedUser, KnownIdentity from authentication.signature_auth import SignatureAuthentication +from files.models import File from toolshed.models import InventoryItem, StorageLocation from toolshed.serializers import InventoryItemSerializer, StorageLocationSerializer diff --git a/backend/toolshed/api/social.py b/backend/toolshed/api/social.py new file mode 100644 index 0000000..74bd899 --- /dev/null +++ b/backend/toolshed/api/social.py @@ -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), +] diff --git a/backend/toolshed/migrations/0006_event_transaction_profile_message.py b/backend/toolshed/migrations/0006_event_transaction_profile_message.py new file mode 100644 index 0000000..d0b108b --- /dev/null +++ b/backend/toolshed/migrations/0006_event_transaction_profile_message.py @@ -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)), + ], + ), + ] diff --git a/backend/toolshed/models.py b/backend/toolshed/models.py index de32826..30d7aca 100644 --- a/backend/toolshed/models.py +++ b/backend/toolshed/models.py @@ -1,4 +1,6 @@ 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_softdelete.models import SoftDeleteModel from rest_framework.exceptions import ValidationError @@ -7,6 +9,52 @@ from authentication.models import ToolshedUser, KnownIdentity 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): name = models.CharField(max_length=255, unique=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_plural = models.CharField(max_length=255, null=True, blank=True) base2_prefix = models.BooleanField(default=False) + # sort_lexicographically = models.BooleanField(default=False) dimensions = models.IntegerField(null=False, blank=False, default=1, validators=[MinValueValidator(1)]) 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) +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): name = models.CharField(max_length=255) description = models.TextField(null=True, blank=True) diff --git a/backend/toolshed/serializers.py b/backend/toolshed/serializers.py index 73edeff..765bba7 100644 --- a/backend/toolshed/serializers.py +++ b/backend/toolshed/serializers.py @@ -1,9 +1,10 @@ from rest_framework import serializers + from authentication.models import KnownIdentity, ToolshedUser, FriendRequestIncoming from authentication.serializers import OwnerSerializer from files.models import File 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): @@ -28,6 +29,18 @@ class FriendRequestSerializer(serializers.ModelSerializer): 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): category = serializers.SlugRelatedField(queryset=Category.objects.all(), slug_field='name') @@ -103,6 +116,8 @@ class InventoryItemSerializer(serializers.ModelSerializer): tags = validated_data.pop('tags', []) props = validated_data.pop('itemproperty_set', []) files = validated_data.pop('files', []) + # if 'category' in validated_data and validated_data['category'] == '': + # validated_data.pop('category') item = InventoryItem.objects.create(**validated_data) for tag in tags: item.tags.add(tag, through_defaults={}) @@ -127,6 +142,8 @@ class InventoryItemSerializer(serializers.ModelSerializer): def update(self, instance, validated_data): tags = validated_data.pop('tags', []) 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.tags.clear() item.properties.clear() diff --git a/backend/toolshed/tests/test_api.py b/backend/toolshed/tests/test_api.py index c926dec..716b670 100644 --- a/backend/toolshed/tests/test_api.py +++ b/backend/toolshed/tests/test_api.py @@ -53,7 +53,8 @@ class CombinedApiTestCase(UserTestMixin, CategoryTestMixin, TagTestMixin, Proper def test_combined_api(self): response = client.get('/api/info/', self.f['local_user1']) 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'], ['cat1', 'cat2', 'cat3', 'cat1/subcat1', 'cat1/subcat2', 'cat1/subcat1/subcat3']) self.assertEqual(response.json()['tags'], ['tag1', 'tag2', 'tag3']) diff --git a/backend/toolshed/tests/test_friend.py b/backend/toolshed/tests/test_friend.py index 31132a3..50751bf 100644 --- a/backend/toolshed/tests/test_friend.py +++ b/backend/toolshed/tests/test_friend.py @@ -369,13 +369,29 @@ class FriendRequestOutgoingTestCase(UserTestMixin, ToolshedTestCase): self.assertEqual(befriendee.friends.first().username, befriender.username) 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): def setUp(self): super().setUp() self.prepare_users() - print(self.f) def test_friend_request_combined(self): befriender = self.f['local_user1'] diff --git a/backend/toolshed/tests/test_social.py b/backend/toolshed/tests/test_social.py new file mode 100644 index 0000000..067185c --- /dev/null +++ b/backend/toolshed/tests/test_social.py @@ -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(), [])