Compare commits
51 commits
stable
...
busti/prot
Author | SHA1 | Date | |
---|---|---|---|
|
64caa7e547 | ||
|
25ee3dd4f2 | ||
|
32f91f7ee3 | ||
dd5c8e3d57 | |||
ec139531f2 | |||
72b8bbab76 | |||
c8fdfbb09a | |||
b5d079c9d9 | |||
79c5645150 | |||
e685f50a88 | |||
01286aede8 | |||
f3b8a3247e | |||
f7c9d1d069 | |||
e2cf04cf18 | |||
08f723e9da | |||
4140214c81 | |||
cc3fec7347 | |||
4453f33ad9 | |||
23028f0027 | |||
49a063f2a7 | |||
5c073c3049 | |||
d288c70e6a | |||
58ad51fc5c | |||
a1fd49c5d1 | |||
7e8f984ee2 | |||
02e41214d2 | |||
ad9109a8e0 | |||
ec5ad720b9 | |||
b98c368540 | |||
06f7c515ef | |||
21cb4018a3 | |||
51d7788324 | |||
dfbe4d5dd4 | |||
c14762e20b | |||
b4ea1d8df2 | |||
c414b24786 | |||
905452cdf3 | |||
297ad7bd99 | |||
9381d229c5 | |||
bbbf15cfb3 | |||
d21f4a331b | |||
49bc03aa4c | |||
86003a8582 | |||
a83083d0eb | |||
c02b1588f2 | |||
645e20bcf0 | |||
bbbdee6aa8 | |||
c8c53a3800 | |||
75cf335744 | |||
3fab242eb8 | |||
20a93639ff |
|
@ -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 --parallel-mode --concurrency=multiprocessing manage.py test --parallel=$(nproc) && coverage report
|
||||
|
|
4
.gitignore
vendored
|
@ -130,5 +130,5 @@ dmypy.json
|
|||
|
||||
staticfiles/
|
||||
userfiles/
|
||||
testdata.py
|
||||
*.sqlite3
|
||||
backend/templates/
|
||||
backend/testdata.py
|
43
README.md
|
@ -1,6 +1,6 @@
|
|||
# toolshed
|
||||
|
||||
## Development
|
||||
## Installation / Development
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/gr4yj3d1/toolshed.git
|
||||
|
@ -12,10 +12,7 @@ or
|
|||
git clone https://git.neulandlabor.de/j3d1/toolshed.git
|
||||
```
|
||||
|
||||
all following development mode commands support auto-reloading and hot-reloading where applicable, they do not need to bw
|
||||
restarted after changes.
|
||||
|
||||
### Backend only
|
||||
### Backend
|
||||
|
||||
``` bash
|
||||
cd toolshed/backend
|
||||
|
@ -29,7 +26,7 @@ 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.
|
||||
|
||||
### Frontend only
|
||||
### Frontend
|
||||
|
||||
``` bash
|
||||
cd toolshed/frontend
|
||||
|
@ -37,45 +34,13 @@ npm install
|
|||
npm run dev
|
||||
```
|
||||
|
||||
### Docs only
|
||||
### Docs
|
||||
|
||||
``` bash
|
||||
cd toolshed/docs
|
||||
mkdocs serve
|
||||
```
|
||||
|
||||
### Full stack
|
||||
|
||||
``` bash
|
||||
cd toolshed
|
||||
docker-compose -f deploy/docker-compose.override.yml up --build
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Requirements
|
||||
|
||||
- python3
|
||||
- python3-pip
|
||||
- python3-venv
|
||||
- wget
|
||||
- unzip
|
||||
- nginx
|
||||
- uwsgi
|
||||
|
||||
### Installation
|
||||
|
||||
* Get the latest release from
|
||||
`https://git.neulandlabor.de/j3d1/toolshed/releases/download/<version>/toolshed.zip` or
|
||||
`https://github.com/gr4yj3d1/toolshed/archive/refs/tags/<version>.zip`.
|
||||
* Unpack it to `/var/www` or wherever you want to install toolshed.
|
||||
* Create a virtual environment and install the requirements.
|
||||
* Then run the configuration script.
|
||||
* Configure your webserver to serve the static files and proxy the requests to the backend.
|
||||
* Configure your webserver to run the backend with uwsgi.
|
||||
|
||||
for detailed instructions see [docs](/docs/deployment.md).
|
||||
|
||||
## CLI Client
|
||||
|
||||
### Requirements
|
||||
|
|
2
backend/.idea/.gitignore
vendored
|
@ -6,5 +6,3 @@
|
|||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# GitHub Copilot persisted chat sessions
|
||||
/copilot/chatSessions
|
||||
|
|
|
@ -60,8 +60,6 @@ def verify_incoming_friend_request(request, raw_request_body):
|
|||
befriender_key = request.data['befriender_key']
|
||||
except KeyError:
|
||||
return False
|
||||
if not befriender or not befriender_key:
|
||||
return False
|
||||
if username + "@" + domain != befriender:
|
||||
return False
|
||||
if len(befriender_key) != 64:
|
||||
|
|
|
@ -8,41 +8,32 @@ import dotenv
|
|||
from django.db import transaction, IntegrityError
|
||||
|
||||
|
||||
class CmdCtx:
|
||||
def yesno(prompt, default=False):
|
||||
if not sys.stdin.isatty():
|
||||
return default
|
||||
yes = {'yes', 'y', 'ye'}
|
||||
no = {'no', 'n'}
|
||||
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
if default:
|
||||
yes.add('')
|
||||
else:
|
||||
no.add('')
|
||||
|
||||
def yesno(self, prompt, default=False):
|
||||
if not sys.stdin.isatty() or self.args.noninteractive:
|
||||
return default
|
||||
elif self.args.yes:
|
||||
hint = ' [Y/n] ' if default else ' [y/N] '
|
||||
|
||||
while True:
|
||||
choice = input(prompt + hint).lower()
|
||||
if choice in yes:
|
||||
return True
|
||||
elif self.args.no:
|
||||
elif choice in no:
|
||||
return False
|
||||
yes = {'yes', 'y', 'ye'}
|
||||
no = {'no', 'n'}
|
||||
|
||||
if default:
|
||||
yes.add('')
|
||||
else:
|
||||
no.add('')
|
||||
|
||||
hint = ' [Y/n] ' if default else ' [y/N] '
|
||||
|
||||
while True:
|
||||
choice = input(prompt + hint).lower()
|
||||
if choice in yes:
|
||||
return True
|
||||
elif choice in no:
|
||||
return False
|
||||
else:
|
||||
print('Please respond with "yes" or "no"')
|
||||
print('Please respond with "yes" or "no"')
|
||||
|
||||
|
||||
def configure(ctx):
|
||||
def configure():
|
||||
if not os.path.exists('.env'):
|
||||
if not ctx.yesno("the .env file does not exist, do you want to create it?", default=True):
|
||||
if not yesno("the .env file does not exist, do you want to create it?", default=True):
|
||||
print('Aborting')
|
||||
exit(0)
|
||||
if not os.path.exists('.env.dist'):
|
||||
|
@ -65,7 +56,7 @@ def configure(ctx):
|
|||
current_hosts = os.getenv('ALLOWED_HOSTS')
|
||||
print('Current ALLOWED_HOSTS: {}'.format(current_hosts))
|
||||
|
||||
if ctx.yesno("Do you want to add ALLOWED_HOSTS?"):
|
||||
if yesno("Do you want to add ALLOWED_HOSTS?"):
|
||||
hosts = input("Enter a comma-separated list of allowed hosts: ")
|
||||
joined_hosts = current_hosts + ',' + hosts if current_hosts else hosts
|
||||
dotenv.set_key('.env', 'ALLOWED_HOSTS', joined_hosts)
|
||||
|
@ -76,29 +67,26 @@ def configure(ctx):
|
|||
django.setup()
|
||||
|
||||
if not os.path.exists('db.sqlite3'):
|
||||
if not ctx.yesno("No database found, do you want to create one?", default=True):
|
||||
if not yesno("No database found, do you want to create one?", default=True):
|
||||
print('Aborting')
|
||||
exit(0)
|
||||
|
||||
from django.core.management import call_command
|
||||
call_command('migrate')
|
||||
|
||||
if ctx.yesno("Do you want to create a superuser?"):
|
||||
if yesno("Do you want to create a superuser?"):
|
||||
from django.core.management import call_command
|
||||
call_command('createsuperuser')
|
||||
|
||||
call_command('collectstatic', '--no-input')
|
||||
|
||||
if ctx.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):
|
||||
from hostadmin.serializers import CategorySerializer, PropertySerializer, TagSerializer
|
||||
from hostadmin.models import ImportedIdentifierSets
|
||||
from hashlib import sha256
|
||||
if not os.path.exists('shared_data'):
|
||||
os.mkdir('shared_data')
|
||||
files = os.listdir('shared_data')
|
||||
idsets = {}
|
||||
hashes = {}
|
||||
for file in files:
|
||||
if file.endswith('.json'):
|
||||
name = "git:" + file[:-5]
|
||||
|
@ -106,8 +94,6 @@ def configure(ctx):
|
|||
try:
|
||||
idset = json.load(f)
|
||||
idsets[name] = idset
|
||||
f.seek(0)
|
||||
hashes[name] = sha256(f.read().encode()).hexdigest()
|
||||
except json.decoder.JSONDecodeError:
|
||||
print('Error: invalid JSON in file {}'.format(file))
|
||||
imported_sets = ImportedIdentifierSets.objects.all()
|
||||
|
@ -122,13 +108,9 @@ def configure(ctx):
|
|||
unmet_deps = [dep for dep in idset['depends'] if not imported_sets.filter(name=dep).exists()]
|
||||
if unmet_deps:
|
||||
if all([dep in idsets.keys() for dep in unmet_deps]):
|
||||
if all([dep in queue for dep in unmet_deps]):
|
||||
print('Not all dependencies for {} are imported, postponing'.format(name))
|
||||
queue.append(name)
|
||||
continue
|
||||
else:
|
||||
print('Error: unresolvable dependencies for {}: {}'.format(name, unmet_deps))
|
||||
continue
|
||||
print('Not all dependencies for {} are imported, postponing'.format(name))
|
||||
queue.append(name)
|
||||
continue
|
||||
else:
|
||||
print('unknown dependencies for {}: {}'.format(name, unmet_deps))
|
||||
continue
|
||||
|
@ -149,15 +131,10 @@ def configure(ctx):
|
|||
serializer = TagSerializer(data=tag)
|
||||
if serializer.is_valid():
|
||||
serializer.save(origin=name)
|
||||
imported_sets.create(name=name, hash=hashes[name])
|
||||
imported_sets.create(name=name)
|
||||
except IntegrityError:
|
||||
print('Error: integrity error while importing {}\n\tmight be cause by name conflicts with existing'
|
||||
' categories, properties or tags'.format(name))
|
||||
transaction.set_rollback(True)
|
||||
continue
|
||||
except Exception as e:
|
||||
print('Error: {}'.format(e))
|
||||
transaction.set_rollback(True)
|
||||
continue
|
||||
|
||||
|
||||
|
@ -206,7 +183,6 @@ def main():
|
|||
parser = ArgumentParser(description='Toolshed Server Configuration')
|
||||
parser.add_argument('--yes', '-y', help='Answer yes to all questions', action='store_true')
|
||||
parser.add_argument('--no', '-n', help='Answer no to all questions', action='store_true')
|
||||
parser.add_argument('--noninteractive', '-x', help="Run in noninteractive mode", action='store_true')
|
||||
parser.add_argument('cmd', help='Command', default='configure', nargs='?')
|
||||
args = parser.parse_args()
|
||||
|
||||
|
@ -214,16 +190,12 @@ def main():
|
|||
print('Error: --yes and --no are mutually exclusive')
|
||||
exit(1)
|
||||
|
||||
ctx = CmdCtx(args)
|
||||
|
||||
if args.cmd == 'configure':
|
||||
configure(ctx)
|
||||
configure()
|
||||
elif args.cmd == 'reset':
|
||||
reset()
|
||||
elif args.cmd == 'testdata':
|
||||
testdata()
|
||||
elif args.cmd == 'migrate':
|
||||
print('not implemented yet')
|
||||
else:
|
||||
print('Unknown command: {}'.format(args.cmd))
|
||||
exit(1)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import Domain, ImportedIdentifierSets
|
||||
from .models import Domain
|
||||
|
||||
|
||||
class DomainAdmin(admin.ModelAdmin):
|
||||
|
@ -9,11 +9,3 @@ class DomainAdmin(admin.ModelAdmin):
|
|||
|
||||
|
||||
admin.site.register(Domain, DomainAdmin)
|
||||
|
||||
|
||||
class ImportedIdentifierSetsAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'hash', 'created_at')
|
||||
list_filter = ('name', 'hash', 'created_at')
|
||||
|
||||
|
||||
admin.site.register(ImportedIdentifierSets, ImportedIdentifierSetsAdmin)
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
# Generated by Django 4.2.2 on 2024-03-11 15:19
|
||||
import os
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('hostadmin', '0002_importedidentifiersets'),
|
||||
]
|
||||
|
||||
def calculate_hash(apps, schema_editor):
|
||||
from hostadmin.models import ImportedIdentifierSets
|
||||
for identifier_set in ImportedIdentifierSets.objects.all():
|
||||
if not identifier_set.hash:
|
||||
print("update", identifier_set.name)
|
||||
filename = "shared_data/" + identifier_set.name.strip('git:') + ".json"
|
||||
if not os.path.exists(filename):
|
||||
continue
|
||||
from hashlib import sha256
|
||||
with open(filename, 'r') as file:
|
||||
data = file.read()
|
||||
identifier_set.hash = sha256(data.encode()).hexdigest()
|
||||
identifier_set.save()
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='importedidentifiersets',
|
||||
name='hash',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
|
||||
),
|
||||
migrations.RunPython(calculate_hash),
|
||||
migrations.AlterField(
|
||||
model_name='importedidentifiersets',
|
||||
name='hash',
|
||||
field=models.CharField(max_length=255, unique=True),
|
||||
),
|
||||
]
|
|
@ -1,17 +0,0 @@
|
|||
# Generated by Django 4.2.2 on 2024-03-14 16:33
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hostadmin', '0003_importedidentifiersets_hash'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='importedidentifiersets',
|
||||
options={'verbose_name_plural': 'imported identifier sets'},
|
||||
),
|
||||
]
|
|
@ -12,8 +12,4 @@ class Domain(models.Model):
|
|||
|
||||
class ImportedIdentifierSets(models.Model):
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
hash = models.CharField(max_length=255, unique=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = 'imported identifier sets'
|
||||
|
|
|
@ -5,32 +5,6 @@ from hostadmin.models import Domain
|
|||
from toolshed.models import Category, Property, Tag
|
||||
|
||||
|
||||
class SlugPathField(serializers.SlugRelatedField):
|
||||
def to_internal_value(self, data):
|
||||
path = data.split('/') if '/' in data else [data]
|
||||
candidates = self.get_queryset().filter(name=path[-1])
|
||||
if len(candidates) == 1:
|
||||
return candidates.first()
|
||||
if len(candidates) == 0:
|
||||
raise serializers.ValidationError(
|
||||
"No {} with name '{}' found".format(self.queryset.model.__name__, path[-1]))
|
||||
if len(candidates) > 1 and len(path) == 1:
|
||||
raise serializers.ValidationError("Multiple {}s with name '{}' found, please specify the parent".format(
|
||||
self.queryset.model.__name__, path[-1]))
|
||||
parent = self.to_internal_value('/'.join(path[:-1]))
|
||||
candidates = self.get_queryset().filter(name=path[-1], parent=parent)
|
||||
if len(candidates) == 1:
|
||||
return candidates.first()
|
||||
if len(candidates) == 0:
|
||||
raise serializers.ValidationError(
|
||||
"No {} with name '{}' found".format(self.queryset.model.__name__, path[-1]))
|
||||
|
||||
def to_representation(self, value):
|
||||
source = getattr(value, self.field_name, None) # should this use self.source?
|
||||
prefix = self.to_representation(source) + '/' if source else ''
|
||||
return prefix + getattr(value, self.slug_field)
|
||||
|
||||
|
||||
class DomainSerializer(serializers.ModelSerializer):
|
||||
owner = OwnerSerializer(read_only=True)
|
||||
|
||||
|
@ -38,21 +12,12 @@ class DomainSerializer(serializers.ModelSerializer):
|
|||
model = Domain
|
||||
fields = ['name', 'owner', 'open_registration']
|
||||
|
||||
def create(self, validated_data):
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class CategorySerializer(serializers.ModelSerializer):
|
||||
parent = SlugPathField(slug_field='name', queryset=Category.objects.all(), required=False)
|
||||
|
||||
def validate(self, attrs):
|
||||
if 'name' in attrs:
|
||||
if '/' in attrs['name']:
|
||||
raise serializers.ValidationError("Category name cannot contain '/'")
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
try:
|
||||
return Category.objects.create(**validated_data)
|
||||
except Exception as e:
|
||||
raise serializers.ValidationError(e)
|
||||
parent = serializers.SlugRelatedField(slug_field='name', queryset=Category.objects.all(), required=False)
|
||||
|
||||
class Meta:
|
||||
model = Category
|
||||
|
@ -62,19 +27,7 @@ class CategorySerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class PropertySerializer(serializers.ModelSerializer):
|
||||
category = SlugPathField(slug_field='name', queryset=Category.objects.all(), required=False)
|
||||
|
||||
def validate(self, attrs):
|
||||
if 'name' in attrs:
|
||||
if '/' in attrs['name']:
|
||||
raise serializers.ValidationError("Property name cannot contain '/'")
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
try:
|
||||
return Property.objects.create(**validated_data)
|
||||
except Exception as e:
|
||||
raise serializers.ValidationError(e)
|
||||
category = serializers.SlugRelatedField(slug_field='name', queryset=Category.objects.all(), required=False)
|
||||
|
||||
class Meta:
|
||||
model = Property
|
||||
|
@ -85,19 +38,7 @@ class PropertySerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
category = SlugPathField(slug_field='name', queryset=Category.objects.all(), required=False)
|
||||
|
||||
def validate(self, attrs):
|
||||
if 'name' in attrs:
|
||||
if '/' in attrs['name']:
|
||||
raise serializers.ValidationError("Tag name cannot contain '/'")
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
try:
|
||||
return Tag.objects.create(**validated_data)
|
||||
except Exception as e:
|
||||
raise serializers.ValidationError(e)
|
||||
category = serializers.SlugRelatedField(slug_field='name', queryset=Category.objects.all(), required=False)
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
|
|
|
@ -100,8 +100,7 @@ class CategoryApiTestCase(UserTestMixin, CategoryTestMixin, ToolshedTestCase):
|
|||
response = client.get('/api/categories/', self.f['local_user1'])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(),
|
||||
["cat1", "cat2", "cat3", "cat1/subcat1",
|
||||
"cat1/subcat2", "cat1/subcat1/subcat1", "cat1/subcat1/subcat2"])
|
||||
["cat1", "cat2", "cat3", "cat1/subcat1", "cat1/subcat2", "cat1/subcat1/subcat3"])
|
||||
|
||||
def test_admin_get_categories_fail(self):
|
||||
response = client.get('/admin/categories/', self.f['local_user1'])
|
||||
|
@ -110,7 +109,7 @@ class CategoryApiTestCase(UserTestMixin, CategoryTestMixin, ToolshedTestCase):
|
|||
def test_admin_get_categories(self):
|
||||
response = client.get('/admin/categories/', self.f['admin'])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(response.json()), 7)
|
||||
self.assertEqual(len(response.json()), 6)
|
||||
self.assertEqual(response.json()[0]['name'], 'cat1')
|
||||
self.assertEqual(response.json()[1]['name'], 'cat2')
|
||||
self.assertEqual(response.json()[2]['name'], 'cat3')
|
||||
|
@ -118,12 +117,10 @@ class CategoryApiTestCase(UserTestMixin, CategoryTestMixin, ToolshedTestCase):
|
|||
self.assertEqual(response.json()[3]['parent'], 'cat1')
|
||||
self.assertEqual(response.json()[4]['name'], 'subcat2')
|
||||
self.assertEqual(response.json()[4]['parent'], 'cat1')
|
||||
self.assertEqual(response.json()[5]['name'], 'subcat1')
|
||||
self.assertEqual(response.json()[5]['parent'], 'cat1/subcat1')
|
||||
self.assertEqual(response.json()[6]['name'], 'subcat2')
|
||||
self.assertEqual(response.json()[6]['parent'], 'cat1/subcat1')
|
||||
self.assertEqual(response.json()[5]['name'], 'subcat3')
|
||||
self.assertEqual(response.json()[5]['parent'], 'subcat1')
|
||||
|
||||
def test_admin_post_category(self):
|
||||
def test_admin_create_category(self):
|
||||
response = client.post('/admin/categories/', self.f['admin'], {'name': 'cat4'})
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.json()['name'], 'cat4')
|
||||
|
@ -131,40 +128,6 @@ class CategoryApiTestCase(UserTestMixin, CategoryTestMixin, ToolshedTestCase):
|
|||
self.assertEqual(response.json()['parent'], None)
|
||||
self.assertEqual(response.json()['origin'], 'api')
|
||||
|
||||
def test_admin_post_category_duplicate(self):
|
||||
response = client.post('/admin/categories/', self.f['admin'], {'name': 'cat3'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_admin_post_category_invalid(self):
|
||||
response = client.post('/admin/categories/', self.f['admin'], {'name': 'cat/4'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_admin_post_category_parent_not_found(self):
|
||||
response = client.post('/admin/categories/', self.f['admin'], {'name': 'subcat4', 'parent': 'cat4'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_admin_post_category_parent_ambiguous(self):
|
||||
response = client.post('/admin/categories/', self.f['admin'], {'name': 'subcat4', 'parent': 'subcat1'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_admin_post_category_parent_subcategory(self):
|
||||
response = client.post('/admin/categories/', self.f['admin'], {'name': 'subcat4', 'parent': 'cat1/subcat1'})
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.json()['name'], 'subcat4')
|
||||
self.assertEqual(response.json()['description'], None)
|
||||
self.assertEqual(response.json()['parent'], 'cat1/subcat1')
|
||||
self.assertEqual(response.json()['origin'], 'api')
|
||||
|
||||
def test_admin_post_category_parent_subcategory_not_found(self):
|
||||
response = client.post('/admin/categories/', self.f['admin'], {'name': 'subcat4', 'parent': 'cat2/subcat1'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_admin_post_category_parent_subcategory_ambiguous(self):
|
||||
from toolshed.models import Category
|
||||
self.f['subcat111'] = Category.objects.create(name='subcat1', parent=self.f['subcat11'], origin='test')
|
||||
response = client.post('/admin/categories/', self.f['admin'], {'name': 'subcat4', 'parent': 'subcat1/subcat1'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_admin_post_subcategory(self):
|
||||
response = client.post('/admin/categories/', self.f['admin'], {'name': 'subcat4', 'parent': 'cat1'})
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
@ -173,18 +136,6 @@ class CategoryApiTestCase(UserTestMixin, CategoryTestMixin, ToolshedTestCase):
|
|||
self.assertEqual(response.json()['parent'], 'cat1')
|
||||
self.assertEqual(response.json()['origin'], 'api')
|
||||
|
||||
def test_admin_post_subcategory_duplicate(self):
|
||||
response = client.post('/admin/categories/', self.f['admin'], {'name': 'subcat2', 'parent': 'cat1'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_admin_post_subcategory_distinct_duplicate(self):
|
||||
response = client.post('/admin/categories/', self.f['admin'], {'name': 'subcat2', 'parent': 'cat2'})
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.json()['name'], 'subcat2')
|
||||
self.assertEqual(response.json()['description'], None)
|
||||
self.assertEqual(response.json()['parent'], 'cat2')
|
||||
self.assertEqual(response.json()['origin'], 'api')
|
||||
|
||||
def test_admin_put_category(self):
|
||||
response = client.put('/admin/categories/1/', self.f['admin'], {'name': 'cat5'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -237,14 +188,6 @@ class TagApiTestCase(UserTestMixin, CategoryTestMixin, TagTestMixin, ToolshedTes
|
|||
self.assertEqual(response.json()['origin'], 'api')
|
||||
self.assertEqual(response.json()['category'], None)
|
||||
|
||||
def test_admin_create_tag_duplicate(self):
|
||||
response = client.post('/admin/tags/', self.f['admin'], {'name': 'tag3'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_admin_create_tag_invalid(self):
|
||||
response = client.post('/admin/tags/', self.f['admin'], {'name': 'tag/4'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_admin_put_tag(self):
|
||||
response = client.put('/admin/tags/1/', self.f['admin'], {'name': 'tag5'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -307,13 +250,7 @@ class PropertyApiTestCase(UserTestMixin, CategoryTestMixin, PropertyTestMixin, T
|
|||
self.assertEqual(response.json()['base2_prefix'], False)
|
||||
self.assertEqual(response.json()['dimensions'], 1)
|
||||
|
||||
def test_admin_create_property_duplicate(self):
|
||||
response = client.post('/admin/properties/', self.f['admin'], {'name': 'prop3', 'category': 'cat1'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_admin_create_property_invalid(self):
|
||||
response = client.post('/admin/properties/', self.f['admin'], {'name': 'prop/4'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
# self.assertEqual(response.json()['sort_lexicographically'], False)
|
||||
|
||||
def test_admin_put_property(self):
|
||||
response = client.put('/admin/properties/1/', self.f['admin'], {'name': 'prop5'})
|
||||
|
@ -328,6 +265,8 @@ class PropertyApiTestCase(UserTestMixin, CategoryTestMixin, PropertyTestMixin, T
|
|||
self.assertEqual(response.json()['base2_prefix'], False)
|
||||
self.assertEqual(response.json()['dimensions'], 1)
|
||||
|
||||
# self.assertEqual(response.json()['sort_lexicographically'], False)
|
||||
|
||||
def test_admin_patch_property(self):
|
||||
response = client.patch('/admin/properties/1/', self.f['admin'], {'name': 'prop5'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from toolshed.models import InventoryItem, Property, Tag, Category
|
||||
from toolshed.models import InventoryItem, Property, Tag, ItemProperty, ItemTag
|
||||
|
||||
|
||||
class InventoryItemAdmin(admin.ModelAdmin):
|
||||
|
@ -12,24 +12,16 @@ admin.site.register(InventoryItem, InventoryItemAdmin)
|
|||
|
||||
|
||||
class PropertyAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'description', 'category', 'unit_symbol', 'base2_prefix', 'dimensions', 'origin')
|
||||
search_fields = ('name', 'description', 'category', 'unit_symbol', 'base2_prefix', 'dimensions', 'origin')
|
||||
list_display = ('name',)
|
||||
search_fields = ('name',)
|
||||
|
||||
|
||||
admin.site.register(Property, PropertyAdmin)
|
||||
|
||||
|
||||
class TagAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'description', 'category', 'origin')
|
||||
search_fields = ('name', 'description', 'category', 'origin')
|
||||
list_display = ('name',)
|
||||
search_fields = ('name',)
|
||||
|
||||
|
||||
admin.site.register(Tag, TagAdmin)
|
||||
|
||||
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'description', 'parent', 'origin')
|
||||
search_fields = ('name', 'description', 'parent', 'origin')
|
||||
|
||||
|
||||
admin.site.register(Category, CategoryAdmin)
|
||||
|
|
|
@ -64,23 +64,15 @@ class FriendsRequests(APIView, ViewSetMixin):
|
|||
befriendee_username, befriendee_domain = split_userhandle_or_throw(request.data['befriendee'])
|
||||
if befriender_domain == befriendee_domain and befriender_username == befriendee_username:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data={'status': 'cannot befriend yourself'})
|
||||
if user := authenticate_request_against_local_users(request, raw_request): # befriender is local
|
||||
if user := authenticate_request_against_local_users(request, raw_request):
|
||||
secret = secrets.token_hex(64)
|
||||
befriendee_user = ToolshedUser.objects.filter(username=befriendee_username, domain=befriendee_domain)
|
||||
if befriendee_user.exists(): # befriendee is local (both are local)
|
||||
if user.friends.filter(username=befriendee_username, domain=befriendee_domain).exists():
|
||||
return Response(status=status.HTTP_208_ALREADY_REPORTED, data={'status': "exists"})
|
||||
existing_request = FriendRequestIncoming.objects.filter(
|
||||
befriender_username=befriender_username,
|
||||
befriender_domain=befriender_domain,
|
||||
befriendee_user=befriendee_user.get())
|
||||
if existing_request.exists():
|
||||
return Response(status=status.HTTP_208_ALREADY_REPORTED, data={'status': "exists"})
|
||||
if befriendee_user.exists():
|
||||
FriendRequestIncoming.objects.create(
|
||||
befriender_username=befriender_username,
|
||||
befriender_domain=befriender_domain,
|
||||
befriender_public_key=user.public_identity.public_key,
|
||||
secret=secret,
|
||||
secret=secret, # request.data['secret'] # TODO ??
|
||||
befriendee_user=befriendee_user.get(),
|
||||
)
|
||||
return Response(status=status.HTTP_201_CREATED, data={'secret': secret, 'status': "pending"})
|
||||
|
@ -89,7 +81,7 @@ class FriendsRequests(APIView, ViewSetMixin):
|
|||
befriender_user=user,
|
||||
befriendee_username=befriendee_username,
|
||||
befriendee_domain=befriendee_domain,
|
||||
secret=secret,
|
||||
secret=secret, # request.data['secret'] # TODO ??
|
||||
)
|
||||
return Response(status=status.HTTP_201_CREATED, data={'secret': secret, 'status': "pending"})
|
||||
elif verify_incoming_friend_request(request, raw_request):
|
||||
|
|
|
@ -5,7 +5,7 @@ from rest_framework.response import Response
|
|||
|
||||
from hostadmin.models import Domain
|
||||
from authentication.signature_auth import SignatureAuthentication
|
||||
from toolshed.models import Tag, Property, Category, InventoryItem
|
||||
from toolshed.models import Tag, Property, Category
|
||||
from toolshed.serializers import CategorySerializer, PropertySerializer
|
||||
from backend.settings import TOOLSHED_VERSION
|
||||
|
||||
|
@ -51,7 +51,8 @@ def list_categories(request, format=None): # /categories/
|
|||
@permission_classes([IsAuthenticated])
|
||||
@authentication_classes([SignatureAuthentication])
|
||||
def list_availability_policies(request, format=None): # /availability_policies/
|
||||
return Response(InventoryItem.AVAILABILITY_POLICY_CHOICES)
|
||||
policies = ['private', 'friends', 'internal', 'public']
|
||||
return Response(policies)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
|
@ -61,11 +62,9 @@ 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 = InventoryItem.AVAILABILITY_POLICY_CHOICES
|
||||
policies = ['private', 'friends', 'internal', 'public']
|
||||
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 = [
|
||||
|
|
|
@ -7,8 +7,8 @@ from rest_framework.response import Response
|
|||
|
||||
from authentication.models import ToolshedUser, KnownIdentity
|
||||
from authentication.signature_auth import SignatureAuthentication
|
||||
from toolshed.models import InventoryItem, StorageLocation
|
||||
from toolshed.serializers import InventoryItemSerializer, StorageLocationSerializer
|
||||
from toolshed.models import InventoryItem
|
||||
from toolshed.serializers import InventoryItemSerializer
|
||||
|
||||
router = routers.SimpleRouter()
|
||||
|
||||
|
@ -24,8 +24,7 @@ def inventory_items(identity):
|
|||
for friend in identity.friends.all():
|
||||
if friend_user := friend.user.get():
|
||||
for item in friend_user.inventory_items.all():
|
||||
if item.availability_policy != 'private':
|
||||
yield item
|
||||
yield item
|
||||
|
||||
|
||||
class InventoryItemViewSet(viewsets.ModelViewSet):
|
||||
|
@ -62,19 +61,7 @@ def search_inventory_items(request):
|
|||
return Response({'error': 'No query provided.'}, status=400)
|
||||
|
||||
|
||||
class StorageLocationViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = StorageLocationSerializer
|
||||
authentication_classes = [SignatureAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
if type(self.request.user) == KnownIdentity and self.request.user.user.exists():
|
||||
return StorageLocation.objects.filter(owner=self.request.user.user.get())
|
||||
return StorageLocation.objects.none()
|
||||
|
||||
|
||||
router.register(r'inventory_items', InventoryItemViewSet, basename='inventory_items')
|
||||
router.register(r'storage_locations', StorageLocationViewSet, basename='storage_locations')
|
||||
|
||||
urlpatterns = router.urls + [
|
||||
path('search/', search_inventory_items, name='search_inventory_items'),
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
# Generated by Django 4.2.2 on 2024-02-20 13:50
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('toolshed', '0003_inventoryitem_files_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='StorageLocation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='storage_locations', to='toolshed.category')),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='storage_locations', to=settings.AUTH_USER_MODEL)),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='toolshed.storagelocation')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryitem',
|
||||
name='storage_location',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inventory_items', to='toolshed.storagelocation'),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.2.2 on 2024-02-20 15:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('toolshed', '0004_storagelocation_inventoryitem_storage_location'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='availability_policy',
|
||||
field=models.CharField(choices=[('sell', 'Sell'), ('rent', 'Rent'), ('lend', 'Lend'), ('share', 'Share'), ('private', 'Private')], default='private', max_length=20),
|
||||
),
|
||||
]
|
|
@ -1,67 +0,0 @@
|
|||
# Generated by Django 4.2.2 on 2024-03-14 16:54
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('toolshed', '0005_alter_inventoryitem_availability_policy'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='tag',
|
||||
options={'verbose_name_plural': 'tags'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='name',
|
||||
field=models.CharField(max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='parent',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='toolshed.category'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='category',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inventory_items', to='toolshed.category'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='property',
|
||||
name='category',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='properties', to='toolshed.category'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tag',
|
||||
name='category',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tags', to='toolshed.category'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='category',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', False)), fields=('name', 'parent'), name='category_unique_name_parent'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='category',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('name',), name='category_unique_name_no_parent'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='property',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('category__isnull', False)), fields=('name', 'category'), name='property_unique_name_category'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='property',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('category__isnull', True)), fields=('name',), name='property_unique_name_no_category'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='tag',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('category__isnull', False)), fields=('name', 'category'), name='tag_unique_name_category'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='tag',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('category__isnull', True)), fields=('name',), name='tag_unique_name_no_category'),
|
||||
),
|
||||
]
|
|
@ -8,19 +8,13 @@ from files.models import File
|
|||
|
||||
|
||||
class Category(SoftDeleteModel):
|
||||
name = models.CharField(max_length=255)
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, related_name='children')
|
||||
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')
|
||||
origin = models.CharField(max_length=255, null=False, blank=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = 'categories'
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['name', 'parent'], condition=models.Q(parent__isnull=False),
|
||||
name='category_unique_name_parent'),
|
||||
models.UniqueConstraint(fields=['name'], condition=models.Q(parent__isnull=True),
|
||||
name='category_unique_name_no_parent')
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
parent = str(self.parent) + "/" if self.parent else ""
|
||||
|
@ -30,7 +24,7 @@ class Category(SoftDeleteModel):
|
|||
class Property(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, related_name='properties')
|
||||
category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True, related_name='properties')
|
||||
unit_symbol = models.CharField(max_length=16, null=True, blank=True)
|
||||
unit_name = models.CharField(max_length=255, null=True, blank=True)
|
||||
unit_name_plural = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
@ -40,12 +34,6 @@ class Property(models.Model):
|
|||
|
||||
class Meta:
|
||||
verbose_name_plural = 'properties'
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['name', 'category'], condition=models.Q(category__isnull=False),
|
||||
name='property_unique_name_category'),
|
||||
models.UniqueConstraint(fields=['name'], condition=models.Q(category__isnull=True),
|
||||
name='property_unique_name_no_category')
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
@ -54,44 +42,26 @@ class Property(models.Model):
|
|||
class Tag(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, related_name='tags')
|
||||
category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True, related_name='tags')
|
||||
origin = models.CharField(max_length=255, null=False, blank=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = 'tags'
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['name', 'category'], condition=models.Q(category__isnull=False),
|
||||
name='tag_unique_name_category'),
|
||||
models.UniqueConstraint(fields=['name'], condition=models.Q(category__isnull=True),
|
||||
name='tag_unique_name_no_category')
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class InventoryItem(SoftDeleteModel):
|
||||
AVAILABILITY_POLICY_CHOICES = (
|
||||
('sell', 'Sell'),
|
||||
('rent', 'Rent'),
|
||||
('lend', 'Lend'),
|
||||
('share', 'Share'),
|
||||
('private', 'Private'),
|
||||
)
|
||||
|
||||
published = models.BooleanField(default=False)
|
||||
name = models.CharField(max_length=255, null=True, blank=True)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, related_name='inventory_items')
|
||||
availability_policy = models.CharField(max_length=20, choices=AVAILABILITY_POLICY_CHOICES, default='private')
|
||||
category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True,
|
||||
related_name='inventory_items')
|
||||
availability_policy = models.CharField(max_length=255, default="private")
|
||||
owned_quantity = models.IntegerField(default=1, validators=[MinValueValidator(0)])
|
||||
owner = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='inventory_items')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
tags = models.ManyToManyField(Tag, through='ItemTag', related_name='inventory_items')
|
||||
properties = models.ManyToManyField(Property, through='ItemProperty')
|
||||
files = models.ManyToManyField(File, related_name='connected_items')
|
||||
storage_location = models.ForeignKey('StorageLocation', on_delete=models.CASCADE, null=True, blank=True,
|
||||
related_name='inventory_items')
|
||||
|
||||
def clean(self):
|
||||
if (self.name is None or self.name == "") and self.files.count() == 0:
|
||||
|
@ -107,16 +77,3 @@ class ItemProperty(models.Model):
|
|||
class ItemTag(models.Model):
|
||||
tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
|
||||
inventory_item = models.ForeignKey(InventoryItem, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class StorageLocation(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True,
|
||||
related_name='storage_locations')
|
||||
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')
|
||||
owner = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='storage_locations')
|
||||
|
||||
def __str__(self):
|
||||
parent = str(self.parent) + "/" if self.parent else ""
|
||||
return parent + self.name
|
||||
|
|
|
@ -3,7 +3,7 @@ from authentication.models import KnownIdentity, ToolshedUser, FriendRequestInco
|
|||
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
|
||||
|
||||
|
||||
class FriendSerializer(serializers.ModelSerializer):
|
||||
|
@ -11,7 +11,7 @@ class FriendSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = KnownIdentity
|
||||
fields = ['id', 'username', 'public_key']
|
||||
fields = ['username', 'public_key']
|
||||
|
||||
def get_username(self, obj):
|
||||
return obj.username + '@' + obj.domain
|
||||
|
@ -48,23 +48,6 @@ class CategorySerializer(serializers.ModelSerializer):
|
|||
return Category.objects.get(name=data.split("/")[-1])
|
||||
|
||||
|
||||
class StorageLocationSerializer(serializers.ModelSerializer):
|
||||
owner = OwnerSerializer(read_only=True)
|
||||
category = CategorySerializer(required=False, allow_null=True)
|
||||
path = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = StorageLocation
|
||||
fields = ['id', 'name', 'description', 'path', 'category', 'owner']
|
||||
read_only_fields = ['path']
|
||||
|
||||
@staticmethod
|
||||
def get_path(obj):
|
||||
if obj.parent:
|
||||
return StorageLocationSerializer.get_path(obj.parent) + "/" + obj.name
|
||||
return obj.name
|
||||
|
||||
|
||||
class ItemPropertySerializer(serializers.ModelSerializer):
|
||||
property = PropertySerializer(read_only=True)
|
||||
|
||||
|
@ -91,7 +74,7 @@ class InventoryItemSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = ['id', 'name', 'description', 'owner', 'category', 'availability_policy', 'owned_quantity', 'owner',
|
||||
'tags', 'properties', 'files', 'storage_location']
|
||||
'tags', 'properties', 'files']
|
||||
|
||||
def to_internal_value(self, data):
|
||||
files = data.pop('files', [])
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from toolshed.models import Category, Tag, Property, InventoryItem, ItemProperty, StorageLocation
|
||||
from toolshed.models import Category, Tag, Property, InventoryItem, ItemProperty
|
||||
|
||||
|
||||
class CategoryTestMixin:
|
||||
|
@ -8,16 +8,13 @@ class CategoryTestMixin:
|
|||
self.f['cat3'] = Category.objects.create(name='cat3', origin='test')
|
||||
self.f['subcat1'] = Category.objects.create(name='subcat1', parent=self.f['cat1'], origin='test')
|
||||
self.f['subcat2'] = Category.objects.create(name='subcat2', parent=self.f['cat1'], origin='test')
|
||||
self.f['subcat11'] = Category.objects.create(name='subcat1', parent=self.f['subcat1'], origin='test')
|
||||
self.f['subcat12'] = Category.objects.create(name='subcat2', parent=self.f['subcat1'], origin='test')
|
||||
self.f['subcat3'] = Category.objects.create(name='subcat3', parent=self.f['subcat1'], origin='test')
|
||||
|
||||
|
||||
class TagTestMixin:
|
||||
def prepare_tags(self):
|
||||
self.f['tag1'] = Tag.objects.create(name='tag1', description='tag1 description', category=self.f['cat1'],
|
||||
origin='test')
|
||||
self.f['tag2'] = Tag.objects.create(name='tag2', description='tag2 description', category=self.f['cat1'],
|
||||
origin='test')
|
||||
self.f['tag1'] = Tag.objects.create(name='tag1', description='tag1 description', category=self.f['cat1'], origin='test')
|
||||
self.f['tag2'] = Tag.objects.create(name='tag2', description='tag2 description', category=self.f['cat1'], origin='test')
|
||||
self.f['tag3'] = Tag.objects.create(name='tag3', origin='test')
|
||||
|
||||
|
||||
|
@ -44,13 +41,3 @@ class InventoryTestMixin(CategoryTestMixin, TagTestMixin, PropertyTestMixin):
|
|||
self.f['item2'].tags.add(self.f['tag2'], through_defaults={})
|
||||
ItemProperty.objects.create(inventory_item=self.f['item2'], property=self.f['prop1'], value='value1').save()
|
||||
ItemProperty.objects.create(inventory_item=self.f['item2'], property=self.f['prop2'], value='value2').save()
|
||||
|
||||
|
||||
class LocationTestMixin:
|
||||
def prepare_locations(self):
|
||||
self.f['loc1'] = StorageLocation.objects.create(name='loc1', owner=self.f['local_user1'])
|
||||
self.f['loc2'] = StorageLocation.objects.create(name='loc2', owner=self.f['local_user1'],
|
||||
category=self.f['cat1'])
|
||||
self.f['loc3'] = StorageLocation.objects.create(name='loc3', owner=self.f['local_user1'], parent=self.f['loc1'])
|
||||
self.f['loc4'] = StorageLocation.objects.create(name='loc4', owner=self.f['local_user1'], parent=self.f['loc1'],
|
||||
category=self.f['cat1'])
|
||||
|
|
|
@ -43,8 +43,7 @@ class CombinedApiTestCase(UserTestMixin, CategoryTestMixin, TagTestMixin, Proper
|
|||
def test_policy_api(self):
|
||||
response = client.get('/api/availability_policies/', self.f['local_user1'])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), [['sell', 'Sell'], ['rent', 'Rent'], ['lend', 'Lend'], ['share', 'Share'],
|
||||
['private', 'Private']])
|
||||
self.assertEqual(response.json(), ['private', 'friends', 'internal', 'public'])
|
||||
|
||||
def test_combined_api_anonymous(self):
|
||||
response = anonymous_client.get('/api/info/')
|
||||
|
@ -53,11 +52,9 @@ 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'], [['sell', 'Sell'], ['rent', 'Rent'], ['lend', 'Lend'],
|
||||
['share', 'Share'], ['private', 'Private']])
|
||||
self.assertEqual(response.json()['availability_policies'], ['private', 'friends', 'internal', 'public'])
|
||||
self.assertEqual(response.json()['categories'],
|
||||
['cat1', 'cat2', 'cat3', 'cat1/subcat1', 'cat1/subcat2', 'cat1/subcat1/subcat1',
|
||||
'cat1/subcat1/subcat2'])
|
||||
['cat1', 'cat2', 'cat3', 'cat1/subcat1', 'cat1/subcat2', 'cat1/subcat1/subcat3'])
|
||||
self.assertEqual(response.json()['tags'], ['tag1', 'tag2', 'tag3'])
|
||||
self.assertEqual([p['name'] for p in response.json()['properties']], ['prop1', 'prop2', 'prop3'])
|
||||
self.assertEqual(response.json()['domains'], ['example.com'])
|
||||
|
|
|
@ -17,11 +17,10 @@ class CategoryTestCase(CategoryTestMixin, UserTestMixin, ToolshedTestCase):
|
|||
self.assertEqual(self.f['cat1'].children.last(), self.f['subcat2'])
|
||||
self.assertEqual(self.f['subcat1'].parent, self.f['cat1'])
|
||||
self.assertEqual(self.f['subcat2'].parent, self.f['cat1'])
|
||||
self.assertEqual(self.f['subcat1'].children.count(), 2)
|
||||
self.assertEqual(self.f['subcat1'].children.count(), 1)
|
||||
self.assertEqual(str(self.f['subcat1']), 'cat1/subcat1')
|
||||
self.assertEqual(str(self.f['subcat2']), 'cat1/subcat2')
|
||||
self.assertEqual(str(self.f['subcat11']), 'cat1/subcat1/subcat1')
|
||||
self.assertEqual(str(self.f['subcat12']), 'cat1/subcat1/subcat2')
|
||||
self.assertEqual(str(self.f['subcat3']), 'cat1/subcat1/subcat3')
|
||||
|
||||
|
||||
class CategoryApiTestCase(CategoryTestMixin, UserTestMixin, ToolshedTestCase):
|
||||
|
@ -34,12 +33,10 @@ class CategoryApiTestCase(CategoryTestMixin, UserTestMixin, ToolshedTestCase):
|
|||
def test_get_categories(self):
|
||||
reply = client.get('/api/categories/', self.f['local_user1'])
|
||||
self.assertEqual(reply.status_code, 200)
|
||||
self.assertEqual(len(reply.json()), 7)
|
||||
self.assertEqual(len(reply.json()), 6)
|
||||
self.assertEqual(reply.json()[0], 'cat1')
|
||||
self.assertEqual(reply.json()[1], 'cat2')
|
||||
self.assertEqual(reply.json()[2], 'cat3')
|
||||
self.assertEqual(reply.json()[3], 'cat1/subcat1')
|
||||
self.assertEqual(reply.json()[4], 'cat1/subcat2')
|
||||
self.assertEqual(reply.json()[5], 'cat1/subcat1/subcat1')
|
||||
self.assertEqual(reply.json()[6], 'cat1/subcat1/subcat2')
|
||||
|
||||
self.assertEqual(reply.json()[5], 'cat1/subcat1/subcat3')
|
||||
|
|
|
@ -130,7 +130,7 @@ class FriendRequestListTestCase(UserTestMixin, ToolshedTestCase):
|
|||
|
||||
def test_delete_friend_request(self):
|
||||
reply = client.delete('/api/friendrequests/{}/'.format(self.friendrequest1.id),
|
||||
self.f['local_user1'])
|
||||
self.f['local_user1'])
|
||||
self.assertEqual(reply.status_code, 204)
|
||||
self.assertEqual(FriendRequestIncoming.objects.count(), 0)
|
||||
|
||||
|
@ -210,17 +210,6 @@ class FriendRequestIncomingTestCase(UserTestMixin, ToolshedTestCase):
|
|||
})
|
||||
self.assertEqual(reply.status_code, 400)
|
||||
|
||||
def test_post_request_missing_key_none(self):
|
||||
befriender = self.f['ext_user1']
|
||||
befriendee = self.f['local_user1']
|
||||
reply = client.post('/api/friendrequests/', befriender, {
|
||||
'befriender': str(befriender),
|
||||
'befriendee': str(befriendee),
|
||||
'befriender_key': None,
|
||||
'secret': 'secret2'
|
||||
})
|
||||
self.assertEqual(reply.status_code, 400)
|
||||
|
||||
def test_post_request_breaking_key(self):
|
||||
befriender = self.f['ext_user1']
|
||||
befriendee = self.f['local_user1']
|
||||
|
@ -368,43 +357,3 @@ class FriendRequestOutgoingTestCase(UserTestMixin, ToolshedTestCase):
|
|||
self.assertEqual(befriendee.friends.count(), 1)
|
||||
self.assertEqual(befriendee.friends.first().username, befriender.username)
|
||||
self.assertEqual(befriendee.friends.first().domain, befriender.domain)
|
||||
|
||||
|
||||
class FriendRequestCombinedTestCase(UserTestMixin, ToolshedTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.prepare_users()
|
||||
|
||||
def test_friend_request_combined(self):
|
||||
befriender = self.f['local_user1']
|
||||
befriendee = self.f['local_user2']
|
||||
reply1 = client.post('/api/friendrequests/', befriender, {
|
||||
'befriender': str(befriender),
|
||||
'befriendee': str(befriendee),
|
||||
})
|
||||
secret = reply1.json()['secret']
|
||||
reply2 = client.post('/api/friendrequests/', befriender, {
|
||||
'befriender': str(befriender),
|
||||
'befriender_key': befriender.public_key(),
|
||||
'befriendee': str(befriendee),
|
||||
'secret': secret
|
||||
})
|
||||
|
||||
self.assertEqual(reply1.status_code, 201)
|
||||
self.assertEqual(reply2.status_code, 208)
|
||||
self.assertEqual(reply1.json()['status'], 'pending')
|
||||
self.assertEqual(reply2.json()['status'], 'exists')
|
||||
self.assertEqual(FriendRequestIncoming.objects.count(), 1)
|
||||
|
||||
def test_friend_request_already_friends(self):
|
||||
befriender = self.f['local_user1']
|
||||
befriendee = self.f['local_user2']
|
||||
befriender.friends.add(befriendee.public_identity)
|
||||
reply1 = client.post('/api/friendrequests/', befriender, {
|
||||
'befriender': str(befriender),
|
||||
'befriendee': str(befriendee),
|
||||
})
|
||||
self.assertEqual(reply1.status_code, 208)
|
||||
self.assertEqual(reply1.json()['status'], 'exists')
|
||||
self.assertEqual(FriendRequestIncoming.objects.count(), 0)
|
||||
|
|
|
@ -38,7 +38,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
|
|||
|
||||
def test_post_new_item(self):
|
||||
reply = client.post('/api/inventory_items/', self.f['local_user1'], {
|
||||
'availability_policy': 'rent',
|
||||
'availability_policy': 'friends',
|
||||
'category': 'cat2',
|
||||
'name': 'test3',
|
||||
'description': 'test',
|
||||
|
@ -50,7 +50,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
|
|||
self.assertEqual(reply.status_code, 201)
|
||||
self.assertEqual(InventoryItem.objects.count(), 3)
|
||||
item = InventoryItem.objects.get(name='test3')
|
||||
self.assertEqual(item.availability_policy, 'rent')
|
||||
self.assertEqual(item.availability_policy, 'friends')
|
||||
self.assertEqual(item.category, Category.objects.get(name='cat2'))
|
||||
self.assertEqual(item.name, 'test3')
|
||||
self.assertEqual(item.description, 'test')
|
||||
|
@ -61,7 +61,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
|
|||
|
||||
def test_post_new_item2(self):
|
||||
reply = client.post('/api/inventory_items/', self.f['local_user1'], {
|
||||
'availability_policy': 'share',
|
||||
'availability_policy': 'friends',
|
||||
'name': 'test3',
|
||||
'description': 'test',
|
||||
'owned_quantity': 1,
|
||||
|
@ -70,7 +70,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
|
|||
self.assertEqual(reply.status_code, 201)
|
||||
self.assertEqual(InventoryItem.objects.count(), 3)
|
||||
item = InventoryItem.objects.get(name='test3')
|
||||
self.assertEqual(item.availability_policy, 'share')
|
||||
self.assertEqual(item.availability_policy, 'friends')
|
||||
self.assertEqual(item.category, None)
|
||||
self.assertEqual(item.name, 'test3')
|
||||
self.assertEqual(item.description, 'test')
|
||||
|
@ -80,7 +80,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
|
|||
|
||||
def test_post_new_item_empty(self):
|
||||
reply = client.post('/api/inventory_items/', self.f['local_user1'], {
|
||||
'availability_policy': 'rent',
|
||||
'availability_policy': 'friends',
|
||||
'owned_quantity': 1,
|
||||
'image': '',
|
||||
})
|
||||
|
@ -89,7 +89,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
|
|||
|
||||
def test_post_new_item3(self):
|
||||
reply = client.post('/api/inventory_items/', self.f['local_user1'], {
|
||||
'availability_policy': 'private',
|
||||
'availability_policy': 'friends',
|
||||
'name': 'test3',
|
||||
'description': 'test',
|
||||
'owned_quantity': 1,
|
||||
|
@ -99,7 +99,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
|
|||
self.assertEqual(reply.status_code, 201)
|
||||
self.assertEqual(InventoryItem.objects.count(), 3)
|
||||
item = InventoryItem.objects.get(name='test3')
|
||||
self.assertEqual(item.availability_policy, 'private')
|
||||
self.assertEqual(item.availability_policy, 'friends')
|
||||
self.assertEqual(item.category, None)
|
||||
self.assertEqual(item.name, 'test3')
|
||||
self.assertEqual(item.description, 'test')
|
||||
|
@ -109,7 +109,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
|
|||
|
||||
def test_put_item(self):
|
||||
reply = client.put('/api/inventory_items/1/', self.f['local_user1'], {
|
||||
'availability_policy': 'sell',
|
||||
'availability_policy': 'friends',
|
||||
'name': 'test4',
|
||||
'description': 'new description',
|
||||
'owned_quantity': 100,
|
||||
|
@ -121,7 +121,7 @@ class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase):
|
|||
self.assertEqual(reply.status_code, 200)
|
||||
self.assertEqual(InventoryItem.objects.count(), 2)
|
||||
item = InventoryItem.objects.get(id=1)
|
||||
self.assertEqual(item.availability_policy, 'sell')
|
||||
self.assertEqual(item.availability_policy, 'friends')
|
||||
self.assertEqual(item.category, None)
|
||||
self.assertEqual(item.name, 'test4')
|
||||
self.assertEqual(item.description, 'new description')
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
from authentication.tests import SignatureAuthClient, UserTestMixin, ToolshedTestCase
|
||||
from files.tests import FilesTestMixin
|
||||
from toolshed.models import InventoryItem, Category
|
||||
from toolshed.tests import InventoryTestMixin, LocationTestMixin
|
||||
|
||||
client = SignatureAuthClient()
|
||||
|
||||
|
||||
class LocationApiTestCase(UserTestMixin, InventoryTestMixin, LocationTestMixin, ToolshedTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.prepare_users()
|
||||
self.prepare_categories()
|
||||
self.prepare_tags()
|
||||
self.prepare_properties()
|
||||
self.prepare_locations()
|
||||
self.prepare_inventory()
|
||||
|
||||
def test_locations(self):
|
||||
self.assertEqual("loc1", str(self.f['loc1']))
|
||||
self.assertEqual("loc1", self.f['loc1'].name)
|
||||
self.assertEqual("loc2", str(self.f['loc2']))
|
||||
self.assertEqual("loc2", self.f['loc2'].name)
|
||||
self.assertEqual("loc1/loc3", str(self.f['loc3']))
|
||||
self.assertEqual("loc3", self.f['loc3'].name)
|
||||
self.assertEqual(self.f['loc1'], self.f['loc3'].parent)
|
||||
self.assertEqual("loc1/loc4", str(self.f['loc4']))
|
||||
self.assertEqual("loc4", self.f['loc4'].name)
|
||||
self.assertEqual(self.f['loc1'], self.f['loc4'].parent)
|
||||
|
||||
def test_get_inventory(self):
|
||||
reply = client.get('/api/inventory_items/', self.f['local_user1'])
|
||||
self.assertEqual(reply.status_code, 200)
|
||||
self.assertEqual(len(reply.json()), 2)
|
||||
self.assertEqual(reply.json()[0]['name'], 'test1')
|
||||
self.assertEqual(reply.json()[0]['description'], 'test')
|
||||
self.assertEqual(reply.json()[0]['owned_quantity'], 1)
|
||||
self.assertEqual(reply.json()[0]['tags'], [])
|
||||
self.assertEqual(reply.json()[0]['properties'], [])
|
||||
self.assertEqual(reply.json()[0]['category'], 'cat1')
|
||||
self.assertEqual(reply.json()[0]['availability_policy'], 'friends')
|
||||
self.assertEqual(reply.json()[1]['name'], 'test2')
|
||||
self.assertEqual(reply.json()[1]['description'], 'test2')
|
||||
self.assertEqual(reply.json()[1]['owned_quantity'], 1)
|
||||
self.assertEqual(reply.json()[1]['tags'], ['tag1', 'tag2'])
|
||||
self.assertEqual(reply.json()[1]['properties'],
|
||||
[{'name': 'prop1', 'value': 'value1'}, {'name': 'prop2', 'value': 'value2'}])
|
||||
self.assertEqual(reply.json()[1]['category'], 'cat1')
|
||||
self.assertEqual(reply.json()[1]['availability_policy'], 'friends')
|
||||
|
||||
def test_get_inventory_item(self):
|
||||
reply = client.get('/api/storage_locations/', self.f['local_user1'])
|
||||
self.assertEqual(reply.status_code, 200)
|
||||
self.assertEqual(len(reply.json()), 4)
|
||||
self.assertEqual(reply.json()[0]['name'], 'loc1')
|
||||
self.assertEqual(reply.json()[0]['description'], None)
|
||||
self.assertEqual(reply.json()[0]['category'], None)
|
||||
self.assertEqual(reply.json()[0]['path'], 'loc1')
|
||||
self.assertEqual(reply.json()[1]['name'], 'loc2')
|
||||
self.assertEqual(reply.json()[1]['description'], None)
|
||||
self.assertEqual(reply.json()[1]['category'], 'cat1')
|
||||
self.assertEqual(reply.json()[1]['path'], 'loc2')
|
||||
self.assertEqual(reply.json()[2]['name'], 'loc3')
|
||||
self.assertEqual(reply.json()[2]['description'], None)
|
||||
self.assertEqual(reply.json()[2]['category'], None)
|
||||
self.assertEqual(reply.json()[2]['path'], 'loc1/loc3')
|
||||
self.assertEqual(reply.json()[3]['name'], 'loc4')
|
||||
self.assertEqual(reply.json()[3]['description'], None)
|
||||
self.assertEqual(reply.json()[3]['category'], 'cat1')
|
||||
self.assertEqual(reply.json()[3]['path'], 'loc1/loc4')
|
|
@ -1,16 +0,0 @@
|
|||
# Use an official Python runtime as instance_a parent image
|
||||
FROM python:3.9
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
# Set work directory
|
||||
WORKDIR /code
|
||||
|
||||
# Install dependencies
|
||||
COPY requirements.txt /code/
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Run the application
|
||||
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000", "--insecure"]
|
|
@ -1,16 +0,0 @@
|
|||
# Use an official Python runtime as instance_a parent image
|
||||
FROM python:3.9
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
# Set work directory
|
||||
WORKDIR /dns
|
||||
|
||||
COPY dns_server.py /dns/
|
||||
|
||||
RUN pip install dnslib
|
||||
|
||||
# Run the application
|
||||
CMD ["python", "dns_server.py"]
|
|
@ -1,13 +0,0 @@
|
|||
# Use an official Node.js runtime as instance_a parent image
|
||||
FROM node:14
|
||||
|
||||
# Set work directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install app dependencies
|
||||
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
||||
COPY package.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
CMD [ "npm", "run", "dev", "--", "--host"]
|
|
@ -1,14 +0,0 @@
|
|||
FROM nginx:bookworm
|
||||
|
||||
# snakeoil for localhost
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y openssl && \
|
||||
openssl genrsa -des3 -passout pass:x -out server.pass.key 2048 && \
|
||||
openssl rsa -passin pass:x -in server.pass.key -out server.key && \
|
||||
rm server.pass.key && \
|
||||
openssl req -new -key server.key -out server.csr \
|
||||
-subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=localhost" && \
|
||||
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt &&\
|
||||
mv server.crt /etc/nginx/nginx.crt && \
|
||||
mv server.key /etc/nginx/nginx.key \
|
|
@ -1,15 +0,0 @@
|
|||
# Use an official Python runtime as instance_a parent image
|
||||
FROM python:3.9
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
# Set work directory
|
||||
WORKDIR /wiki
|
||||
|
||||
# Install dependencies
|
||||
RUN pip install --no-cache-dir mkdocs
|
||||
|
||||
# Run the application
|
||||
CMD ["mkdocs", "serve", "--dev-addr=0.0.0.0:8001"]
|
|
@ -1,72 +0,0 @@
|
|||
import http.server
|
||||
import socketserver
|
||||
import urllib.parse
|
||||
import dnslib
|
||||
import base64
|
||||
|
||||
try:
|
||||
|
||||
def resolve(zone, qname, qtype):
|
||||
for record in zone:
|
||||
if record["name"] == qname and record["type"] == qtype and "value" in record:
|
||||
return record["value"]
|
||||
|
||||
|
||||
class DnsHttpRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
try:
|
||||
with open("/dns/zone.json", "r") as f:
|
||||
import json
|
||||
zone = json.load(f)
|
||||
|
||||
url = urllib.parse.urlparse(self.path)
|
||||
if url.path != "/dns-query":
|
||||
self.send_response(404)
|
||||
return
|
||||
query = urllib.parse.parse_qs(url.query)
|
||||
if "dns" not in query:
|
||||
self.send_response(400)
|
||||
return
|
||||
query_base64 = query["dns"][0]
|
||||
padded = query_base64 + "=" * (4 - len(query_base64) % 4)
|
||||
raw = base64.b64decode(padded)
|
||||
dns = dnslib.DNSRecord.parse(raw)
|
||||
|
||||
response = dnslib.DNSRecord(dnslib.DNSHeader(id=dns.header.id, qr=1, aa=1, ra=1), q=dns.q)
|
||||
|
||||
record = resolve(zone, dns.q.qname, dnslib.QTYPE[dns.q.qtype])
|
||||
if record:
|
||||
if dns.q.qtype == dnslib.QTYPE.SRV:
|
||||
print("SRV record")
|
||||
reply = dnslib.SRV(record["priority"], record["weight"], record["port"], record["target"])
|
||||
response.add_answer(dnslib.RR(dns.q.qname, dns.q.qtype, rdata=reply))
|
||||
else:
|
||||
response.header.rcode = dnslib.RCODE.NXDOMAIN
|
||||
|
||||
print(response)
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "application/dns-message")
|
||||
self.end_headers()
|
||||
pack = response.pack()
|
||||
self.wfile.write(pack)
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
self.send_response(500)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
self.wfile.write(b"Internal Server Error")
|
||||
|
||||
|
||||
handler_object = DnsHttpRequestHandler
|
||||
|
||||
PORT = 8053
|
||||
my_server = socketserver.TCPServer(("", PORT), handler_object)
|
||||
|
||||
# Start the server
|
||||
print(f"Starting server on port {PORT}")
|
||||
my_server.serve_forever()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG=True
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY='e*lm&*!j0_stqaiod$1zob(vs@aq6+n-i$1%!rek)_v9n^ue$3'
|
||||
|
||||
ALLOWED_HOSTS="*"
|
|
@ -1,3 +0,0 @@
|
|||
[
|
||||
"127.0.0.3:5353"
|
||||
]
|
|
@ -1,3 +0,0 @@
|
|||
[
|
||||
"a.localhost"
|
||||
]
|
|
@ -1,96 +0,0 @@
|
|||
events {}
|
||||
|
||||
http {
|
||||
upstream backend {
|
||||
server backend-a:8000;
|
||||
}
|
||||
|
||||
upstream frontend {
|
||||
server frontend:5173;
|
||||
}
|
||||
|
||||
upstream wiki {
|
||||
server wiki:8001;
|
||||
}
|
||||
|
||||
upstream dns {
|
||||
server dns:8053;
|
||||
}
|
||||
|
||||
server {
|
||||
|
||||
listen 8080 ssl;
|
||||
server_name localhost;
|
||||
|
||||
ssl_certificate /etc/nginx/nginx.crt;
|
||||
ssl_certificate_key /etc/nginx/nginx.key;
|
||||
|
||||
location /api {
|
||||
proxy_set_header Host $host:$server_port;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host:$server_port;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_pass http://backend;
|
||||
}
|
||||
|
||||
location /auth {
|
||||
proxy_set_header Host $host:$server_port;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host:$server_port;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_pass http://backend;
|
||||
}
|
||||
|
||||
location /docs {
|
||||
proxy_pass http://backend/docs;
|
||||
}
|
||||
|
||||
location /static {
|
||||
proxy_pass http://backend/static;
|
||||
}
|
||||
|
||||
location /wiki {
|
||||
proxy_pass http://wiki/wiki;
|
||||
}
|
||||
|
||||
location /livereload {
|
||||
proxy_pass http://wiki/livereload;
|
||||
}
|
||||
|
||||
location /local/ {
|
||||
alias /var/www/;
|
||||
try_files $uri.json =404;
|
||||
add_header Content-Type application/json;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_pass http://frontend;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
# DoH server
|
||||
server {
|
||||
listen 5353 ssl;
|
||||
server_name localhost;
|
||||
|
||||
ssl_certificate /etc/nginx/nginx.crt;
|
||||
ssl_certificate_key /etc/nginx/nginx.key;
|
||||
|
||||
location /dns-query {
|
||||
proxy_pass http://dns;
|
||||
# allow any origin
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG=True
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY='7ccxjje%q@@0*z+r&-$fy3(rj9n)%$!sk-k++-&rb=_u(wpjbe'
|
||||
|
||||
ALLOWED_HOSTS="*"
|
|
@ -1,46 +0,0 @@
|
|||
events {}
|
||||
|
||||
http {
|
||||
upstream backend {
|
||||
server backend-b:8000;
|
||||
}
|
||||
|
||||
server {
|
||||
|
||||
listen 8080 ssl;
|
||||
server_name localhost;
|
||||
|
||||
ssl_certificate /etc/nginx/nginx.crt;
|
||||
ssl_certificate_key /etc/nginx/nginx.key;
|
||||
|
||||
location /api {
|
||||
#proxy_set_header X-Forwarded-For "$http_x_forwarded_for, $realip_remote_addr";
|
||||
proxy_set_header Host $host:$server_port;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host:$server_port;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_pass http://backend;
|
||||
}
|
||||
|
||||
location /auth {
|
||||
#proxy_set_header X-Forwarded-For "$http_x_forwarded_for, $realip_remote_addr";
|
||||
proxy_set_header Host $host:$server_port;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host:$server_port;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_pass http://backend;
|
||||
}
|
||||
|
||||
location /docs {
|
||||
proxy_pass http://backend/docs;
|
||||
}
|
||||
|
||||
location /static {
|
||||
proxy_pass http://backend/static;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
[
|
||||
{
|
||||
"name": "_toolshed-server._tcp.a.localhost.",
|
||||
"type": "SRV",
|
||||
"ttl": 60,
|
||||
"value": {
|
||||
"priority": 0,
|
||||
"weight": 5,
|
||||
"port": 8080,
|
||||
"target": "127.0.0.1."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "_toolshed-server._tcp.b.localhost.",
|
||||
"type": "SRV",
|
||||
"ttl": 60,
|
||||
"value": {
|
||||
"priority": 0,
|
||||
"weight": 5,
|
||||
"port": 8080,
|
||||
"target": "127.0.0.2."
|
||||
}
|
||||
}
|
||||
]
|
|
@ -1,78 +0,0 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend-a:
|
||||
build:
|
||||
context: ../backend/
|
||||
dockerfile: ../deploy/dev/Dockerfile.backend
|
||||
volumes:
|
||||
- ../backend:/code
|
||||
- ../deploy/dev/instance_a/a.env:/code/.env
|
||||
- ../deploy/dev/instance_a/a.sqlite3:/code/db.sqlite3
|
||||
expose:
|
||||
- 8000
|
||||
command: bash -c "python configure.py; python configure.py testdata; python manage.py runserver 0.0.0.0:8000 --insecure"
|
||||
|
||||
backend-b:
|
||||
build:
|
||||
context: ../backend/
|
||||
dockerfile: ../deploy/dev/Dockerfile.backend
|
||||
volumes:
|
||||
- ../backend:/code
|
||||
- ../deploy/dev/instance_b/b.env:/code/.env
|
||||
- ../deploy/dev/instance_b/b.sqlite3:/code/db.sqlite3
|
||||
expose:
|
||||
- 8000
|
||||
command: bash -c "python configure.py; python configure.py testdata; python manage.py runserver 0.0.0.0:8000 --insecure"
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ../frontend/
|
||||
dockerfile: ../deploy/dev/Dockerfile.frontend
|
||||
volumes:
|
||||
- ../frontend:/app:ro
|
||||
- /app/node_modules
|
||||
expose:
|
||||
- 5173
|
||||
command: npm run dev -- --host
|
||||
|
||||
wiki:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: deploy/dev/Dockerfile.wiki
|
||||
volumes:
|
||||
- ../mkdocs.yml:/wiki/mkdocs.yml
|
||||
- ../docs:/wiki/docs
|
||||
expose:
|
||||
- 8001
|
||||
command: mkdocs serve --dev-addr=0.0.0.0:8001
|
||||
|
||||
proxy-a:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: dev/Dockerfile.proxy
|
||||
volumes:
|
||||
- ./dev/instance_a/nginx-a.dev.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./dev/instance_a/dns.json:/var/www/dns.json:ro
|
||||
- ./dev/instance_a/domains.json:/var/www/domains.json:ro
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
- "127.0.0.3:5353:5353"
|
||||
|
||||
proxy-b:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: dev/Dockerfile.proxy
|
||||
volumes:
|
||||
- ./dev/instance_b/nginx-b.dev.conf:/etc/nginx/nginx.conf:ro
|
||||
ports:
|
||||
- "127.0.0.2:8080:8080"
|
||||
|
||||
dns:
|
||||
build:
|
||||
context: ./dev/
|
||||
dockerfile: Dockerfile.dns
|
||||
volumes:
|
||||
- ./dev/zone.json:/dns/zone.json
|
||||
expose:
|
||||
- 8053
|
43
docker-compose.yml
Normal file
|
@ -0,0 +1,43 @@
|
|||
version: '3.3'
|
||||
|
||||
services:
|
||||
# db:
|
||||
# image: postgres
|
||||
# container_name: docker-django-vue-db
|
||||
# environment:
|
||||
# POSTGRES_USER: user
|
||||
# POSTGRES_PASSWORD: pass
|
||||
# POSTGRES_DB: db
|
||||
# restart: unless-stopped
|
||||
# ports:
|
||||
# - "5432:5432"
|
||||
django:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: ./Dockerfile
|
||||
command: python backend/manage.py runserver 0.0.0.0:8000
|
||||
volumes:
|
||||
- .:/app
|
||||
ports:
|
||||
- "8002:8000"
|
||||
networks:
|
||||
- internal
|
||||
# depends_on:
|
||||
# - db
|
||||
vue:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: ./Dockerfile
|
||||
command: nginx -g 'daemon off;'
|
||||
volumes:
|
||||
- .:/app
|
||||
ports:
|
||||
- "8001:80"
|
||||
networks:
|
||||
- internal
|
||||
- external
|
||||
depends_on:
|
||||
- django
|
||||
networks:
|
||||
external:
|
||||
internal:
|
|
@ -1,102 +0,0 @@
|
|||
# Deployment
|
||||
|
||||
## Native
|
||||
|
||||
### Requirements
|
||||
|
||||
- python3
|
||||
- python3-pip
|
||||
- python3-venv
|
||||
- wget
|
||||
- unzip
|
||||
- nginx
|
||||
- uwsgi
|
||||
- certbot
|
||||
|
||||
### Installation
|
||||
|
||||
Get the latest release:
|
||||
|
||||
``` bash
|
||||
cd /var/www # or wherever you want to install toolshed
|
||||
wget https://git.neulandlabor.de/j3d1/toolshed/releases/download/<version>/toolshed.zip
|
||||
```
|
||||
or from github:
|
||||
``` bash
|
||||
cd /var/www # or wherever you want to install toolshed
|
||||
wget https://github.com/gr4yj3d1/toolshed/archive/refs/tags/<version>.zip -O toolshed.zip
|
||||
```
|
||||
|
||||
Extract and configure the backend:
|
||||
|
||||
``` bash
|
||||
unzip toolshed.zip
|
||||
cd toolshed/backend
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python configure.py
|
||||
```
|
||||
|
||||
Configure uWSGI to serve the backend locally:
|
||||
|
||||
``` bash
|
||||
cd /var/www/toolshed/backend
|
||||
cp toolshed.ini /etc/uwsgi/apps-available/
|
||||
ln -s /etc/uwsgi/apps-available/toolshed.ini /etc/uwsgi/apps-enabled/
|
||||
systemctl restart uwsgi
|
||||
```
|
||||
|
||||
Configure nginx to serve the static files and proxy the requests to the backend:
|
||||
|
||||
``` bash
|
||||
cd /var/www/toolshed/backend
|
||||
cp toolshed.nginx /etc/nginx/sites-available/toolshed
|
||||
ln -s /etc/nginx/sites-available/toolsheed /etc/nginx/sites-enabled/
|
||||
systemctl restart nginx
|
||||
```
|
||||
|
||||
Configure certbot to get a certificate for the domain:
|
||||
|
||||
``` bash
|
||||
certbot --nginx -d <domain>
|
||||
```
|
||||
|
||||
### Update
|
||||
|
||||
``` bash
|
||||
cd /var/www
|
||||
wget https://git.neulandlabor.de/j3d1/toolshed/releases/download/<version>/toolshed.zip
|
||||
unzip toolshed.zip
|
||||
cd toolshed/backend
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python configure.py
|
||||
systemctl restart uwsgi
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
### Requirements
|
||||
|
||||
- docker
|
||||
- docker-compose
|
||||
- git
|
||||
|
||||
### Installation
|
||||
|
||||
``` bash
|
||||
git clone https://git.neulandlabor.de/j3d1/toolshed.git
|
||||
# or
|
||||
git clone https://github.com/gr4yj3d1/toolshed.git
|
||||
cd toolshed
|
||||
docker-compose -f deploy/docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
### Update
|
||||
|
||||
``` bash
|
||||
toolshed
|
||||
git pull
|
||||
docker-compose -f deploy/docker-compose.prod.yml up -d --build
|
||||
```
|
|
@ -1,105 +0,0 @@
|
|||
# Development
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/gr4yj3d1/toolshed.git
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
``` bash
|
||||
git clone https://git.neulandlabor.de/j3d1/toolshed.git
|
||||
```
|
||||
|
||||
## Native
|
||||
|
||||
To a certain extent, the frontend and backend can be developed independently. The frontend is a Vue.js project and the
|
||||
backend is a DRF (Django-Rest-Framework) project. If you want to develop the frontend, you can do so without the backend
|
||||
and vice
|
||||
versa. However, especially for the frontend, it is recommended to use the backend as well, as the frontend does not have
|
||||
a lot of 'offline' functionality.
|
||||
If you want to run the fullstack application, it is recommended to use the [docker-compose](#docker) method.
|
||||
|
||||
### Frontend
|
||||
|
||||
install `node.js` and `npm`
|
||||
|
||||
on Debian* for example: `sudo apt install npm`
|
||||
|
||||
``` bash
|
||||
cd toolshed/frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Backend
|
||||
|
||||
Install `python3`, `pip` and `virtualenv`
|
||||
|
||||
on Debian* for example: `sudo apt install python3 python3-pip python3-venv`
|
||||
|
||||
Prepare backend environment
|
||||
|
||||
``` bash
|
||||
cd toolshed/backend
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Run the test suite:
|
||||
|
||||
``` bash
|
||||
python manage.py test
|
||||
```
|
||||
|
||||
optionally with coverage:
|
||||
|
||||
``` bash
|
||||
coverage run manage.py test
|
||||
coverage report
|
||||
```
|
||||
|
||||
Start the backend in development mode:
|
||||
|
||||
``` bash
|
||||
python manage.py migrate
|
||||
cp .env.dist .env
|
||||
echo "DEBUG = True" >> .env
|
||||
python manage.py runserver 0.0.0.0:8000
|
||||
```
|
||||
|
||||
provides the api docs at `http://localhost:8000/docs/`
|
||||
|
||||
### Docs (Wiki)
|
||||
|
||||
Install `mkdocs`
|
||||
|
||||
on Debian* for example: `sudo apt install mkdocs`
|
||||
|
||||
Start the docs server:
|
||||
|
||||
``` bash
|
||||
cd toolshed/docs
|
||||
mkdocs serve -a 0.0.0.0:8080
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
### Fullstack
|
||||
|
||||
Install `docker` and `docker-compose`
|
||||
|
||||
on Debian* for example: `sudo apt install docker.io docker-compose`
|
||||
|
||||
Start the fullstack application:
|
||||
|
||||
``` bash
|
||||
docker-compose -f deploy/docker-compose.override.yml up --build
|
||||
```
|
||||
|
||||
This will start an instance of the frontend and wiki, a limited DoH (DNS over HTTPS) server and **two** instances of the backend.
|
||||
The two backend instances are set up to use the domains `a.localhost` and `b.localhost`, the local DoH
|
||||
server is used to direct the frontend to the correct backend instance.
|
||||
The frontend is configured to act as if it was served from the domain `a.localhost`.
|
||||
Access the frontend at `http://localhost:8080/`, backend at `http://localhost:8080/api/`, api docs
|
||||
at `http://localhost:8080/docs/` and the wiki at `http://localhost:8080/wiki/`.
|
|
@ -1,23 +0,0 @@
|
|||
# Federation
|
||||
|
||||
This section will cover how federation works in Toolshed.
|
||||
|
||||
## What is Federation?
|
||||
|
||||
Since user of Toolshed you can search and interact the inventory of all their 'friends' that are potentially on
|
||||
different servers there is a need for a way to communicate between servers. We don't want to rely on a central server that
|
||||
stores all the data and we don't want to have a central server that handles all the communication between servers. This
|
||||
is where federation comes in. Toolshed uses a protocol that can not only exchange data with the server where the user
|
||||
is registered but also with the servers where their friends are registered.
|
||||
|
||||
## How does it work?
|
||||
|
||||
Any user can register on any server and creates a personal key pair. The public key is stored on the server and the private
|
||||
key is stored on the client. The private key is used to sign all requests to the server and the public key is used to
|
||||
verify the signature. Once a user has registered on a server they can send friend requests to other users containing
|
||||
their public key. If the other user accepts the friend request, the server stores the public key of the friend and
|
||||
uses it to verify access to the friend's inventory. While accepting a friend request the user also automatically sends
|
||||
their own public key to the friend's server. This way both users can access each other's inventory.
|
||||
|
||||
The protocol is based on a simple HTTPS API exchanging JSON data that is signed with the user's private key. By default
|
||||
Toolshed servers provide a documentation of the API at [/docs/api](/docs/api).
|
|
@ -6,8 +6,47 @@ This is the documentation for the Toolshed project. It is a work in progress.
|
|||
`#social` `#network` `#federation` `#decentralized` `#federated` `#socialnetwork` `#fediverse` `#community` `#hashtags`
|
||||
|
||||
## Getting Started
|
||||
- [Deploying Toolshed](deployment.md)
|
||||
- [Development Setup](development.md)
|
||||
- [About Federation](federation.md)
|
||||
|
||||
## Installation
|
||||
|
||||
``` bash
|
||||
# TODO add installation instructions
|
||||
# similar to development instructions just with more docker
|
||||
# TODO add docker-compose.yml
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/gr4yj3d1/toolshed.git
|
||||
```
|
||||
or
|
||||
``` bash
|
||||
git clone https://git.neulandlabor.de/j3d1/toolshed.git
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
``` bash
|
||||
cd toolshed/frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Backend
|
||||
|
||||
``` bash
|
||||
cd toolshed/backend
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python manage.py migrate
|
||||
python manage.py runserver 0.0.0.0:8000
|
||||
```
|
||||
|
||||
### Docs
|
||||
|
||||
``` bash
|
||||
cd toolshed/docs
|
||||
mkdocs serve -a 0.0.0.0:8080
|
||||
```
|
||||
|
|
14
frontend/Dockerfile
Normal file
|
@ -0,0 +1,14 @@
|
|||
FROM node:alpine as builder
|
||||
WORKDIR /app
|
||||
COPY ./package.json /app/package.json
|
||||
COPY . /app
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
|
||||
FROM nginx:alpine as runner
|
||||
RUN apk add --update npm
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY ./nginx.conf /etc/nginx/nginx.conf
|
||||
EXPOSE 80
|
87
frontend/fullchain.pem
Normal file
|
@ -0,0 +1,87 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIEXTCCA0WgAwIBAgISBEHk0Sh8UrMfT+VPkRhr83mfMA0GCSqGSIb3DQEBCwUA
|
||||
MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
|
||||
EwJSMzAeFw0yMzA2MDIxNzI1MDVaFw0yMzA4MzExNzI1MDRaMBsxGTAXBgNVBAMT
|
||||
EHRvb2xzaGVkLmozZDEuZGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARYeMpT
|
||||
k1DaC8cigL3DivanGrLQahYBEDm5B26VaS3gUmq9T0RNkEUxJIPnZBwdF8p7xAEB
|
||||
hlTXwgy3eBLAp8lAo4ICTTCCAkkwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQG
|
||||
CCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTRtqOn
|
||||
Qht7rD+2IEGzuYt8frTgVjAfBgNVHSMEGDAWgBQULrMXt1hWy65QCUDmH6+dixTC
|
||||
xjBVBggrBgEFBQcBAQRJMEcwIQYIKwYBBQUHMAGGFWh0dHA6Ly9yMy5vLmxlbmNy
|
||||
Lm9yZzAiBggrBgEFBQcwAoYWaHR0cDovL3IzLmkubGVuY3Iub3JnLzAbBgNVHREE
|
||||
FDASghB0b29sc2hlZC5qM2QxLmRlMEwGA1UdIARFMEMwCAYGZ4EMAQIBMDcGCysG
|
||||
AQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQu
|
||||
b3JnMIIBBgYKKwYBBAHWeQIEAgSB9wSB9ADyAHcAtz77JN+cTbp18jnFulj0bF38
|
||||
Qs96nzXEnh0JgSXttJkAAAGIfVskxgAABAMASDBGAiEA+D8rCaCpttJm7w0M4N5N
|
||||
3cmJSfPNmh/t2ojaDB0iSe0CIQCS2XkwJzUrDZ35fIJ9evwJduk/K2I/tmWs4Uk5
|
||||
vnPSNQB3AK33vvp8/xDIi509nB4+GGq0Zyldz7EMJMqFhjTr3IKKAAABiH1bJOoA
|
||||
AAQDAEgwRgIhAPHQQwLf5xSi1VH6BeOpiUKyTMawd36FFU8eCIdB43q6AiEA0KDD
|
||||
yRssPcmGnyWDGq9Of3mpKjCChFrnxzpeDXCTlsswDQYJKoZIhvcNAQELBQADggEB
|
||||
AKo8APReSKNTydks9yqASKhUjuLfXS+mpFQSl2tbU8ER6eIiYHx8o+n2QCdT7h91
|
||||
ZLkGx8ZAmWBvVwXC3QPH5W08ilogi4EU/+HGffkditG5K6/Qn2bzjqnmFIyYqgdT
|
||||
RVaRcxqS9byAGEw3oU5FSCIOuFSBeOHeTwaj+lSVMZTv6LmoovOpCo8sA5xZ6K6H
|
||||
XVwNXIwunssaR4MrnWupB/5N+T7zkhanky4GgiLRuTm+mDbK+OIDx47Hv9jTe+tm
|
||||
s4aixD0eWhzAaiA7HuHJI3Xi64YjK7eNlrwE0ZKdgy8KveDwUBiVcVtz7LR+0v1l
|
||||
P27Z/OkZlA+42LvIJdISMl4=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw
|
||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw
|
||||
WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
|
||||
RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
|
||||
AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP
|
||||
R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx
|
||||
sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm
|
||||
NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg
|
||||
Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG
|
||||
/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC
|
||||
AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB
|
||||
Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA
|
||||
FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw
|
||||
AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw
|
||||
Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB
|
||||
gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W
|
||||
PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl
|
||||
ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz
|
||||
CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm
|
||||
lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4
|
||||
avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2
|
||||
yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O
|
||||
yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids
|
||||
hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+
|
||||
HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv
|
||||
MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX
|
||||
nLRbwHOoq7hHwg==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/
|
||||
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
|
||||
DkRTVCBSb290IENBIFgzMB4XDTIxMDEyMDE5MTQwM1oXDTI0MDkzMDE4MTQwM1ow
|
||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwggIiMA0GCSqGSIb3DQEB
|
||||
AQUAA4ICDwAwggIKAoICAQCt6CRz9BQ385ueK1coHIe+3LffOJCMbjzmV6B493XC
|
||||
ov71am72AE8o295ohmxEk7axY/0UEmu/H9LqMZshftEzPLpI9d1537O4/xLxIZpL
|
||||
wYqGcWlKZmZsj348cL+tKSIG8+TA5oCu4kuPt5l+lAOf00eXfJlII1PoOK5PCm+D
|
||||
LtFJV4yAdLbaL9A4jXsDcCEbdfIwPPqPrt3aY6vrFk/CjhFLfs8L6P+1dy70sntK
|
||||
4EwSJQxwjQMpoOFTJOwT2e4ZvxCzSow/iaNhUd6shweU9GNx7C7ib1uYgeGJXDR5
|
||||
bHbvO5BieebbpJovJsXQEOEO3tkQjhb7t/eo98flAgeYjzYIlefiN5YNNnWe+w5y
|
||||
sR2bvAP5SQXYgd0FtCrWQemsAXaVCg/Y39W9Eh81LygXbNKYwagJZHduRze6zqxZ
|
||||
Xmidf3LWicUGQSk+WT7dJvUkyRGnWqNMQB9GoZm1pzpRboY7nn1ypxIFeFntPlF4
|
||||
FQsDj43QLwWyPntKHEtzBRL8xurgUBN8Q5N0s8p0544fAQjQMNRbcTa0B7rBMDBc
|
||||
SLeCO5imfWCKoqMpgsy6vYMEG6KDA0Gh1gXxG8K28Kh8hjtGqEgqiNx2mna/H2ql
|
||||
PRmP6zjzZN7IKw0KKP/32+IVQtQi0Cdd4Xn+GOdwiK1O5tmLOsbdJ1Fu/7xk9TND
|
||||
TwIDAQABo4IBRjCCAUIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw
|
||||
SwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5pZGVudHJ1
|
||||
c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTEp7Gkeyxx
|
||||
+tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEB
|
||||
ATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQu
|
||||
b3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0LmNvbS9E
|
||||
U1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFHm0WeZ7tuXkAXOACIjIGlj26Ztu
|
||||
MA0GCSqGSIb3DQEBCwUAA4IBAQAKcwBslm7/DlLQrt2M51oGrS+o44+/yQoDFVDC
|
||||
5WxCu2+b9LRPwkSICHXM6webFGJueN7sJ7o5XPWioW5WlHAQU7G75K/QosMrAdSW
|
||||
9MUgNTP52GE24HGNtLi1qoJFlcDyqSMo59ahy2cI2qBDLKobkx/J3vWraV0T9VuG
|
||||
WCLKTVXkcGdtwlfFRjlBz4pYg1htmf5X6DYO8A4jqv2Il9DjXA6USbW1FzXSLr9O
|
||||
he8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC
|
||||
Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5
|
||||
-----END CERTIFICATE-----
|
14
frontend/nginx.conf
Normal file
|
@ -0,0 +1,14 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://django:8000;
|
||||
}
|
||||
|
||||
}
|
1432
frontend/package-lock.json
generated
|
@ -8,7 +8,6 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^4.6.2",
|
||||
"bootstrap-icons-vue": "^1.10.3",
|
||||
"dns-query": "^0.11.2",
|
||||
"js-nacl": "^1.4.0",
|
||||
|
@ -22,7 +21,7 @@
|
|||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vue/test-utils": "^2.3.2",
|
||||
"jsdom": "^22.0.0",
|
||||
"sass": "^1.72.0",
|
||||
"sass": "^1.62.1",
|
||||
"vite": "^4.1.4",
|
||||
"vitest": "^0.31.1"
|
||||
}
|
||||
|
|
5
frontend/privkey.pem
Normal file
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgD6EmCAUWob1zUw4Q
|
||||
F+Pf9cSOmSCTODe6u+Gst177IoihRANCAARYeMpTk1DaC8cigL3DivanGrLQahYB
|
||||
EDm5B26VaS3gUmq9T0RNkEUxJIPnZBwdF8p7xAEBhlTXwgy3eBLAp8lA
|
||||
-----END PRIVATE KEY-----
|
BIN
frontend/public/assets/img/avatars/avatar-2.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
frontend/public/assets/img/avatars/avatar-3.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/public/assets/img/avatars/avatar-4.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
frontend/public/assets/img/avatars/avatar-5.png
Normal file
After Width: | Height: | Size: 1 MiB |
BIN
frontend/public/assets/img/avatars/avatar-6.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/public/assets/img/avatars/avatar.png
Normal file
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/public/assets/img/photos/unsplash-1.jpg
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/public/assets/img/photos/unsplash-2.jpg
Normal file
After Width: | Height: | Size: 888 KiB |
BIN
frontend/public/assets/img/photos/unsplash-3.jpg
Normal file
After Width: | Height: | Size: 970 KiB |
|
@ -3,13 +3,23 @@
|
|||
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
<!-- TODO UI für Freunde liste, add, remove -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
import {mapMutations} from 'vuex';
|
||||
import store from '@/store';
|
||||
|
||||
export default {
|
||||
name: 'App'
|
||||
name: 'App',
|
||||
methods: {
|
||||
...mapMutations(['init']),
|
||||
},
|
||||
beforeCreate () {
|
||||
store.commit('load_local')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|