├── Code ├── bookmarks │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── locations │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── 0002_like_note.py │ ├── models.py │ ├── serializers.py │ ├── templates │ │ └── locations │ │ │ └── hello.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── manage.py ├── static │ └── style.css └── templates │ └── base.html ├── LICENSE └── README.md /Code/bookmarks/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for bookmarks project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'oqqe0)ye)3su&cemkk@o2r9se1l497%2ytdz=!me5q9xm!^8u4' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'rest_framework', 41 | 'locations' 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'bookmarks.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [ 60 | os.path.join(BASE_DIR, 'templates'), 61 | ], 62 | 'APP_DIRS': True, 63 | 'OPTIONS': { 64 | 'context_processors': [ 65 | 'django.template.context_processors.debug', 66 | 'django.template.context_processors.request', 67 | 'django.contrib.auth.context_processors.auth', 68 | 'django.contrib.messages.context_processors.messages', 69 | ], 70 | }, 71 | }, 72 | ] 73 | 74 | WSGI_APPLICATION = 'bookmarks.wsgi.application' 75 | 76 | 77 | # Database 78 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 79 | 80 | DATABASES = { 81 | 'default': { 82 | 'ENGINE': 'django.db.backends.postgresql', 83 | 'NAME': 'bookmarks2', 84 | 'USER': 'bookmarksuser', 85 | 'PASSWORD': 'mypasswordhere', 86 | 'HOST': 'localhost', 87 | 'PORT': '5432', 88 | } 89 | } 90 | ''' 91 | 'default': { 92 | 'ENGINE': 'django.db.backends.sqlite3', 93 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 94 | } 95 | ''' 96 | 97 | # Password validation 98 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 99 | 100 | AUTH_PASSWORD_VALIDATORS = [ 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 106 | }, 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 109 | }, 110 | { 111 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 112 | }, 113 | ] 114 | 115 | 116 | # Internationalization 117 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 118 | 119 | LANGUAGE_CODE = 'en-us' 120 | 121 | TIME_ZONE = 'UTC' 122 | 123 | USE_I18N = True 124 | 125 | USE_L10N = True 126 | 127 | USE_TZ = True 128 | 129 | 130 | # Static files (CSS, JavaScript, Images) 131 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 132 | 133 | STATIC_URL = '/static/' 134 | 135 | STATICFILES_DIRS = ( 136 | os.path.join(BASE_DIR, "static"), 137 | '/static/', 138 | ) 139 | -------------------------------------------------------------------------------- /Code/bookmarks/urls.py: -------------------------------------------------------------------------------- 1 | """bookmarks URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | urlpatterns = [ 20 | path('locations/', include('locations.urls')), 21 | path('admin/', admin.site.urls), 22 | ] 23 | -------------------------------------------------------------------------------- /Code/bookmarks/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for bookmarks project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bookmarks.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /Code/locations/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /Code/locations/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class LocationsConfig(AppConfig): 5 | name = 'locations' 6 | -------------------------------------------------------------------------------- /Code/locations/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-02-22 02:58 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | def create_bookmarks(apps, schema_editor): 8 | Bookmark = apps.get_model('locations', 'Bookmark') 9 | Comment = apps.get_model('locations', 'Comment') 10 | bookmark1 = Bookmark(link='google.com') 11 | bookmark1.save() 12 | bookmark2 = Bookmark(link='cnn.com') 13 | bookmark2.save() 14 | bookmark3 = Bookmark(link='apple.com') 15 | bookmark3.save() 16 | comment1 = Comment(text='These people make the iPhone', bookmark=bookmark3) 17 | comment1.save() 18 | 19 | class Migration(migrations.Migration): 20 | 21 | initial = True 22 | 23 | dependencies = [ 24 | ] 25 | 26 | operations = [ 27 | migrations.CreateModel( 28 | name='Bookmark', 29 | fields=[ 30 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 31 | ('link', models.URLField(db_index=True, max_length=1000)), 32 | ], 33 | ), 34 | migrations.CreateModel( 35 | name='Comment', 36 | fields=[ 37 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 38 | ('time', models.DateTimeField(auto_now_add=True)), 39 | ('text', models.TextField()), 40 | ('bookmark', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='locations.Bookmark')), 41 | ], 42 | ), 43 | migrations.RunPython(create_bookmarks), 44 | ] 45 | -------------------------------------------------------------------------------- /Code/locations/migrations/0002_like_note.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.6 on 2018-07-09 09:34 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('locations', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Like', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('bookmark', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='likes', to='locations.Bookmark')), 19 | ('comment', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='likes', to='locations.Comment')), 20 | ], 21 | ), 22 | migrations.CreateModel( 23 | name='Note', 24 | fields=[ 25 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 26 | ('time', models.DateTimeField(auto_now_add=True)), 27 | ('text', models.TextField()), 28 | ('bookmark', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='locations.Bookmark')), 29 | ], 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /Code/locations/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models import Model 3 | from django.db.models.fields import URLField, DateTimeField, TextField 4 | from django.db.models.fields.related import ForeignKey 5 | from django.db.utils import IntegrityError 6 | 7 | # Create your models here. 8 | 9 | class Bookmark(Model): 10 | link = URLField(max_length=1000, null=False, blank=False, db_index=True) 11 | 12 | def __str__(self): 13 | return str(self.link) 14 | 15 | 16 | class Comment(Model): 17 | bookmark = ForeignKey(Bookmark, on_delete=models.CASCADE, related_name='comments') 18 | time = DateTimeField(auto_now_add=True, null=False, blank=True) 19 | text = TextField() 20 | 21 | def __str__(self): 22 | return self.text 23 | 24 | def link_str(self): 25 | return 'This comment: {} is for link: {}'.format( 26 | self.text, self.bookmark.link 27 | ) 28 | 29 | class Note(Model): 30 | bookmark = ForeignKey(Bookmark, on_delete=models.CASCADE, related_name='notes') 31 | time = DateTimeField(auto_now_add=True, null=False, blank=True) 32 | text = TextField() 33 | 34 | def __str__(self): 35 | return self.text 36 | 37 | 38 | class Like(Model): 39 | bookmark = ForeignKey( 40 | Bookmark, on_delete=models.CASCADE, related_name='likes', null=True 41 | ) 42 | comment = ForeignKey( 43 | Comment, on_delete=models.CASCADE, related_name='likes', null=True 44 | ) 45 | 46 | def __str__(self): 47 | return '{} like'.format( 48 | self.bookmark.link if self.bookmark else self.comment.link 49 | ) 50 | 51 | def clean(self): 52 | super().clean(self) 53 | if self.bookmark is None: 54 | if self.comment is None: 55 | raise IntegrityError( 56 | 'A like must be made to either a bookmark or a comment' 57 | ) 58 | elif self.comment is not None: 59 | raise IntegrityError( 60 | 'A like cannot be made to both a bookmark and a comment' 61 | ) 62 | -------------------------------------------------------------------------------- /Code/locations/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.serializers import HyperlinkedModelSerializer, Serializer,\ 2 | ModelSerializer 3 | from rest_framework.fields import IntegerField, URLField 4 | from locations.models import Bookmark, Comment, Note, Like 5 | 6 | class BookmarkManualSerializer(Serializer): 7 | id = IntegerField(read_only=True) 8 | link = URLField(required=False, max_length=1000) 9 | 10 | def create(self, validated_data): 11 | """ 12 | Create and return a new `Bookmark` instance, given the validated data. 13 | """ 14 | return Bookmark.objects.create(**validated_data) 15 | 16 | def update(self, instance, validated_data): 17 | """ 18 | Update and return an existing `Snippet` instance, given the validated data. 19 | """ 20 | instance.url = validated_data.get('url', instance.url) 21 | instance.save() 22 | return instance 23 | 24 | 25 | class CommentSerializer(HyperlinkedModelSerializer): 26 | class Meta: 27 | model = Comment 28 | fields = ['url', 'id', 'bookmark', 'time', 'text'] 29 | 30 | 31 | class CommentSerializerWithLikes(HyperlinkedModelSerializer): 32 | num_likes = IntegerField(read_only=True) 33 | class Meta: 34 | model = Comment 35 | fields = ['url', 'id', 'bookmark', 'time', 'text', 'num_likes'] 36 | 37 | 38 | class NoteSerializer(HyperlinkedModelSerializer): 39 | class Meta: 40 | model = Note 41 | fields = ['url', 'id', 'bookmark', 'time', 'text'] 42 | 43 | class BookmarkSerializer(ModelSerializer): 44 | class Meta: 45 | model = Bookmark 46 | fields = ['id', 'link'] 47 | 48 | 49 | class BookmarkLinkSerializer(HyperlinkedModelSerializer): 50 | comments = CommentSerializer(many=True, read_only=True) 51 | notes = NoteSerializer(many=True, read_only=True) 52 | num_likes = IntegerField(read_only=True) 53 | 54 | class Meta: 55 | model = Bookmark 56 | fields = ['url', 'id', 'link', 'comments', 'notes', 'num_likes'] 57 | -------------------------------------------------------------------------------- /Code/locations/templates/locations/hello.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Hello {{ name }}{% endblock %} 4 | {% block header %} 5 |
Today is the {% now "jS F Y" %}
10 |There are {{ name|length }} characters in your name
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /Code/locations/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import SimpleTestCase, TestCase 2 | from django.test.client import RequestFactory 3 | from django.urls.base import reverse 4 | from django.test import tag 5 | from .views import TemplateHelloPerson, BookmarkViewSet 6 | from rest_framework.test import APIClient 7 | from locations.models import Bookmark, Note, Comment, Like 8 | from django.utils.six import BytesIO 9 | from rest_framework.parsers import JSONParser 10 | from django.db.models.aggregates import Count 11 | from rest_framework.test import APIRequestFactory 12 | from unittest.mock import patch, Mock 13 | from rest_framework.response import Response 14 | from rest_framework import status 15 | 16 | # Create your tests here. 17 | 18 | 19 | class ITTest_TemplateHelloPerson(SimpleTestCase): 20 | @tag('integration_test') 21 | def test_render(self): 22 | response = self.client.get( 23 | reverse('hello-view3', kwargs={'name': 'Allan'}), follow=True 24 | ) 25 | self.assertEqual(response.status_code, 200) 26 | self.assertEqual(response.resolver_match.url_name, 'hello-view3') 27 | self.assertContains(response, b'There are 5 characters in your name
' 30 | ) 31 | self.assertContains( 32 | response, b'you have not won' 33 | ) 34 | 35 | 36 | class UTTest_TemplateHelloPerson(SimpleTestCase): 37 | def setUp(self): 38 | super().setUp() 39 | self.request = RequestFactory().get('/fake-path') 40 | self.view = TemplateHelloPerson() 41 | self.view = setup_view_test(self.view, self.request) 42 | 43 | @tag('unit_test') 44 | def test_class_attributes(self): 45 | self.assertEqual(self.view.template_name, 'locations/hello.html') 46 | 47 | @tag('unit_test') 48 | def test_get_context_data(self): 49 | self.view.kwargs['name'] = 'Fred' 50 | context = self.view.get_context_data() 51 | self.assertEqual(context['name'], 'Fred') 52 | 53 | 54 | def setup_view_test(view, request, *args, **kwargs): 55 | """ 56 | Mimic as_view() returned callable, but returns view instance. 57 | 58 | args and kwargs are the same you would pass to ``reverse()`` 59 | """ 60 | view.request = request 61 | view.args = args 62 | view.kwargs = kwargs 63 | return view 64 | 65 | 66 | class Test_BookmarkViewset(TestCase): 67 | def setUp(self): 68 | super().setUp() 69 | bookmark1 = Bookmark(link="http://www.google.com/") 70 | bookmark1.save() 71 | bookmark2 = Bookmark(link="http://www.cnn.com/") 72 | bookmark2.save() 73 | bookmark3 = Bookmark(link="http://www.bbc.co.uk/") 74 | bookmark3.save() 75 | note = Note(text="This is a note", bookmark=bookmark2) 76 | note.save() 77 | comment = Comment(bookmark=bookmark3, text="This is a comment") 78 | comment.save() 79 | like1 = Like(bookmark=bookmark1) 80 | like1.save() 81 | like2 = Like(comment=comment) 82 | like2.save() 83 | 84 | @tag('integration_test') 85 | def test_get(self): 86 | client = APIClient() 87 | result = client.get('/locations/bookmarks/') 88 | stream = BytesIO(result.content) 89 | data = JSONParser().parse(stream) 90 | self.assertEqual(len(data), 6) 91 | 92 | @tag('integration_test') 93 | def test_add_like(self): 94 | bookmark = Bookmark.objects.annotate(num_likes=Count('likes')).get(id=6) 95 | self.assertEqual(bookmark.num_likes, 0) 96 | client = APIClient() 97 | result = client.post('/locations/bookmarks/6/add_like/') 98 | bookmark = Bookmark.objects.annotate(num_likes=Count('likes')).get(id=6) 99 | self.assertEqual(bookmark.num_likes, 1) 100 | result = client.post('/locations/bookmarks/6/add_like/') 101 | bookmark = Bookmark.objects.annotate(num_likes=Count('likes')).get(id=6) 102 | self.assertEqual(bookmark.num_likes, 2) 103 | 104 | @tag('unit_test') 105 | @patch('locations.views.Response') 106 | @patch('locations.views.BookmarkViewSet.get_object') 107 | @patch('locations.views.Like') 108 | def test_add_like(self, l_patch, go_patch, r_patch): 109 | factory = APIRequestFactory() 110 | request = factory.post( 111 | '/locations/bookmarks/6/add_like/', {} 112 | ) 113 | view = BookmarkViewSet() 114 | result = view.add_like(request) 115 | self.assertEqual(go_patch.call_count, 1) 116 | self.assertEqual(l_patch.call_count, 1) 117 | self.assertEqual(l_patch.return_value.bookmark, go_patch.return_value) 118 | self.assertEqual(l_patch.return_value.save.call_count, 1) 119 | self.assertEqual(r_patch.call_count, 1) 120 | self.assertEqual(r_patch.call_args[0], ({'status': 'bookmark like set'},)) 121 | self.assertEqual(r_patch.call_args[1], {'status': status.HTTP_201_CREATED}) 122 | self.assertEqual(result, r_patch.return_value) 123 | -------------------------------------------------------------------------------- /Code/locations/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, include 2 | from django.urls import path 3 | from locations import views 4 | from .views import SimpleHelloWorld, SimpleHelloPerson, TemplateHelloPerson,\ 5 | SimpleHelloWorldAPI 6 | from locations.views import BookmarkListView, BookmarkDetailView,\ 7 | BookmarkList, BookmarkDetail, BookmarkViewSet, CommentViewSet,\ 8 | NoteViewSet 9 | from rest_framework.urlpatterns import format_suffix_patterns 10 | from rest_framework import routers 11 | 12 | router = routers.DefaultRouter() 13 | router.register(r'bookmarks', BookmarkViewSet) 14 | router.register(r'comments', CommentViewSet) 15 | router.register(r'notes', NoteViewSet) 16 | 17 | urlpatterns = [ 18 | path('hello1/', SimpleHelloWorld.as_view(), name='hello-view1'), 19 | path('hello2/