This commit is contained in:
j3d1 2023-12-11 22:18:33 +01:00
parent 6aaa522a6b
commit ba427c7a84
25 changed files with 274 additions and 236 deletions

View file

@ -1,57 +1,85 @@
from rest_framework import routers, viewsets, serializers from rest_framework import routers, viewsets, serializers, permissions
from rest_framework.decorators import api_view, permission_classes, authentication_classes from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.authentication import BasicAuthentication from django.contrib.auth import login
from django.contrib.auth.models import User
from django.urls import path from django.urls import path
from django.dispatch import receiver
from django.db.models.signals import post_save
from knox.models import AuthToken
from knox.views import LoginView as KnoxLoginView
from rest_framework.authtoken.serializers import AuthTokenSerializer
from authentication.models import ExtendedUser
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = ExtendedUser
fields = ('id', 'username', 'email', 'first_name', 'last_name') fields = ('id', 'username', 'email', 'first_name', 'last_name')
class RegisterUserSerializer(serializers.ModelSerializer): @receiver(post_save, sender=ExtendedUser)
class Meta: def create_auth_token(sender, instance=None, created=False, **kwargs):
model = User if created:
fields = ('username', 'password', 'email') AuthToken.objects.create(user=instance)
extra_kwargs = {
'password': {'write_only': True},
}
class UserViewSet(viewsets.ModelViewSet): class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all() queryset = ExtendedUser.objects.all()
serializer_class = UserSerializer serializer_class = UserSerializer
authentication_classes = [BasicAuthentication]
permission_classes = []
@api_view(['GET']) @api_view(['POST'])
@permission_classes([]) def selfUser(request):
@authentication_classes([BasicAuthentication]) serializer = UserSerializer(request.user)
def token(request): return Response(serializer.data, status=200)
return Response({
'token': request.user.auth_token.key
})
@api_view(['POST']) @api_view(['POST'])
@permission_classes([]) @permission_classes([])
@authentication_classes([]) @authentication_classes([])
def registerUser(request): def registerUser(request):
serializer = RegisterUserSerializer(data=request.data) try:
if serializer.is_valid(): username = request.data.get('username')
user = serializer.save() password = request.data.get('password')
email = request.data.get('email')
errors = {}
if not username:
errors['username'] = 'Username is required'
if not password:
errors['password'] = 'Password is required'
if not email:
errors['email'] = 'Email is required'
if ExtendedUser.objects.filter(email=email).exists():
errors['email'] = 'Email already exists'
if ExtendedUser.objects.filter(username=username).exists():
errors['username'] = 'Username already exists'
if errors:
return Response({'errors': errors}, status=400)
user = ExtendedUser.objects.create_user(username, email, password)
return Response({'username': user.username, 'email': user.email}, status=201) return Response({'username': user.username, 'email': user.email}, status=201)
return Response(serializer.errors, status=400) except Exception as e:
return Response({'errors': str(e)}, status=400)
class LoginView(KnoxLoginView):
permission_classes = (permissions.AllowAny,)
authentication_classes = ()
def post(self, request, format=None):
serializer = AuthTokenSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
login(request, user)
return super(LoginView, self).post(request, format=None)
router = routers.SimpleRouter() router = routers.SimpleRouter()
router.register(r'users', UserViewSet, basename='users') router.register(r'users', UserViewSet, basename='users')
urlpatterns = router.urls + [ urlpatterns = router.urls + [
path('token/', token), path('self/', selfUser),
path('login/', LoginView.as_view()),
path('register/', registerUser), path('register/', registerUser),
] ]

View file

@ -1,35 +0,0 @@
# Generated by Django 4.2.7 on 2023-11-26 00:16
from django.conf import settings
from django.db import migrations
from django.contrib.auth.models import Permission, Group
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('authentication', '0001_initial'),
('inventory', '0003_alter_item_options'),
('tickets', '0003_alter_issuethread_options'),
]
def create_groups(apps, schema_editor):
admins = Group.objects.create(name='Admin')
orga = Group.objects.create(name='Orga')
team = Group.objects.create(name='Team')
users = Group.objects.create(name='User')
admins.permissions.add(*Permission.objects.all())
users.permissions.add(*Permission.objects.filter(codename__in=
['view_item', 'add_item', 'change_item', 'match_item']))
team.permissions.add(*Permission.objects.filter(codename__in=
['delete_item', 'view_issuethread', 'add_issuethread',
'change_issuethread', 'delete_issuethread', 'send_mail']),
*users.permissions.all())
orga.permissions.add(*Permission.objects.filter(codename__in=['add_event']),
*team.permissions.all())
operations = [
migrations.RunPython(create_groups),
]

View file

@ -0,0 +1,34 @@
from django.test import TestCase
from django.contrib.auth.models import Permission
from authentication.models import EventPermission, ExtendedUser
from inventory.models import Event
class PermissionsTestCase(TestCase):
def setUp(self):
super().setUp()
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
event1 = Event.objects.create(slug='testevent1', name='testevent1')
event2 = Event.objects.create(slug='testevent2', name='testevent2')
permission1 = Permission.objects.get(codename='view_event')
EventPermission.objects.create(user=self.user, permission=permission1, event=event1)
EventPermission.objects.create(user=self.user, permission=permission1, event=event2)
def test_user_permissions(self):
"""
Test that a user can only access their own data.
"""
self.client.force_login(self.user)
response = self.client.get('/api/2/users/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 2)
self.assertEqual(response.json()[0]['username'], 'testuser')
self.assertEqual(response.json()[0]['email'], 'test')
self.assertEqual(response.json()[0]['first_name'], '')
self.assertEqual(response.json()[0]['last_name'], '')
self.assertEqual(response.json()[0]['id'], 1)
self.assertEqual(response.json()[1]['username'], 'testuser')
self.assertEqual(response.json()[1]['email'], 'test')
self.assertEqual(response.json()[1]['first_name'], '')
self.assertEqual(response.json()[1]['last_name'], '')

View file

@ -1,18 +1,93 @@
from django.test import TestCase, Client from django.test import TestCase, Client
from knox.models import AuthToken
from authentication.models import ExtendedUser
from core import settings from core import settings
client = Client()
class UserApiTest(TestCase):
class IssueApiTest(TestCase): def setUp(self):
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.save()
self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
def test_issues(self): def test_users(self):
response = client.get('/api/2/users/') response = self.client.get('/api/2/users/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1) self.assertEqual(len(response.json()), 2)
self.assertEqual(response.json()[0]['username'], settings.LEGACY_USER_NAME) self.assertEqual(response.json()[0]['username'], settings.LEGACY_USER_NAME)
self.assertEqual(response.json()[0]['email'], 'mail@' + settings.MAIL_DOMAIN) self.assertEqual(response.json()[0]['email'], 'mail@' + settings.MAIL_DOMAIN)
self.assertEqual(response.json()[0]['first_name'], '') self.assertEqual(response.json()[0]['first_name'], '')
self.assertEqual(response.json()[0]['last_name'], '') self.assertEqual(response.json()[0]['last_name'], '')
self.assertEqual(response.json()[0]['id'], 1) self.assertEqual(response.json()[0]['id'], 1)
self.assertEqual(response.json()[1]['username'], 'testuser')
self.assertEqual(response.json()[1]['email'], 'test')
self.assertEqual(response.json()[1]['first_name'], '')
self.assertEqual(response.json()[1]['last_name'], '')
def test_self_user(self):
response = self.client.post('/api/2/self/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['username'], 'testuser')
self.assertEqual(response.json()['email'], 'test')
self.assertEqual(response.json()['first_name'], '')
self.assertEqual(response.json()['last_name'], '')
def test_register_user(self):
anonymous = Client()
response = anonymous.post('/api/2/register/', {'username': 'testuser2', 'password': 'test', 'email': 'test2'},
content_type='application/json')
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json()['username'], 'testuser2')
self.assertEqual(response.json()['email'], 'test2')
self.assertEqual(len(ExtendedUser.objects.all()), 3)
self.assertEqual(ExtendedUser.objects.get(username='testuser2').email, 'test2')
self.assertTrue(ExtendedUser.objects.get(username='testuser2').check_password('test'))
def test_register_user_duplicate(self):
anonymous = Client()
response = anonymous.post('/api/2/register/', {'username': 'testuser', 'password': 'test', 'email': 'test2'},
content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()['errors']['username'], 'Username already exists')
self.assertEqual(len(ExtendedUser.objects.all()), 2)
def test_register_user_no_username(self):
anonymous = Client()
response = anonymous.post('/api/2/register/', {'password': 'test', 'email': 'test2'},
content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()['errors']['username'], 'Username is required')
self.assertEqual(len(ExtendedUser.objects.all()), 2)
def test_register_user_no_password(self):
anonymous = Client()
response = anonymous.post('/api/2/register/', {'username': 'testuser2', 'email': 'test2'},
content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()['errors']['password'], 'Password is required')
self.assertEqual(len(ExtendedUser.objects.all()), 2)
def test_register_user_no_email(self):
anonymous = Client()
response = anonymous.post('/api/2/register/', {'username': 'testuser2', 'password': 'test'},
content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()['errors']['email'], 'Email is required')
self.assertEqual(len(ExtendedUser.objects.all()), 2)
def test_register_user_duplicate_email(self):
anonymous = Client()
response = anonymous.post('/api/2/register/', {'username': 'testuser2', 'password': 'test', 'email': 'test'},
content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()['errors']['email'], 'Email already exists')
self.assertEqual(len(ExtendedUser.objects.all()), 2)
def test_get_token(self):
anonymous = Client()
response = anonymous.post('/api/2/login/', {'username': 'testuser', 'password': 'test'},
content_type='application/json')

View file

@ -51,7 +51,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django_extensions', 'django_extensions',
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'knox',
'drf_yasg', 'drf_yasg',
'channels', 'channels',
'authentication', 'authentication',
@ -63,9 +63,13 @@ INSTALLED_APPS = [
] ]
REST_FRAMEWORK = { REST_FRAMEWORK = {
'TEST_REQUEST_DEFAULT_FORMAT': 'json' 'TEST_REQUEST_DEFAULT_FORMAT': 'json',
'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthentication',),
'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated',),
} }
AUTH_USER_MODEL = 'authentication.ExtendedUser'
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
'SECURITY_DEFINITIONS': { 'SECURITY_DEFINITIONS': {
'api_key': { 'api_key': {

View file

@ -16,6 +16,8 @@ class FileViewSet(viewsets.ModelViewSet):
serializer_class = FileSerializer serializer_class = FileSerializer
queryset = File.objects.all() queryset = File.objects.all()
lookup_field = 'hash' lookup_field = 'hash'
permission_classes = []
authentication_classes = []
router = routers.SimpleRouter(trailing_slash=False) router = routers.SimpleRouter(trailing_slash=False)

View file

@ -1,4 +1,4 @@
# Generated by Django 4.2.7 on 2023-11-18 11:28 # Generated by Django 4.2.7 on 2023-12-09 02:13
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion

View file

@ -1,22 +1,26 @@
from django.test import TestCase, Client from django.test import TestCase, Client
from authentication.models import ExtendedUser
from files.models import File from files.models import File
from inventory.models import Event, Container, Item from inventory.models import Event, Container, Item
from knox.models import AuthToken
client = Client()
class FileTestCase(TestCase): class FileTestCase(TestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.save()
self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
self.event = Event.objects.create(slug='EVENT', name='Event') self.event = Event.objects.create(slug='EVENT', name='Event')
self.box = Container.objects.create(name='BOX') self.box = Container.objects.create(name='BOX')
def test_list_files(self): def test_list_files(self):
import base64 import base64
item = File.objects.create(data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')) item = File.objects.create(data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
response = client.get('/api/2/files/') response = self.client.get('/api/2/files/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()[0]['hash'], item.hash) self.assertEqual(response.json()[0]['hash'], item.hash)
self.assertEqual(len(response.json()[0]['hash']), 64) self.assertEqual(len(response.json()[0]['hash']), 64)
@ -24,7 +28,7 @@ class FileTestCase(TestCase):
def test_one_file(self): def test_one_file(self):
import base64 import base64
item = File.objects.create(data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')) item = File.objects.create(data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
response = client.get(f'/api/2/files/{item.hash}/') response = self.client.get(f'/api/2/files/{item.hash}/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['hash'], item.hash) self.assertEqual(response.json()['hash'], item.hash)
self.assertEqual(len(response.json()['hash']), 64) self.assertEqual(len(response.json()['hash']), 64)
@ -33,7 +37,7 @@ class FileTestCase(TestCase):
import base64 import base64
Item.objects.create(container=self.box, event=self.event, description='1') Item.objects.create(container=self.box, event=self.event, description='1')
item = Item.objects.create(container=self.box, event=self.event, description='2') item = Item.objects.create(container=self.box, event=self.event, description='2')
response = client.post('/api/2/files/', response = self.client.post('/api/2/files/',
{'data': "data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')}, {'data': "data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')},
content_type='application/json') content_type='application/json')
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
@ -45,5 +49,5 @@ class FileTestCase(TestCase):
File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')) File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
file = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"bar").decode('utf-8')) file = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"bar").decode('utf-8'))
self.assertEqual(len(File.objects.all()), 2) self.assertEqual(len(File.objects.all()), 2)
response = client.delete(f'/api/2/files/{file.hash}/') response = self.client.delete(f'/api/2/files/{file.hash}/')
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, 204)

View file

@ -20,7 +20,6 @@ class EventViewSet(viewsets.ModelViewSet):
serializer_class = EventSerializer serializer_class = EventSerializer
queryset = Event.objects.all() queryset = Event.objects.all()
permission_classes = [] permission_classes = []
authentication_classes = []
class ContainerSerializer(serializers.ModelSerializer): class ContainerSerializer(serializers.ModelSerializer):
@ -39,7 +38,6 @@ class ContainerViewSet(viewsets.ModelViewSet):
serializer_class = ContainerSerializer serializer_class = ContainerSerializer
queryset = Container.objects.all() queryset = Container.objects.all()
permission_classes = [] permission_classes = []
authentication_classes = []
class ItemSerializer(serializers.ModelSerializer): class ItemSerializer(serializers.ModelSerializer):
@ -93,8 +91,6 @@ class ItemSerializer(serializers.ModelSerializer):
@api_view(['GET']) @api_view(['GET'])
@permission_classes([])
@authentication_classes([])
def search_items(request, event_slug, query): def search_items(request, event_slug, query):
try: try:
event = Event.objects.get(slug=event_slug) event = Event.objects.get(slug=event_slug)
@ -109,8 +105,6 @@ def search_items(request, event_slug, query):
@api_view(['GET', 'POST']) @api_view(['GET', 'POST'])
@permission_classes([])
@authentication_classes([])
def item(request, event_slug): def item(request, event_slug):
try: try:
event = Event.objects.get(slug=event_slug) event = Event.objects.get(slug=event_slug)
@ -126,8 +120,6 @@ def item(request, event_slug):
@api_view(['GET', 'PUT', 'DELETE']) @api_view(['GET', 'PUT', 'DELETE'])
@permission_classes([])
@authentication_classes([])
def item_by_id(request, event_slug, id): def item_by_id(request, event_slug, id):
try: try:
event = Event.objects.get(slug=event_slug) event = Event.objects.get(slug=event_slug)

View file

@ -1,4 +1,4 @@
# Generated by Django 4.2.7 on 2023-11-18 11:28 # Generated by Django 4.2.7 on 2023-12-09 02:13
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -15,11 +15,16 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Container', name='Container',
fields=[ fields=[
('is_deleted', models.BooleanField(default=False)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('cid', models.AutoField(primary_key=True, serialize=False)), ('cid', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)), ('name', models.CharField(max_length=255)),
('created_at', models.DateTimeField(blank=True, null=True)), ('created_at', models.DateTimeField(blank=True, null=True)),
('updated_at', models.DateTimeField(blank=True, null=True)), ('updated_at', models.DateTimeField(blank=True, null=True)),
], ],
options={
'abstract': False,
},
), ),
migrations.CreateModel( migrations.CreateModel(
name='Event', name='Event',
@ -38,6 +43,8 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Item', name='Item',
fields=[ fields=[
('is_deleted', models.BooleanField(default=False)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('iid', models.AutoField(primary_key=True, serialize=False)), ('iid', models.AutoField(primary_key=True, serialize=False)),
('uid', models.IntegerField()), ('uid', models.IntegerField()),
('description', models.TextField()), ('description', models.TextField()),
@ -48,6 +55,7 @@ class Migration(migrations.Migration):
('event', models.ForeignKey(db_column='eid', on_delete=django.db.models.deletion.CASCADE, to='inventory.event')), ('event', models.ForeignKey(db_column='eid', on_delete=django.db.models.deletion.CASCADE, to='inventory.event')),
], ],
options={ options={
'permissions': [('match_item', 'Can match item')],
'unique_together': {('uid', 'event')}, 'unique_together': {('uid', 'event')},
}, },
), ),

View file

@ -1,33 +0,0 @@
# Generated by Django 4.2.7 on 2023-11-20 11:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='container',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='container',
name='is_deleted',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='item',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='item',
name='is_deleted',
field=models.BooleanField(default=False),
),
]

View file

@ -1,17 +0,0 @@
# Generated by Django 4.2.7 on 2023-12-06 13:45
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0002_container_deleted_at_container_is_deleted_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='item',
options={'permissions': [('match_item', 'Can match item')]},
),
]

View file

@ -12,8 +12,6 @@ class EmailSerializer(serializers.ModelSerializer):
class EmailViewSet(viewsets.ModelViewSet): class EmailViewSet(viewsets.ModelViewSet):
serializer_class = EmailSerializer serializer_class = EmailSerializer
queryset = Email.objects.all() queryset = Email.objects.all()
permission_classes = []
authentication_classes = []
router = routers.SimpleRouter() router = routers.SimpleRouter()

View file

@ -1,4 +1,4 @@
# Generated by Django 4.2.7 on 2023-12-06 02:52 # Generated by Django 4.2.7 on 2023-12-09 02:13
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -9,8 +9,8 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('inventory', '0001_initial'),
('tickets', '0001_initial'), ('tickets', '0001_initial'),
('inventory', '0002_container_deleted_at_container_is_deleted_and_more'),
] ]
operations = [ operations = [

View file

@ -1,16 +1,16 @@
import inspect import inspect
from unittest import mock from unittest import mock
from knox.models import AuthToken
from django.test import TestCase, Client from django.test import TestCase, Client
from authentication.models import ExtendedUser
from core.settings import MAIL_DOMAIN from core.settings import MAIL_DOMAIN
from inventory.models import Event from inventory.models import Event
from mail.models import Email from mail.models import Email
from mail.protocol import LMTPHandler from mail.protocol import LMTPHandler
from tickets.models import IssueThread from tickets.models import IssueThread
client = Client()
def make_mocked_coro(return_value=mock.sentinel, raise_exception=mock.sentinel): def make_mocked_coro(return_value=mock.sentinel, raise_exception=mock.sentinel):
async def mock_coro(*args, **kwargs): async def mock_coro(*args, **kwargs):
@ -25,6 +25,13 @@ def make_mocked_coro(return_value=mock.sentinel, raise_exception=mock.sentinel):
class EmailsApiTest(TestCase): class EmailsApiTest(TestCase):
def setUp(self):
super().setUp()
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.save()
self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
def test_mails(self): def test_mails(self):
Event.objects.get_or_create( Event.objects.get_or_create(
name="Test event", name="Test event",
@ -36,7 +43,7 @@ class EmailsApiTest(TestCase):
sender='test', sender='test',
recipient='test', recipient='test',
) )
response = client.get('/api/2/mails/') response = self.client.get('/api/2/mails/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1) self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['subject'], 'test') self.assertEqual(response.json()[0]['subject'], 'test')
@ -45,13 +52,20 @@ class EmailsApiTest(TestCase):
self.assertEqual(response.json()[0]['recipient'], 'test') self.assertEqual(response.json()[0]['recipient'], 'test')
def test_mails_empty(self): def test_mails_empty(self):
response = client.get('/api/2/mails/') response = self.client.get('/api/2/mails/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), []) self.assertEqual(response.json(), [])
class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test
def setUp(self):
super().setUp()
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.save()
self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
def test_handle_client(self): def test_handle_client(self):
from aiosmtpd.smtp import Envelope from aiosmtpd.smtp import Envelope
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
@ -156,8 +170,7 @@ class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test
) )
import aiosmtplib import aiosmtplib
aiosmtplib.send = make_mocked_coro() aiosmtplib.send = make_mocked_coro()
client = Client() response = self.client.post(f'/api/2/tickets/{issue_thread.id}/reply/', {
response = client.post(f'/api/2/tickets/{issue_thread.id}/reply/', {
'message': 'test' 'message': 'test'
}) })
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)

View file

@ -13,8 +13,6 @@ class SystemEventSerializer(serializers.ModelSerializer):
class SystemEventViewSet(viewsets.ModelViewSet): class SystemEventViewSet(viewsets.ModelViewSet):
serializer_class = SystemEventSerializer serializer_class = SystemEventSerializer
queryset = SystemEvent.objects.all() queryset = SystemEvent.objects.all()
permission_classes = []
authentication_classes = []
router = routers.SimpleRouter() router = routers.SimpleRouter()

View file

@ -1,4 +1,4 @@
# Generated by Django 4.2.7 on 2023-12-01 18:19 # Generated by Django 4.2.7 on 2023-12-09 02:13
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,10 +1,11 @@
import logging import logging
from django.db import models from django.db import models
from django.contrib.auth.models import User
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from channels.layers import get_channel_layer from channels.layers import get_channel_layer
from authentication.models import ExtendedUser
class SystemEvent(models.Model): class SystemEvent(models.Model):
TYPE_CHOICES = [('ticket_created', 'ticket_created'), TYPE_CHOICES = [('ticket_created', 'ticket_created'),
@ -19,7 +20,7 @@ class SystemEvent(models.Model):
('event_deleted', 'event_deleted'), ] ('event_deleted', 'event_deleted'), ]
id = models.AutoField(primary_key=True) id = models.AutoField(primary_key=True)
timestamp = models.DateTimeField(auto_now_add=True) timestamp = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(User, models.SET_NULL, null=True) user = models.ForeignKey(ExtendedUser, models.SET_NULL, null=True)
type = models.CharField(max_length=255, choices=TYPE_CHOICES) type = models.CharField(max_length=255, choices=TYPE_CHOICES)
reference = models.IntegerField(blank=True, null=True) reference = models.IntegerField(blank=True, null=True)

View file

@ -53,13 +53,9 @@ class IssueSerializer(serializers.ModelSerializer):
class IssueViewSet(viewsets.ModelViewSet): class IssueViewSet(viewsets.ModelViewSet):
serializer_class = IssueSerializer serializer_class = IssueSerializer
queryset = IssueThread.objects.all() queryset = IssueThread.objects.all()
permission_classes = []
authentication_classes = []
@api_view(['POST']) @api_view(['POST'])
@permission_classes([])
@authentication_classes([])
def reply(request, pk): def reply(request, pk):
issue = IssueThread.objects.get(pk=pk) issue = IssueThread.objects.get(pk=pk)
# email = issue.reply(request.data['body']) # TODO evaluate if this is a useful abstraction # email = issue.reply(request.data['body']) # TODO evaluate if this is a useful abstraction

View file

@ -1,4 +1,4 @@
# Generated by Django 4.2.7 on 2023-12-06 02:34 # Generated by Django 4.2.7 on 2023-12-09 02:13
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -18,9 +18,13 @@ class Migration(migrations.Migration):
('is_deleted', models.BooleanField(default=False)), ('is_deleted', models.BooleanField(default=False)),
('deleted_at', models.DateTimeField(blank=True, null=True)), ('deleted_at', models.DateTimeField(blank=True, null=True)),
('id', models.AutoField(primary_key=True, serialize=False)), ('id', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('state', models.CharField(default='new', max_length=255)),
('assigned_to', models.CharField(max_length=255, null=True)),
('last_activity', models.DateTimeField(auto_now=True)),
], ],
options={ options={
'abstract': False, 'permissions': [('send_mail', 'Can send mail')],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
@ -29,7 +33,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(primary_key=True, serialize=False)), ('id', models.AutoField(primary_key=True, serialize=False)),
('state', models.CharField(max_length=255)), ('state', models.CharField(max_length=255)),
('timestamp', models.DateTimeField(auto_now_add=True)), ('timestamp', models.DateTimeField(auto_now_add=True)),
('issue_thread', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tickets.issuethread')), ('issue_thread', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='state_changes', to='tickets.issuethread')),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
@ -38,7 +42,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(primary_key=True, serialize=False)), ('id', models.AutoField(primary_key=True, serialize=False)),
('comment', models.TextField()), ('comment', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True)), ('timestamp', models.DateTimeField(auto_now_add=True)),
('issue_thread', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tickets.issuethread')), ('issue_thread', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='tickets.issuethread')),
], ],
), ),
] ]

View file

@ -1,45 +0,0 @@
# Generated by Django 4.2.7 on 2023-12-06 03:53
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tickets', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='issuethread',
name='assigned_to',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='issuethread',
name='last_activity',
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name='issuethread',
name='name',
field=models.CharField(default='unnamed issue', max_length=255),
preserve_default=False,
),
migrations.AddField(
model_name='issuethread',
name='state',
field=models.CharField(default='new', max_length=255),
),
migrations.AlterField(
model_name='comment',
name='issue_thread',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='tickets.issuethread'),
),
migrations.AlterField(
model_name='statechange',
name='issue_thread',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='state_changes', to='tickets.issuethread'),
),
]

View file

@ -1,17 +0,0 @@
# Generated by Django 4.2.7 on 2023-12-06 13:47
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('tickets', '0002_issuethread_assigned_to_issuethread_last_activity_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='issuethread',
options={'permissions': [('send_mail', 'Can send mail')]},
),
]

View file

@ -2,16 +2,24 @@ from datetime import datetime, timedelta
from django.test import TestCase, Client from django.test import TestCase, Client
from authentication.models import ExtendedUser
from mail.models import Email from mail.models import Email
from tickets.models import IssueThread, StateChange, Comment from tickets.models import IssueThread, StateChange, Comment
from django.contrib.auth.models import User
client = Client() from knox.models import AuthToken
class IssueApiTest(TestCase): class IssueApiTest(TestCase):
def setUp(self):
super().setUp()
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.save()
self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
def test_issues_empty(self): def test_issues_empty(self):
response = client.get('/api/2/tickets/') response = self.client.get('/api/2/tickets/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), []) self.assertEqual(response.json(), [])
@ -48,7 +56,7 @@ class IssueApiTest(TestCase):
timestamp=now + timedelta(seconds=3), timestamp=now + timedelta(seconds=3),
) )
response = client.get('/api/2/tickets/') response = self.client.get('/api/2/tickets/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1) self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['id'], issue.id) self.assertEqual(response.json()[0]['id'], issue.id)

View file

@ -138,7 +138,8 @@ const store = new Vuex.Store({
actions: { actions: {
async login({commit, dispatch, state}, {username, password, remember}) { async login({commit, dispatch, state}, {username, password, remember}) {
commit('setRemember', remember); commit('setRemember', remember);
const data = await fetch('/api/2/auth/token/', { try{
const data = await fetch('/api/2/token/', {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: username, password: password}), body: JSON.stringify({username: username, password: password}),
@ -151,6 +152,10 @@ const store = new Vuex.Store({
} else { } else {
return false; return false;
} }
} catch (e) {
console.error(e);
return false;
}
}, },
async loadEvents({commit}) { async loadEvents({commit}) {
const {data} = await axios.get('/1/events/'); const {data} = await axios.get('/1/events/');

View file

@ -31,6 +31,19 @@
</div> </div>
</div> </div>
<div :class="errors.email?['mb-3','is-invalid']:['mb-3']">
<label class="form-label">E-Mail</label>
<div class="input-group">
<input class="form-control"
type="email" v-model="form.email" id="validationCustomEmail"
placeholder="Enter your email" required/>
</div>
<div class="invalid-feedback">
{{ errors.email }}
</div>
</div>
<div :class="errors.password?['mb-3','is-invalid']:['mb-3']"> <div :class="errors.password?['mb-3','is-invalid']:['mb-3']">
<label class="form-label">Password</label> <label class="form-label">Password</label>
<input class="form-control" type="password" <input class="form-control" type="password"
@ -79,11 +92,13 @@ export default {
form: { form: {
username: '', username: '',
password: '', password: '',
email: '',
}, },
errors: { errors: {
username: null, username: null,
password: null, password: null,
password2: null, password2: null,
email: null,
} }
} }
}, },
@ -97,7 +112,7 @@ export default {
} else { } else {
this.errors.password2 = null; this.errors.password2 = null;
} }
fetch('/api/2/auth/register/', { fetch('/api/2/register/', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -135,7 +150,7 @@ export default {
display: block; display: block;
} }
input{ input {
background-color: var(--dark); background-color: var(--dark);
border: var(--gray) 1px solid;; border: var(--gray) 1px solid;;