From bd59c40ac60af84b528366e9bc3752a6c76faaaa Mon Sep 17 00:00:00 2001
From: jedi <git@m.j3d1.de>
Date: Fri, 7 Jul 2023 18:45:05 +0200
Subject: [PATCH] stash

---
 .build.yml                                    |  3 +-
 .gitignore                                    |  3 +-
 README.md                                     |  8 ++-
 backend/authentication/models.py              | 26 ++++---
 backend/authentication/tests/test_auth.py     |  1 +
 backend/backend/urls.py                       |  6 +-
 backend/configure.py                          |  8 +++
 backend/shared_data/base.json                 | 59 ++++++++++++++++
 backend/shared_data/screws.json               | 30 +++++++++
 backend/toolshed/admin.py                     | 33 ++++++++-
 backend/toolshed/aggregators.py               | 16 +++++
 backend/toolshed/api/info.py                  |  6 +-
 backend/toolshed/api/inventory.py             |  3 +-
 backend/toolshed/api/social.py                | 40 +++++++++++
 .../0006_event_transaction_profile_message.py | 67 +++++++++++++++++++
 backend/toolshed/models.py                    | 66 ++++++++++++++++++
 backend/toolshed/serializers.py               | 19 +++++-
 backend/toolshed/tests/test_api.py            |  3 +-
 backend/toolshed/tests/test_friend.py         | 18 ++++-
 backend/toolshed/tests/test_social.py         | 41 ++++++++++++
 20 files changed, 431 insertions(+), 25 deletions(-)
 create mode 100644 backend/shared_data/base.json
 create mode 100644 backend/shared_data/screws.json
 create mode 100644 backend/toolshed/aggregators.py
 create mode 100644 backend/toolshed/api/social.py
 create mode 100644 backend/toolshed/migrations/0006_event_transaction_profile_message.py
 create mode 100644 backend/toolshed/tests/test_social.py

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(), [])