Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Annotations #639

Merged
merged 13 commits into from
Dec 19, 2023
Empty file.
16 changes: 16 additions & 0 deletions project/annotation_project/annotation_project/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
ASGI config for annotation_project project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'annotation_project.settings')

application = get_asgi_application()
134 changes: 134 additions & 0 deletions project/annotation_project/annotation_project/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""
Django settings for annotation_project project.

Generated by 'django-admin startproject' using Django 4.2.6.

For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""

from pathlib import Path
import os

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'annotations',
'corsheaders',
]

MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'annotation_project.urls'

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

WSGI_APPLICATION = 'annotation_project.wsgi.application'


# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases

DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get('DATABASE_NAME'),
"USER": os.environ.get('DATABASE_USER'),
"PASSWORD": os.environ.get('DATABASE_PASSWORD'),
"HOST": os.environ.get('DATABASE_HOST'),
"PORT": os.environ.get('DATABASE_PORT'),
}
}


# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]


# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/

STATIC_URL = 'static/'

# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

CORS_ALLOW_ALL_ORIGINS = True # THIS PART SHOULD BE RESTRICTED IN A LATER PR
7 changes: 7 additions & 0 deletions project/annotation_project/annotation_project/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path('admin/', admin.site.urls),
path('annotations/', include("annotations.urls")) # url must start with 'annotations/' according to the standard: https://www.w3.org/TR/annotation-protocol/#:~:text=5.2%20Suggesting%20an%20IRI%20for%20an%20Annotation
]
16 changes: 16 additions & 0 deletions project/annotation_project/annotation_project/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
WSGI config for annotation_project project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'annotation_project.settings')

application = get_wsgi_application()
Empty file.
8 changes: 8 additions & 0 deletions project/annotation_project/annotations/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.contrib import admin
from .models import *

admin.site.register(Source)
admin.site.register(Selector)
admin.site.register(Body)
admin.site.register(Creator)
admin.site.register(Annotation)
6 changes: 6 additions & 0 deletions project/annotation_project/annotations/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class AnnotationsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'annotations'
Empty file.
27 changes: 27 additions & 0 deletions project/annotation_project/annotations/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from django.db import models

class Source(models.Model):
uri = models.URLField(max_length=100, unique=True)

class Selector(models.Model):
type = models.CharField(max_length=100, default='TextPositionSelector')
start = models.PositiveIntegerField()
end = models.PositiveIntegerField()
source = models.ForeignKey(Source, on_delete=models.CASCADE)

class Body(models.Model):
type = models.CharField(max_length=100, default='TextualBody')
value = models.TextField(max_length=400)
format = models.CharField(max_length=100, default='text/plain')
language = models.CharField(max_length=100, default='en')

class Creator(models.Model):
name = models.CharField(max_length=100)
type = models.TextField(max_length=100, default='Person')

class Annotation(models.Model):
type = models.CharField(max_length=100, default='Annotation')
body = models.ForeignKey(Body, on_delete=models.CASCADE)
target = models.ForeignKey(Selector, on_delete=models.CASCADE)
creator = models.ForeignKey(Creator, on_delete=models.PROTECT)
created = models.DateTimeField(auto_now_add=True)
145 changes: 145 additions & 0 deletions project/annotation_project/annotations/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from django.test import TestCase, Client
from django.urls import reverse
from datetime import datetime
import json

from .models import *

class AnnotationGetTest(TestCase):
def setUp(self):
source = Source.objects.create(uri='http://example.com/source1')
body = Body.objects.create(value='Annotation Test Content')
creator = Creator.objects.create(name='testcreator@example.com')
selector = Selector.objects.create(start=0, end=5, source=source)
self.annotation = Annotation.objects.create(
body=body,
target=selector,
creator=creator,
created=datetime.now(),
)

def tearDown(self):
Annotation.objects.all().delete()
Selector.objects.all().delete()
Creator.objects.all().delete()
Body.objects.all().delete()
Source.objects.all().delete()
print("All Annotation Get API Tests Completed")

def test_get_annotation_by_id(self):
#Test the annotation response format
client = Client()

url = reverse('get_annotation_by_id', args=[self.annotation.id])

response = client.get(url)
self.assertEqual(response.status_code, 200)

response = response.json()
self.assertEqual(response['@context'], 'http://www.w3.org/ns/anno.jsonld')
self.assertEqual(response['id'],f'http://13.51.55.11:8001/annotations/annotation/{self.annotation.id}')
self.assertEqual(response['type'], self.annotation.type)
self.assertEqual(response['body'], {
'type': self.annotation.body.type,
'format': self.annotation.body.format,
'language': self.annotation.body.language,
'value': self.annotation.body.value,
})
self.assertEqual(response['target'], {
'id': self.annotation.target.source.uri,
'type': 'text',
'selector': {
'type': self.annotation.target.type,
'start': self.annotation.target.start,
'end': self.annotation.target.end,
}
})
self.assertTrue(response['creator']['id'], self.annotation.creator.name)
self.assertIn('created', response)

class AnnotationPostTest(TestCase):
def setUp(self):
self.client = Client()
self.url = reverse('create_annotation')
self.headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
self.json = {
"@context": "http://www.w3.org/ns/anno.jsonld",
"type": "Annotation",
"body": {
"type": "TextualBody",
"format": "text/html",
"language": "en",
"value": "Turing machine is great!!!"
},
"target": {
"id": "http://13.51.205.39/node/55#theorem",
"type": "text",
"selector": {
"type": "TextPositionSelector",
"start": 30,
"end": 45
}
},
"creator": {
"id": "http://13.51.205.39/profile/cemsay@gmail.com",
"type": "Person"
}
}
self.body = {
"@context": "http://www.w3.org/ns/anno.jsonld",
"type": "Annotation",
"body": str(self.json['body']),
"target": str(self.json['target']),
"creator": str(self.json['creator'])
}
self.body_missing_body = {
"@context": "http://www.w3.org/ns/anno.jsonld",
"type": "Annotation",
"target": str(self.json['target']),
"creator": str(self.json['creator'])
}
self.body_missing_target = {
"@context": "http://www.w3.org/ns/anno.jsonld",
"type": "Annotation",
"body": str(self.json['body']),
"creator": str(self.json['creator'])
}
self.body_missing_creator = {
"@context": "http://www.w3.org/ns/anno.jsonld",
"type": "Annotation",
"body": str(self.json['body']),
"target": str(self.json['target']),
}

def tearDown(self):
Annotation.objects.all().delete()
Selector.objects.all().delete()
Creator.objects.all().delete()
Body.objects.all().delete()
Source.objects.all().delete()
print("All Annotation Post API Tests Completed")

def test_create_annotation(self):
response = self.client.post(self.url, data=self.body_missing_body, headers=self.headers)
self.assertEqual(response.status_code, 400, "Missing body field test failed. expected 400, got " + str(response.status_code))
response = self.client.post(self.url, data=self.body_missing_target, headers=self.headers)
self.assertEqual(response.status_code, 400, "Missing target field test failed. expected 400, got " + str(response.status_code))
response = self.client.post(self.url, data=self.body_missing_creator, headers=self.headers)
self.assertEqual(response.status_code, 400, "Missing creator field test failed. expected 400, got " + str(response.status_code))

response = self.client.post(self.url, data=self.body, headers=self.headers, content_type='application/json')
self.assertEqual(response.status_code, 200, "Successful create annotation test failed. expected 200, got " + str(response.status_code))

records = Annotation.objects.filter(id=response.json()['id'])
self.assertEqual(len(records), 1, "Successful create annotation test (database insertion) failed. expected 1 row, got " + str(len(records)))

if records:
record = records[0]
data = response.json()
self.assertEqual(data['body'], self.json['body'], "Successful create annotation test (body) failed. expected " + str(self.json['body']) + ", got " + str(data['body']))
self.assertEqual(data['target'], self.json['target'], "Successful create annotation test (target) failed. expected " + str(self.json['target']) + ", got " + str(data['target']))
self.assertEqual(data['creator'], self.json['creator'], "Successful create annotation test (creator) failed. expected " + str(self.json['creator']) + ", got " + str(data['creator']))
self.assertEqual(data['id'], "http://13.51.55.11:8001/annotations/annotation/" + str(record.pk), "Successful create annotation test (id) failed. expected " + "http://13.51.55.11:8001/annotations/annotation/" + str(record.pk))
11 changes: 11 additions & 0 deletions project/annotation_project/annotations/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.urls import path

from .views import *

urlpatterns = [
path('get_annotation/', matched_annotations_get_view, name='get_annotation'),
path('annotation/<annotation_id>/', get_annotation_by_id, name='get_annotation_by_id'),
path('create_annotation/', create_annotation, name='create_annotation'),
]


Loading