diff --git a/blango/__pycache__/__init__.cpython-311.pyc b/blango/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000..271e86f76c Binary files /dev/null and b/blango/__pycache__/__init__.cpython-311.pyc differ diff --git a/blango/__pycache__/settings.cpython-311.pyc b/blango/__pycache__/settings.cpython-311.pyc new file mode 100644 index 0000000000..b51fed2619 Binary files /dev/null and b/blango/__pycache__/settings.cpython-311.pyc differ diff --git a/blango/__pycache__/urls.cpython-311.pyc b/blango/__pycache__/urls.cpython-311.pyc new file mode 100644 index 0000000000..96a223ba30 Binary files /dev/null and b/blango/__pycache__/urls.cpython-311.pyc differ diff --git a/blango/__pycache__/wsgi.cpython-311.pyc b/blango/__pycache__/wsgi.cpython-311.pyc new file mode 100644 index 0000000000..52e81caee0 Binary files /dev/null and b/blango/__pycache__/wsgi.cpython-311.pyc differ diff --git a/blango/data.json b/blango/data.json new file mode 100644 index 0000000000..bcb6ea11ef --- /dev/null +++ b/blango/data.json @@ -0,0 +1 @@ +{"model": "blog.comment", "pk": 1, "fields": {"creator": 1, "content": "What a great post!", "content_type": 7, "object_id": 1, "created_at": "2024-04-27T08:05:47.496Z", "modified_at": "2024-04-27T08:05:47.496Z"}}, {"model": "blog.comment", "pk": 2, "fields": {"creator": 1, "content": "This is a comment caputured on the web frontend", "content_type": 7, "object_id": 1, "created_at": "2024-04-27T09:07:47.913Z", "modified_at": "2024-04-27T09:07:47.913Z"}}, {"model": "blog.comment", "pk": 3, "fields": {"creator": 1, "content": "some extras after changing everything to use crispy forms", "content_type": 7, "object_id": 1, "created_at": "2024-04-27T10:26:20.183Z", "modified_at": "2024-04-27T10:26:20.183Z"}}, {"model": "blog.tag", "pk": 1, "fields": {"value": "Django"}}, {"model": "blog.tag", "pk": 2, "fields": {"value": "Coursera"}}, {"model": "blog.post", "pk": 1, "fields": {"author": 1, "created_at": "2024-04-25T20:17:53.544Z", "modified_at": "2024-04-25T20:18:42.894Z", "published_at": "2024-04-25T20:16:00Z", "title": "New Post Title", "slug": "new-post-title", "summary": "This is new title summary", "content": "

Content of Test Post

\r\n

this is text

", "tags": [1, 2]}}, {"model": "blog.post", "pk": 2, "fields": {"author": 1, "created_at": "2024-04-27T08:47:42.299Z", "modified_at": "2024-04-27T08:47:42.299Z", "published_at": "2024-04-27T08:46:32Z", "title": "Post 2", "slug": "post-2", "summary": "this is summary of post 2", "content": "

THIS IS POST 2

\r\n

this is post 2 text

", "tags": [1, 2]}}, {"model": "blog.post", "pk": 3, "fields": {"author": 1, "created_at": "2024-04-27T08:49:33.124Z", "modified_at": "2024-04-27T08:49:33.124Z", "published_at": "2024-04-23T08:48:00Z", "title": "Post 3", "slug": "post-3", "summary": "Post 3 dated 23rd", "content": "

title of post 3

\r\n

jlsjafkjsjsjsaiuroalaahhaofzoaujvahvahoao

", "tags": [1, 2]}}, {"model": "blango_auth.Userr", "pk": 1, "fields": {"password": "pbkdf2_sha256$720000$f9fIrc28qkOJIh8JDDNUBq$eRdASO3rp/4JnBGbKSeES1SqN2aDU4aUXOzOtQ+u3zo=", "last_login": "2024-04-25T20:15:49.518Z", "is_superuser": true, "username": "codio", "first_name": "", "last_name": "", "email": "", "is_staff": true, "is_active": true, "date_joined": "2024-04-25T20:09:08.771Z", "groups": [], "user_permissions": []}}] \ No newline at end of file diff --git a/blango/environ_test.py b/blango/environ_test.py new file mode 100644 index 0000000000..bc2aacb2b3 --- /dev/null +++ b/blango/environ_test.py @@ -0,0 +1,16 @@ +from os import environ + +environ.setdefault("PYTHON_DEFAULT", "Python Default") + +print(f"Value of 'MUST_BE_SET': '{environ['MUST_BE_SET']}'") +print(f"Value of 'PYTHON_DEFAULT': '{environ['PYTHON_DEFAULT']}'") + +try: + print(f"Value of 'ALWAYS_OVERRIDDEN' before override: '{environ['ALWAYS_OVERRIDDEN']}'") +except KeyError: + print("'ALWAYS_OVERRIDDEN' was not set.") + +environ["ALWAYS_OVERRIDDEN"] = "Always Overridden In Python" + +print(f"Value of 'ALWAYS_OVERRIDDEN' after override: '{environ['ALWAYS_OVERRIDDEN']}'") +print(f"Value of 'OPTIONAL': '{environ.get('OPTIONAL')}'") \ No newline at end of file diff --git a/blango/requests_test.py b/blango/requests_test.py new file mode 100644 index 0000000000..4099f107da --- /dev/null +++ b/blango/requests_test.py @@ -0,0 +1,32 @@ +import requests + +# put your real credentials here +EMAIL_ADDRESS = "codio@abc.com" +PASSWORD = "codio" +BASE_URL = "http://localhost:8000/" + +anon_post_resp = requests.get(BASE_URL + "api/v1/posts/") +anon_post_resp.raise_for_status() + +anon_post_count = anon_post_resp.json()["count"] +print(f"Anon users have {anon_post_count} post{'' if anon_post_count == 1 else 's'}") + +auth_resp = requests.post( + BASE_URL + "api/v1/token-auth/", + json={"username": EMAIL_ADDRESS, "password": PASSWORD}, +) +auth_resp.raise_for_status() +token = auth_resp.json()["token"] + +# Use the token in a request +authed_post_resp = requests.get( + BASE_URL + "api/v1/posts/", headers={"Authorization": f"Token {token}"} +) +authed_post_count = authed_post_resp.json()["count"] + +print( + f"Authenticated user has {authed_post_count} post{'' if authed_post_count == 1 else 's'}" +) + +# Since requests doesn't remember headers between requests, this next request is unauthenticated again +anon_post_resp = requests.get(BASE_URL + "api/v1/posts/") \ No newline at end of file diff --git a/blango/settings.py b/blango/settings.py index f9209bef27..b3cf806f59 100644 --- a/blango/settings.py +++ b/blango/settings.py @@ -1,125 +1,221 @@ -""" -Django settings for blango project. +""" +Django settings for blango project. +Generated by 'django-admin startproject' using Django 3.2.5. +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" -Generated by 'django-admin startproject' using Django 3.2.7. -For more information on this file, see -https://docs.djangoproject.com/en/3.2/topics/settings/ -For the full list of settings and their values, see -https://docs.djangoproject.com/en/3.2/ref/settings/ -""" +import os +from pathlib import Path +from datetime import timedelta -from pathlib import Path +from configurations import Configuration +from configurations import values -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent +class Dev(Configuration): -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ +# Build paths inside the project like this: BASE_DIR / 'subdir'. + BASE_DIR = Path(__file__).resolve().parent.parent -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-+sn%dpa!086+g+%44z9*^j^q-u4n!j(#wl)x9a%_1op@zz2+1-' -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True + # Quick-start development settings - unsuitable for production + # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ + # SECURITY WARNING: keep the secret key used in production secret! + + SECRET_KEY = 'django-insecure-&!=9y436&^-bc$qia-mxngyf&xx)@ct)8lu@)=qxg_07-=z01w' + # SECURITY WARNING: don't run with debug turned on in production! -ALLOWED_HOSTS = [] + DEBUG = True + ALLOWED_HOSTS = ['*', 'localhost'] -# Application definition -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', -] + AUTH_USER_MODEL = "blango_auth.User" -MIDDLEWARE = [ - '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 = 'blango.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', - ], + #X_FRAME_OPTIONS = 'ALLOW-FROM ' + os.environ.get('CODIO_HOSTNAME') + '-8000.codio.io' + CSRF_COOKIE_SAMESITE = None + #CSRF_TRUSTED_ORIGINS = [os.environ.get('CODIO_HOSTNAME') + '-8000.codio.io'] + CSRF_COOKIE_SECURE = True + SESSION_COOKIE_SECURE = True + CSRF_COOKIE_SAMESITE = 'None' + SESSION_COOKIE_SAMESITE = 'None' + + + # Application definition + INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'blog', + 'crispy_forms', + 'crispy_bootstrap5', + 'blango_auth', + 'rest_framework', + 'rest_framework.authtoken', + 'drf_yasg', + 'django_filters', + 'versatileimagefield', + ] + + + MIDDLEWARE = [ + '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', + + ] + + ''' + Reminder: MIDDLEWARE COMMENT OUT these changes only apply to working with Django on Codio. Do + not make these changes to a project you plan on making available on the + internet. + ''' + + ROOT_URLCONF = 'blango.urls' + + + TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + "DIRS": [BASE_DIR / "templates"], + '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 = 'blango.wsgi.application' + + + # Database + # https://docs.djangoproject.com/en/3.2/ref/settings/#databases + + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } + } + + + # Password validation + # https://docs.djangoproject.com/en/3.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', }, - }, -] + ] -WSGI_APPLICATION = 'blango.wsgi.application' + # Internationalization + # https://docs.djangoproject.com/en/3.2/topics/i18n/ -# Database -# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + LANGUAGE_CODE = 'en-us' + TIME_ZONE = ("UTC") + USE_I18N = True + USE_L10N = True + USE_TZ = True -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } -} + # Static files (CSS, JavaScript, Images) + # https://docs.djangoproject.com/en/3.2/howto/static-files/ + + STATIC_URL = '/static/' + + MEDIA_ROOT = BASE_DIR / "media" + MEDIA_URL = "/media/" -# Password validation -# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators + # Default primary key field type + # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field -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', - }, -] + DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" + CRISPY_TEMPLATE_PACK = "bootstrap5" -# Internationalization -# https://docs.djangoproject.com/en/3.2/topics/i18n/ + REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.BasicAuthentication", + "rest_framework.authentication.SessionAuthentication", + "rest_framework.authentication.TokenAuthentication", + ], -LANGUAGE_CODE = 'en-us' + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.IsAuthenticatedOrReadOnly" + ], -TIME_ZONE = 'UTC' + #throttling + "DEFAULT_THROTTLE_CLASSES": [ + "blog.api.throttling.AnonSustainedThrottle", + "blog.api.throttling.AnonBurstThrottle", + "blog.api.throttling.UserSustainedThrottle", + "blog.api.throttling.UserBurstThrottle", + ], + "DEFAULT_THROTTLE_RATES": { + "anon_sustained": "500/day", + "anon_burst": "10/minute", + "user_sustained": "5000/day", + "user_burst": "100/minute", + }, + + #pagination + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 100, -USE_I18N = True -USE_L10N = True + #extending with django filters for rest rest_framework + "DEFAULT_FILTER_BACKENDS": [ + "django_filters.rest_framework.DjangoFilterBackend", + "rest_framework.filters.OrderingFilter", + ], -USE_TZ = True + #extending with JWT Json web tokens + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.BasicAuthentication", + "rest_framework.authentication.SessionAuthentication", + "rest_framework.authentication.TokenAuthentication", + "rest_framework_simplejwt.authentication.JWTAuthentication" + ], -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/3.2/howto/static-files/ + } -STATIC_URL = '/static/' -# Default primary key field type -# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + SWAGGER_SETTINGS = { + "SECURITY_DEFINITIONS": { + "Token": {"type": "apiKey", "name": "Authorization", "in": "header"}, + "Basic": {"type": "basic"}, + } + } \ No newline at end of file diff --git a/blango/settings_orig.py b/blango/settings_orig.py new file mode 100644 index 0000000000..62acdb3720 --- /dev/null +++ b/blango/settings_orig.py @@ -0,0 +1,160 @@ +""" +Django settings for blango project. + +Generated by 'django-admin startproject' using Django 3.2.7. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" + +import os +from pathlib import Path +#from configurations import Configuration +#from configurations import values + +class Dev(Configuration): + +# 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/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! + SECRET_KEY = 'django-insecure-+sn%dpa!086+g+%44z9*^j^q-u4n!j(#wl)x9a%_1op@zz2+1-' + +# 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', + 'blog', + 'crispy_forms', + 'crispy_bootstrap5', + 'debug_toolbar', +] + +#INTERNAL_IPS = ["192.168.10.93"] + +MIDDLEWARE = [ + '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', + 'debug_toolbar.middleware.DebugToolbarMiddleware', +] +''' +Reminder: MIDDLEWARE COMMENT OUT these changes only apply to working with Django on Codio. Do +not make these changes to a project you plan on making available on the +internet. +''' + +ROOT_URLCONF = 'blango.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + '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 = 'blango.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.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/3.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = '/static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + + +CRISPY_ALLOWED_TEMPLATE_PACKS = 'bootstrap5' +CRISPY_TEMPLATE_PACK = 'bootstrap5' + +#....this is set only for the course, remove when finished +''' +Reminder: these changes only apply to working with Django on Codio. Do +not make these changes to a project you plan on making available on the +internet. +''' +# X_FRAME_OPTIONS = 'ALLOW-FROM ' + os.environ.get('CODIO_HOSTNAME') + '-8000.codio.io' +# CSRF_COOKIE_SAMESITE = None +# CSRF_TRUSTED_ORIGINS = [os.environ.get('CODIO_HOSTNAME') + '-8000.codio.io'] +# CSRF_COOKIE_SECURE = True +# SESSION_COOKIE_SECURE = True +# CSRF_COOKIE_SAMESITE = 'None' +# SESSION_COOKIE_SAMESITE = 'None' \ No newline at end of file diff --git a/blango/urls.py b/blango/urls.py index cde05802f9..589c88e692 100644 --- a/blango/urls.py +++ b/blango/urls.py @@ -14,8 +14,31 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.conf.urls.static import static + +from django.urls import path, include +#import debug_toolbar +import blog.views +import blango_auth.views urlpatterns = [ path('admin/', admin.site.urls), + path("", blog.views.index), + path("post//", blog.views.post_detail, name="blog-post-detail"), + path("ip/", blog.views.get_ip), + path("accounts/", include("django.contrib.auth.urls")), + path("accounts/profile/", blango_auth.views.profile, name="profile"), + path("api/v1/", include("blog.api.urls")), + + path("post-table/", blog.views.post_table, name="blog-post-table"), + ] + + +""" +if settings.DEBUG: + urlpatterns += [ + path("__debug__/", include(debug_toolbar.urls)), + ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + +""" diff --git a/blango/wsgi.py b/blango/wsgi.py index 83565cf12c..387004ddc3 100644 --- a/blango/wsgi.py +++ b/blango/wsgi.py @@ -7,10 +7,13 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ """ + import os -from django.core.wsgi import get_wsgi_application +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blango.settings") +os.environ.setdefault("DJANGO_CONFIGURATION", "Prod") #--set for config -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blango.settings') +from configurations.wsgi import get_wsgi_application +#from django.core.wsgi import get_wsgi_application application = get_wsgi_application() diff --git a/blango_auth/__init__.py b/blango_auth/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blango_auth/__pycache__/__init__.cpython-311.pyc b/blango_auth/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000..468da9eade Binary files /dev/null and b/blango_auth/__pycache__/__init__.cpython-311.pyc differ diff --git a/blango_auth/__pycache__/admin.cpython-311.pyc b/blango_auth/__pycache__/admin.cpython-311.pyc new file mode 100644 index 0000000000..ed4bccdeab Binary files /dev/null and b/blango_auth/__pycache__/admin.cpython-311.pyc differ diff --git a/blango_auth/__pycache__/apps.cpython-311.pyc b/blango_auth/__pycache__/apps.cpython-311.pyc new file mode 100644 index 0000000000..cb61824728 Binary files /dev/null and b/blango_auth/__pycache__/apps.cpython-311.pyc differ diff --git a/blango_auth/__pycache__/models.cpython-311.pyc b/blango_auth/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000..fbca41dc85 Binary files /dev/null and b/blango_auth/__pycache__/models.cpython-311.pyc differ diff --git a/blango_auth/__pycache__/views.cpython-311.pyc b/blango_auth/__pycache__/views.cpython-311.pyc new file mode 100644 index 0000000000..42a09af34d Binary files /dev/null and b/blango_auth/__pycache__/views.cpython-311.pyc differ diff --git a/blango_auth/admin.py b/blango_auth/admin.py new file mode 100644 index 0000000000..88152ce3a2 --- /dev/null +++ b/blango_auth/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from blango_auth.models import User +# Register your models here. + + + +admin.site.register(User, UserAdmin) \ No newline at end of file diff --git a/blango_auth/apps.py b/blango_auth/apps.py new file mode 100644 index 0000000000..3619a45e56 --- /dev/null +++ b/blango_auth/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BlangoAuthConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'blango_auth' diff --git a/blango_auth/migrations/0001_initial.py b/blango_auth/migrations/0001_initial.py new file mode 100644 index 0000000000..6b3075a04c --- /dev/null +++ b/blango_auth/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 5.0.4 on 2024-05-25 08:00 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/blango_auth/migrations/__init__.py b/blango_auth/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blango_auth/migrations/__pycache__/0001_initial.cpython-311.pyc b/blango_auth/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000000..0ee94d8583 Binary files /dev/null and b/blango_auth/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/blango_auth/migrations/__pycache__/__init__.cpython-311.pyc b/blango_auth/migrations/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000..5d9b74c928 Binary files /dev/null and b/blango_auth/migrations/__pycache__/__init__.cpython-311.pyc differ diff --git a/blango_auth/models.py b/blango_auth/models.py new file mode 100644 index 0000000000..eec6c516e3 --- /dev/null +++ b/blango_auth/models.py @@ -0,0 +1,9 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser, UserManager +# Create your models here. + + +class User(AbstractUser): + pass + + diff --git a/blango_auth/templates/blango_auth/profile.html b/blango_auth/templates/blango_auth/profile.html new file mode 100644 index 0000000000..5e51727c25 --- /dev/null +++ b/blango_auth/templates/blango_auth/profile.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} +{% load blog_extras %} +{% block title %}Blango Profile{% endblock %} +{% block content %} +{% row %} +{% col %} +

Logged in as {{ request.user }}.

+

Log Out

+{% endcol %} +{% endrow %} +{% endblock content %} \ No newline at end of file diff --git a/blango_auth/templates/registration/login.html b/blango_auth/templates/registration/login.html new file mode 100644 index 0000000000..2b2f392f9f --- /dev/null +++ b/blango_auth/templates/registration/login.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} +{% load crispy_forms_tags blog_extras %} +{% block title %}Log In to Blango{% endblock %} +{% block content %} +{% row "justify-content-center" %} +{% col "col-md-6" %} +{% if next %} +{% if user.is_authenticated %} +

Your account doesn't have access to this page. To +proceed, +please login with an account that has access.

+{% else %} +

Please login to see this page.

+{% endif %} +{% endif %} +{% endcol %} +{% endrow %} +{% row "justify-content-center" %} +{% col "col-md-6" %} +
+{% csrf_token %} +{{ form|crispy }} + + +
+

Lost password? +

+{% endcol %} +{% endrow %} +{% endblock %} \ No newline at end of file diff --git a/blango_auth/tests.py b/blango_auth/tests.py new file mode 100644 index 0000000000..7ce503c2dd --- /dev/null +++ b/blango_auth/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/blango_auth/views.py b/blango_auth/views.py new file mode 100644 index 0000000000..36a76d2034 --- /dev/null +++ b/blango_auth/views.py @@ -0,0 +1,10 @@ +from django.shortcuts import render + +# Create your views here. +from django.contrib.auth.decorators import login_required +from django.shortcuts import render + + +@login_required +def profile(request): + return render(request, "blango_auth/profile.html") diff --git a/blog/__init__.py b/blog/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blog/__pycache__/__init__.cpython-311.pyc b/blog/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000..b4f9abca7f Binary files /dev/null and b/blog/__pycache__/__init__.cpython-311.pyc differ diff --git a/blog/__pycache__/admin.cpython-311.pyc b/blog/__pycache__/admin.cpython-311.pyc new file mode 100644 index 0000000000..11db98946e Binary files /dev/null and b/blog/__pycache__/admin.cpython-311.pyc differ diff --git a/blog/__pycache__/apps.cpython-311.pyc b/blog/__pycache__/apps.cpython-311.pyc new file mode 100644 index 0000000000..b452d18c5c Binary files /dev/null and b/blog/__pycache__/apps.cpython-311.pyc differ diff --git a/blog/__pycache__/forms.cpython-311.pyc b/blog/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000000..4c1ed74350 Binary files /dev/null and b/blog/__pycache__/forms.cpython-311.pyc differ diff --git a/blog/__pycache__/models.cpython-311.pyc b/blog/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000..4bcf19d77a Binary files /dev/null and b/blog/__pycache__/models.cpython-311.pyc differ diff --git a/blog/__pycache__/views.cpython-311.pyc b/blog/__pycache__/views.cpython-311.pyc new file mode 100644 index 0000000000..79071ce1c3 Binary files /dev/null and b/blog/__pycache__/views.cpython-311.pyc differ diff --git a/blog/admin.py b/blog/admin.py new file mode 100644 index 0000000000..c7631bbbf1 --- /dev/null +++ b/blog/admin.py @@ -0,0 +1,20 @@ +from django.contrib import admin + +# Register your models here. + + +from blog.models import Tag, Post, Comment, AuthorProfile + +class PostAdmin(admin.ModelAdmin): + prepopulated_fields = {"slug": ("title",)} + list_display = ('slug', 'published_at') + + + +admin.site.register(Tag) +admin.site.register(Post, PostAdmin) +admin.site.register(Comment) + +admin.site.register(AuthorProfile) + + diff --git a/blog/api/__init__.py b/blog/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blog/api/__pycache__/__init__.cpython-311.pyc b/blog/api/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000..bd8186ba5e Binary files /dev/null and b/blog/api/__pycache__/__init__.cpython-311.pyc differ diff --git a/blog/api/__pycache__/filters.cpython-311.pyc b/blog/api/__pycache__/filters.cpython-311.pyc new file mode 100644 index 0000000000..974a585a06 Binary files /dev/null and b/blog/api/__pycache__/filters.cpython-311.pyc differ diff --git a/blog/api/__pycache__/permissions.cpython-311.pyc b/blog/api/__pycache__/permissions.cpython-311.pyc new file mode 100644 index 0000000000..302f1d162a Binary files /dev/null and b/blog/api/__pycache__/permissions.cpython-311.pyc differ diff --git a/blog/api/__pycache__/serializers.cpython-311.pyc b/blog/api/__pycache__/serializers.cpython-311.pyc new file mode 100644 index 0000000000..f9b3342d7d Binary files /dev/null and b/blog/api/__pycache__/serializers.cpython-311.pyc differ diff --git a/blog/api/__pycache__/throttling.cpython-311.pyc b/blog/api/__pycache__/throttling.cpython-311.pyc new file mode 100644 index 0000000000..312a27eac7 Binary files /dev/null and b/blog/api/__pycache__/throttling.cpython-311.pyc differ diff --git a/blog/api/__pycache__/urls.cpython-311.pyc b/blog/api/__pycache__/urls.cpython-311.pyc new file mode 100644 index 0000000000..93656a421a Binary files /dev/null and b/blog/api/__pycache__/urls.cpython-311.pyc differ diff --git a/blog/api/__pycache__/views.cpython-311.pyc b/blog/api/__pycache__/views.cpython-311.pyc new file mode 100644 index 0000000000..6b75e86f46 Binary files /dev/null and b/blog/api/__pycache__/views.cpython-311.pyc differ diff --git a/blog/api/filters.py b/blog/api/filters.py new file mode 100644 index 0000000000..e55c6af26b --- /dev/null +++ b/blog/api/filters.py @@ -0,0 +1,31 @@ +from django_filters import rest_framework as filters + +from blog.models import Post + + +class PostFilterSet(filters.FilterSet): + published_from = filters.DateFilter( + field_name="published_at", lookup_expr="gte", label="Published Date From" + ) + published_to = filters.DateFilter( + field_name="published_at", lookup_expr="lte", label="Published Date To" + ) + author_email = filters.CharFilter( + field_name="author__email", + lookup_expr="icontains", + label="Author Email Contains", + ) + summary = filters.CharFilter( + field_name="summary", + lookup_expr="icontains", + label="Summary Contains", + ) + content = filters.CharFilter( + field_name="content", + lookup_expr="icontains", + label="Content Contains", + ) + + class Meta: + model = Post + fields = ["author", "tags"] diff --git a/blog/api/permissions.py b/blog/api/permissions.py new file mode 100644 index 0000000000..606cba90f1 --- /dev/null +++ b/blog/api/permissions.py @@ -0,0 +1,13 @@ +from rest_framework import permissions + + +class AuthorModifyOrReadOnly(permissions.IsAuthenticatedOrReadOnly): + def has_object_permission(self, request, view, obj): + if request.method in permissions.SAFE_METHODS: + return True + + return request.user == obj.author + +class IsAdminUserForObject(permissions.IsAdminUser): + def has_object_permission(self, request, view, obj): + return bool(request.user and request.user.is_staff) diff --git a/blog/api/serializers.py b/blog/api/serializers.py new file mode 100644 index 0000000000..5e5fa98d8e --- /dev/null +++ b/blog/api/serializers.py @@ -0,0 +1,99 @@ +from rest_framework import serializers +from blog.models import Post, Tag, Comment + +from blango_auth.models import User +from versatileimagefield.serializers import VersatileImageFieldSerializer + + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ["first_name", "last_name", "email"] + + + + + +class CommentSerializer(serializers.ModelSerializer): + id = serializers.IntegerField(required=False) + creator = UserSerializer(read_only=True) + + class Meta: + model = Comment + fields = ["id", "creator", "content", "modified_at", "created_at"] + readonly = ["modified_at", "created_at"] + + + + +class PostSerializer(serializers.ModelSerializer): + tags = serializers.SlugRelatedField( + slug_field="value", many=True, queryset=Tag.objects.all() + ) + + author = serializers.HyperlinkedRelatedField( + queryset=User.objects.all(), view_name="api_user_detail", lookup_field="email" + ) + + hero_image = VersatileImageFieldSerializer( + sizes=[ + ("full_size", "url"), + ("thumbnail", "thumbnail__100x100"), + ], + read_only=True, + ) + + + class Meta: + model = Post + #fields = "__all__" + exclude = ["ppoi"] + + readonly = ["modified_at", "created_at"] + + + +class TagField(serializers.SlugRelatedField): + def to_internal_value(self, data): + try: + return self.get_queryset().get_or_create(value=data.lower())[0] + except (TypeError, ValueError): + self.fail(f"Tag value {data} is invalid") + + +class TagSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + fields = "__all__" + + + +class PostDetailSerializer(PostSerializer): + comments = CommentSerializer(many=True) + + hero_image = VersatileImageFieldSerializer(sizes=[ + ("full_size", "url"), + ("thumbnail", "thumbnail__100x100"), + ("square_crop", "crop__200x200"), + ], + read_only=True, + ) + + def update(self, instance, validated_data): + comments = validated_data.pop("comments") + + + + instance = super(PostDetailSerializer, self).update(instance, validated_data) + + for comment_data in comments: + if comment_data.get("id"): + # comment has an ID so was pre-existing + continue + comment = Comment(**comment_data) + comment.creator = self.context["request"].user + comment.content_object = instance + comment.save() + + return instance \ No newline at end of file diff --git a/blog/api/throttling.py b/blog/api/throttling.py new file mode 100644 index 0000000000..41baa64d6e --- /dev/null +++ b/blog/api/throttling.py @@ -0,0 +1,18 @@ +from rest_framework.throttling import AnonRateThrottle, UserRateThrottle + + + +class AnonSustainedThrottle(AnonRateThrottle): + scope = "anon_sustained" + + +class AnonBurstThrottle(AnonRateThrottle): + scope = "anon_burst" + + +class UserSustainedThrottle(UserRateThrottle): + scope = "user_sustained" + + +class UserBurstThrottle(UserRateThrottle): + scope = "user_burst" \ No newline at end of file diff --git a/blog/api/urls.py b/blog/api/urls.py new file mode 100644 index 0000000000..0cb862ea2c --- /dev/null +++ b/blog/api/urls.py @@ -0,0 +1,68 @@ +from django.urls import path, include, re_path +from drf_yasg import openapi +from drf_yasg.views import get_schema_view +import os + +from rest_framework.routers import DefaultRouter + +from rest_framework.urlpatterns import format_suffix_patterns +from rest_framework.authtoken import views + +#adding JWT imports +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView + + + + +#from blog.api.views import PostList, PostDetail, UserDetail, TagViewSet +from blog.api.views import UserDetail, TagViewSet, PostViewSet + + + +router = DefaultRouter() +router.register("tags", TagViewSet) +router.register("posts", PostViewSet) + + +urlpatterns = [ + #path("posts/", PostList.as_view(), name="api_post_list"), + #path("posts/", PostDetail.as_view(), name="api_post_detail"), + path("users/", UserDetail.as_view(), name="api_user_detail"), +] + +urlpatterns = format_suffix_patterns(urlpatterns) + +schema_view = get_schema_view( + openapi.Info( + title="Blango API", + default_version="v1", + description="API for Blango Blog", + ), + url=f"https://{os.environ.get('CODIO_HOSTNAME')}-8000.codio.io/api/v1/", + public=True, +) + + +urlpatterns += [ + path("auth/", include("rest_framework.urls")), + path("token-auth/", views.obtain_auth_token), +#adding JWT token urls + path("jwt/", TokenObtainPairView.as_view(), name="jwt_obtain_pair"), + path("jwt/refresh/", TokenRefreshView.as_view(), name="jwt_refresh"), + + path("", include(router.urls)), + ##filtering to be added after router.urls + path("posts/by-time//", PostViewSet.as_view({"get": "list"}), name="posts-by-time",), + + + re_path( + r"^swagger(?P\.json|\.yaml)$", + schema_view.without_ui(cache_timeout=0), + name="schema-json", + ), + path( + "swagger/", + schema_view.with_ui("swagger", cache_timeout=0), + name="schema-swagger-ui", + ), +] diff --git a/blog/api/views.py b/blog/api/views.py new file mode 100644 index 0000000000..9ddee50887 --- /dev/null +++ b/blog/api/views.py @@ -0,0 +1,193 @@ +#querying using db models Q +from django.db.models import Q +from django.utils import timezone +from datetime import timedelta +from django.http import Http404 + +#adding reference to filters.py +from blog.api.filters import PostFilterSet + +#caching imports +from django.utils.decorators import method_decorator +from django.views.decorators.cache import cache_page +from django.views.decorators.vary import vary_on_headers, vary_on_cookie + +from rest_framework.exceptions import PermissionDenied +#end caching imports + +from rest_framework import generics, viewsets +from rest_framework.decorators import action +from rest_framework.response import Response + +from blango_auth.models import User + +from blog.api.serializers import ( + PostSerializer, + UserSerializer, + PostDetailSerializer, + TagSerializer, +) +from blog.models import Post, Tag + +from blog.api.permissions import AuthorModifyOrReadOnly, IsAdminUserForObject + +""" +class PostList(generics.ListCreateAPIView): + queryset = Post.objects.all() + serializer_class = PostSerializer + + +class PostDetail(generics.RetrieveUpdateDestroyAPIView): + permission_classes = [AuthorModifyOrReadOnly | IsAdminUserForObject] + queryset = Post.objects.all() + serializer_class = PostDetailSerializer + +""" +class PostViewSet(viewsets.ModelViewSet): + #removing and replacing to use filters.py: filterset_fields = ["author", "tags"] + filterset_class = PostFilterSet + ordering_fields = ["published_at", "author", "title", "slug"] + permission_classes = [AuthorModifyOrReadOnly | IsAdminUserForObject] + queryset = Post.objects.all() + + + #user based filtering + + def get_queryset(self): + if self.request.user.is_anonymous: + # published only + queryset = self.queryset.filter(published_at__lte=timezone.now()) + + elif not self.request.user.is_staff: + # allow all + queryset = self.queryset + else: + queryset = self.queryset.filter( + Q(published_at__lte=timezone.now()) | Q(author=self.request.user) + ) + + time_period_name = self.kwargs.get("period_name") + + if not time_period_name: + # no further filtering required + return queryset + + if time_period_name == "new": + return queryset.filter( + published_at__gte=timezone.now() - timedelta(hours=1) + ) + elif time_period_name == "today": + return queryset.filter( + published_at__date=timezone.now().date(), + ) + elif time_period_name == "week": + return queryset.filter(published_at__gte=timezone.now() - timedelta(days=7)) + else: + raise Http404( + f"Time period {time_period_name} is not valid, should be " + f"'new', 'today' or 'week'" + ) + +#end user filtering + +#serializer + + def get_serializer_class(self): + if self.action in ("list", "create"): + return PostSerializer + return PostDetailSerializer + +#caching function starts here# + + @method_decorator(cache_page(300)) + @method_decorator(vary_on_headers("Authorization")) + @method_decorator(vary_on_cookie) + @action(methods=["get"], detail=False, name="Posts by the logged in user") + def mine(self, request): + if request.user.is_anonymous: + raise PermissionDenied("You must be logged in to see which Posts are yours") + posts = self.get_queryset().filter(author=request.user) + + + #pagination starts + page = self.paginate_queryset(posts) + + if page is not None: + serializer = PostSerializer(page, many=True, context={"request": request}) + return self.get_paginated_response(serializer.data) + + #pagination ends + + serializer = PostSerializer(posts, many=True, context={"request": request}) + return Response(serializer.data) + +#We also want to cache the list of Posts for two +# minutes, which means overriding the list() view. Implement this list() # + @method_decorator(cache_page(120)) + def list(self, *args, **kwargs): + return super(PostViewSet, self).list(*args, **kwargs) + +#--caching ends--# + + +class UserDetail(generics.RetrieveAPIView): + lookup_field = "email" + queryset = User.objects.all() + serializer_class = UserSerializer + + + +#caching function starts here# + ##Since this is a view, and not a viewset, we want to override and cache on the view methods. + @method_decorator(cache_page(300)) + def get(self, *args, **kwargs): + return super(UserDetail, self).get(*args, *kwargs) + + +#--caching ends--# + + + +class TagViewSet(viewsets.ModelViewSet): + queryset = Tag.objects.all() + serializer_class = TagSerializer + + @action(methods=["get"], detail=True, name="Posts with the Tag") + def posts(self, request, pk=None): + tag = self.get_object() + + #paginaton starts here + + page = self.paginate_queryset(tag.posts) + if page is not None: + post_serializer = PostSerializer( + page, many=True, context={"request": request} + ) + return self.get_paginated_response(post_serializer.data) + + #pagination ends here + + + post_serializer = PostSerializer( + tag.posts, many=True, context={"request": request} + ) + return Response(post_serializer.data) + +#caching function starts here# +#We’ll add caching to both the list() and retrieve() methods. # + + @method_decorator(cache_page(300)) + def list(self, *args, **kwargs): + return super(TagViewSet, self).list(*args, **kwargs) + + @method_decorator(cache_page(300)) + def retrieve(self, *args, **kwargs): + return super(TagViewSet, self).retrieve(*args, **kwargs) + + +#--caching ends--# + +""" +https://arieldomino-jargontarget-8000.codio.io/api/v1/tags/1/ +""" + diff --git a/blog/apps.py b/blog/apps.py new file mode 100644 index 0000000000..94788a5eac --- /dev/null +++ b/blog/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BlogConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'blog' diff --git a/blog/data.json b/blog/data.json new file mode 100644 index 0000000000..bcb6ea11ef --- /dev/null +++ b/blog/data.json @@ -0,0 +1 @@ +{"model": "blog.comment", "pk": 1, "fields": {"creator": 1, "content": "What a great post!", "content_type": 7, "object_id": 1, "created_at": "2024-04-27T08:05:47.496Z", "modified_at": "2024-04-27T08:05:47.496Z"}}, {"model": "blog.comment", "pk": 2, "fields": {"creator": 1, "content": "This is a comment caputured on the web frontend", "content_type": 7, "object_id": 1, "created_at": "2024-04-27T09:07:47.913Z", "modified_at": "2024-04-27T09:07:47.913Z"}}, {"model": "blog.comment", "pk": 3, "fields": {"creator": 1, "content": "some extras after changing everything to use crispy forms", "content_type": 7, "object_id": 1, "created_at": "2024-04-27T10:26:20.183Z", "modified_at": "2024-04-27T10:26:20.183Z"}}, {"model": "blog.tag", "pk": 1, "fields": {"value": "Django"}}, {"model": "blog.tag", "pk": 2, "fields": {"value": "Coursera"}}, {"model": "blog.post", "pk": 1, "fields": {"author": 1, "created_at": "2024-04-25T20:17:53.544Z", "modified_at": "2024-04-25T20:18:42.894Z", "published_at": "2024-04-25T20:16:00Z", "title": "New Post Title", "slug": "new-post-title", "summary": "This is new title summary", "content": "

Content of Test Post

\r\n

this is text

", "tags": [1, 2]}}, {"model": "blog.post", "pk": 2, "fields": {"author": 1, "created_at": "2024-04-27T08:47:42.299Z", "modified_at": "2024-04-27T08:47:42.299Z", "published_at": "2024-04-27T08:46:32Z", "title": "Post 2", "slug": "post-2", "summary": "this is summary of post 2", "content": "

THIS IS POST 2

\r\n

this is post 2 text

", "tags": [1, 2]}}, {"model": "blog.post", "pk": 3, "fields": {"author": 1, "created_at": "2024-04-27T08:49:33.124Z", "modified_at": "2024-04-27T08:49:33.124Z", "published_at": "2024-04-23T08:48:00Z", "title": "Post 3", "slug": "post-3", "summary": "Post 3 dated 23rd", "content": "

title of post 3

\r\n

jlsjafkjsjsjsaiuroalaahhaofzoaujvahvahoao

", "tags": [1, 2]}}, {"model": "blango_auth.Userr", "pk": 1, "fields": {"password": "pbkdf2_sha256$720000$f9fIrc28qkOJIh8JDDNUBq$eRdASO3rp/4JnBGbKSeES1SqN2aDU4aUXOzOtQ+u3zo=", "last_login": "2024-04-25T20:15:49.518Z", "is_superuser": true, "username": "codio", "first_name": "", "last_name": "", "email": "", "is_staff": true, "is_active": true, "date_joined": "2024-04-25T20:09:08.771Z", "groups": [], "user_permissions": []}}] \ No newline at end of file diff --git a/blog/forms.py b/blog/forms.py new file mode 100644 index 0000000000..96ec088576 --- /dev/null +++ b/blog/forms.py @@ -0,0 +1,17 @@ +from django import forms +from crispy_forms.layout import Submit +from crispy_forms.helper import FormHelper + + +from blog.models import Comment + + +class CommentForm(forms.ModelForm): + class Meta: + model = Comment + fields = ["content"] + + def __init__(self, *args, **kwargs): + super(CommentForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.add_input(Submit('submit', 'Submit')) diff --git a/blog/migrations/0001_initial.py b/blog/migrations/0001_initial.py new file mode 100644 index 0000000000..0a6ab378d8 --- /dev/null +++ b/blog/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# Generated by Django 5.0.4 on 2024-04-25 20:12 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.TextField(max_length=100)), + ], + ), + migrations.CreateModel( + name='Post', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('modified_at', models.DateTimeField(auto_now=True)), + ('published_at', models.DateTimeField(blank=True, null=True)), + ('title', models.TextField(max_length=100)), + ('slug', models.SlugField()), + ('summary', models.TextField(max_length=500)), + ('content', models.TextField()), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ('tags', models.ManyToManyField(related_name='posts', to='blog.tag')), + ], + ), + ] diff --git a/blog/migrations/0002_comment.py b/blog/migrations/0002_comment.py new file mode 100644 index 0000000000..948f72985b --- /dev/null +++ b/blog/migrations/0002_comment.py @@ -0,0 +1,29 @@ +# Generated by Django 5.0.4 on 2024-04-27 07:59 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0001_initial'), + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.TextField()), + ('object_id', models.PositiveIntegerField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('modified_at', models.DateTimeField(auto_now=True)), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/blog/migrations/0003_alter_tag_options_alter_comment_created_at_and_more.py b/blog/migrations/0003_alter_tag_options_alter_comment_created_at_and_more.py new file mode 100644 index 0000000000..d653109b1f --- /dev/null +++ b/blog/migrations/0003_alter_tag_options_alter_comment_created_at_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 5.0.4 on 2024-05-25 08:00 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0002_comment'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterModelOptions( + name='tag', + options={'ordering': ['value']}, + ), + migrations.AlterField( + model_name='comment', + name='created_at', + field=models.DateTimeField(auto_now_add=True, db_index=True), + ), + migrations.AlterField( + model_name='comment', + name='object_id', + field=models.PositiveIntegerField(db_index=True), + ), + migrations.AlterField( + model_name='post', + name='published_at', + field=models.DateTimeField(blank=True, db_index=True, null=True), + ), + migrations.AlterField( + model_name='tag', + name='value', + field=models.TextField(max_length=100, unique=True), + ), + migrations.CreateModel( + name='AuthorProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('bio', models.TextField()), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/blog/migrations/0004_post_hero_image_post_ppoi.py b/blog/migrations/0004_post_hero_image_post_ppoi.py new file mode 100644 index 0000000000..2264cd4536 --- /dev/null +++ b/blog/migrations/0004_post_hero_image_post_ppoi.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.4 on 2024-05-26 15:02 + +import versatileimagefield.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0003_alter_tag_options_alter_comment_created_at_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='post', + name='hero_image', + field=versatileimagefield.fields.VersatileImageField(blank=True, null=True, upload_to='hero_images'), + ), + migrations.AddField( + model_name='post', + name='ppoi', + field=versatileimagefield.fields.PPOIField(blank=True, default='0.5x0.5', editable=False, max_length=20, null=True), + ), + ] diff --git a/blog/migrations/__init__.py b/blog/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blog/migrations/__pycache__/0001_initial.cpython-311.pyc b/blog/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000000..a06868eb6c Binary files /dev/null and b/blog/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/blog/migrations/__pycache__/0002_comment.cpython-311.pyc b/blog/migrations/__pycache__/0002_comment.cpython-311.pyc new file mode 100644 index 0000000000..d4d38a0248 Binary files /dev/null and b/blog/migrations/__pycache__/0002_comment.cpython-311.pyc differ diff --git a/blog/migrations/__pycache__/0003_alter_tag_options_alter_comment_created_at_and_more.cpython-311.pyc b/blog/migrations/__pycache__/0003_alter_tag_options_alter_comment_created_at_and_more.cpython-311.pyc new file mode 100644 index 0000000000..77ed6504a8 Binary files /dev/null and b/blog/migrations/__pycache__/0003_alter_tag_options_alter_comment_created_at_and_more.cpython-311.pyc differ diff --git a/blog/migrations/__pycache__/0004_post_hero_image_post_ppoi.cpython-311.pyc b/blog/migrations/__pycache__/0004_post_hero_image_post_ppoi.cpython-311.pyc new file mode 100644 index 0000000000..7b8466c31b Binary files /dev/null and b/blog/migrations/__pycache__/0004_post_hero_image_post_ppoi.cpython-311.pyc differ diff --git a/blog/migrations/__pycache__/__init__.cpython-311.pyc b/blog/migrations/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000..1e0378c9ab Binary files /dev/null and b/blog/migrations/__pycache__/__init__.cpython-311.pyc differ diff --git a/blog/models.py b/blog/models.py new file mode 100644 index 0000000000..477782627b --- /dev/null +++ b/blog/models.py @@ -0,0 +1,59 @@ +from django.db import models +from django.conf import settings +from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey +from django.contrib.contenttypes.models import ContentType + +#versatile immage +from versatileimagefield.fields import VersatileImageField, PPOIField + + + + +class Tag(models.Model): + value = models.TextField(max_length=100, unique=True) + + class Meta: + ordering = ["value"] + + def __str__(self): + return self.value + + + +class Comment(models.Model): + creator = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + content = models.TextField() + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField(db_index=True) + content_object = GenericForeignKey("content_type", "object_id") + created_at = models.DateTimeField(auto_now_add=True, db_index=True) + modified_at = models.DateTimeField(auto_now=True) + + +class Post(models.Model): + author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) + created_at = models.DateTimeField(auto_now_add=True) + modified_at = models.DateTimeField(auto_now=True) + published_at = models.DateTimeField(blank=True, null=True, db_index=True) + title = models.TextField(max_length=100) + slug = models.SlugField() + summary = models.TextField(max_length=500) + content = models.TextField() + tags = models.ManyToManyField(Tag, related_name="posts") + comments = GenericRelation(Comment) + hero_image = VersatileImageField(upload_to="hero_images", ppoi_field="ppoi", null=True, blank=True) + ppoi = PPOIField(null=True, blank=True) + + + def __str__(self): + return self.title + + + +class AuthorProfile(models.Model): + user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="profile") + bio = models.TextField() + + def __str__(self): + return f"{self.__class__.__name__} object for {self.user}" + diff --git a/blog/static/blog/blog copy.js b/blog/static/blog/blog copy.js new file mode 100644 index 0000000000..cbe2fddb79 --- /dev/null +++ b/blog/static/blog/blog copy.js @@ -0,0 +1,232 @@ + + +//REACTJS CODE STARTS HERE +class ClickButton extends React.Component { + state = { + wasClicked: false + } + + handleClick () { + this.setState( + {wasClicked: true} + ) + } + + + +//this is JSX + render () { + let buttonText + + if (this.state.wasClicked) + buttonText = 'Clicked!' + else + buttonText = 'Click Me' + + return + } +//end JSX + + + /* + render () { + let buttonText + + if (this.state.wasClicked) + buttonText = 'Clicked!' + else + buttonText = 'Click Me' + return React.createElement( + 'button', + { + className: 'btn btn-primary mt-2', + onClick: () => { + this.handleClick() + } + }, + buttonText + ) + }*/ + +} + +//mount the reactjs code to the html +const domContainer = document.getElementById('react_root') +ReactDOM.render( + React.createElement(ClickButton), + domContainer +) + + +//REACTJS ENDS HERE + + + +/* + // CONSOLE JAVASCRIPT - BACKEND ONLY STARTS HERE + +function resolvedCallback(data) { + console.log('Resolved with data ' + data) +} + +function rejectedCallback(message) { + console.log('Rejected with message ' + message) +} + +const lazyAdd = function (a, b) { + const doAdd = (resolve, reject) => { + if (typeof a !== "number" || typeof b !== "number") { + reject("a and b must both be numbers") + } else { + const sum = a + b + resolve(sum) + } + } + + return new Promise(doAdd) +} + +const p = lazyAdd(3, 4) +p.then(resolvedCallback, rejectedCallback) + +lazyAdd("nan", "alsonan").then(resolvedCallback, rejectedCallback) + + + + + + +const theNumber = 1 +let yourName = 'Ben' + +if (theNumber === 1) { + let yourName = 'Leo' + alert(yourName) +} + +alert(yourName) + + +console.time('myTimer') +console.count('counter1') +console.log('A normal log message') +console.warn('Warning: something bad might happen') +console.error('Something bad did happen!') +console.count('counter1') +console.log('All the things above took this long to happen:') +console.timeEnd('myTimer') + + + +function sayHello(yourName) { + if (yourName === undefined) { + console.log('Hello, no name') + } else { + console.log('Hello, ' + yourName) + } +} + +const yourName = 'Your Name' // Put your name here + +console.log('Before setTimeout') + +setTimeout(() => { + sayHello(yourName) + }, 2000 +) + +console.log('After setTimeout') + + + +for(let i = 0; i < 10; i += 1) { + console.log('for loop i: ' + i) +} + +let j = 0 +while(j < 10) { + console.log('while loop j: ' + j) + j += 1 +} + +let k = 10 + +do { + console.log('do while k: ' + k) +} while(k < 10) + +const numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + +numbers.forEach((value => { + console.log('For each value ' + value) +})) + +const doubled = numbers.map(value => value * 2) + +console.log('Here are the doubled numbers') + +console.log(doubled) + + + +class Greeter { + constructor (name) { + this.name = name + } + + getGreeting () { + if (this.name === undefined) { + return 'Hello, no name' + } + + return 'Hello, ' + this.name + } + + showGreeting (greetingMessage) { + console.log(greetingMessage) + } + + greet () { + this.showGreeting(this.getGreeting()) + } +} + +const g = new Greeter('Patchy') // Put your name here if you like +g.greet() + + + +class DelayedGreeter extends Greeter { + delay = 2000 + + constructor (name, delay) { + super(name) + if (delay !== undefined) { + this.delay = delay + } + } + + greet () { + setTimeout( + () => { + this.showGreeting(this.getGreeting()) + }, this.delay + ) + } +} + +const dg2 = new DelayedGreeter('Patso 2 Seconds') +dg2.greet() + +const dg1 = new DelayedGreeter('Patchu 1 Second', 1000) +dg1.greet() +*/ \ No newline at end of file diff --git a/blog/static/blog/blog.js b/blog/static/blog/blog.js new file mode 100644 index 0000000000..5e9b8a233b --- /dev/null +++ b/blog/static/blog/blog.js @@ -0,0 +1,150 @@ +/*this is the fetch and react hooks + +['/api/v1/posts/', '/', '/abadurl/'].forEach(url => { + fetch(url).then(response => { + if (response.status !== 200) { + throw new Error('Invalid status from server: ' + response.statusText) + } + + return response.json() + }).then(data => { + // do something with data, for example + console.log(data) + }).catch(e => { + console.error(e) + }) +}) + +*/ + + + +//this is the post-table code, do not remove +//this is the post-table code, do not remove + +class PostRow extends React.Component { + render () { + const post = this.props.post + + let thumbnail + + if (post.hero_image.thumbnail) { + thumbnail = + } else { + thumbnail = '-' + } + + return + {post.title} + + {thumbnail} + + {post.tags.join(', ')} + {post.slug} + {post.summary} + View + + } +} + +class PostTable extends React.Component { + + state = { + dataLoaded: false, + data: null + } + + componentDidMount () { + fetch(this.props.url).then(response => { + + if (response.status !== 200) { + throw new Error('Invalid status from server: ' + response.statusText) + } + + return response.json() + }).then(data => { + this.setState({ + dataLoaded: true, + data: data + }) + }).catch(e => { + console.error(e) + this.setState({ + dataLoaded: true, + data: { + results: [] + } + }) + }) + } + + + + + + /*state = { + dataLoaded: true, + data: { + results: [ + { + id: 15, + tags: [ + 'django', 'react' + ], + 'hero_image': { + 'thumbnail': '/media/__sized__/hero_images/snake-419043_1920-thumbnail-100x100-70.jpg', + 'full_size': '/media/hero_images/snake-419043_1920.jpg' + }, + title: 'Test Post', + slug: 'test-post', + summary: 'A test post, created for Django/React.' + } + ] + } + }*/ + + render () { + let rows + if (this.state.dataLoaded) { + if (this.state.data.results.length) { + rows = this.state.data.results.map(post => ) + } else { + rows = + No results found. + + } + } else { + rows = + Loading… + + } + + return + + + + + + + + + + + + {rows} + +
TitleImageTagsSlugSummaryLink
+ } + +} + + + +const domContainer = document.getElementById('react_root') +ReactDOM.render( + React.createElement( + PostTable, + {url: postListUrl} + ), + domContainer +) diff --git a/blog/templatetags/__init__.py b/blog/templatetags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blog/templatetags/__pycache__/__init__.cpython-311.pyc b/blog/templatetags/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000..62b79dc635 Binary files /dev/null and b/blog/templatetags/__pycache__/__init__.cpython-311.pyc differ diff --git a/blog/templatetags/__pycache__/blog_extras.cpython-311.pyc b/blog/templatetags/__pycache__/blog_extras.cpython-311.pyc new file mode 100644 index 0000000000..b6c6c52509 Binary files /dev/null and b/blog/templatetags/__pycache__/blog_extras.cpython-311.pyc differ diff --git a/blog/templatetags/blog_extras.py b/blog/templatetags/blog_extras.py new file mode 100644 index 0000000000..f0884aa5f3 --- /dev/null +++ b/blog/templatetags/blog_extras.py @@ -0,0 +1,72 @@ +from django.utils.html import format_html +from django import template +from django.contrib.auth import get_user_model +from blog.models import Post, Comment + + +register = template.Library() + + +user_model = get_user_model() + +@register.simple_tag(takes_context=True) +def author_details_tag(context): + request = context["request"] + current_user = request.user + post = context["post"] + author = post.author + + if author == current_user: + return format_html("me") + + if author.first_name and author.last_name: + name = f"{author.first_name} {author.last_name}" + else: + name = f"{author.username}" + + if author.email: + prefix = format_html('', + author.email) + suffix = format_html("") + + else: + prefix = "" + suffix = "" + + return format_html("{}{}{}", prefix, name, suffix) + + + + +@register.simple_tag +def row(): + return format_html('
') + + +@register.simple_tag +def endrow(): + return format_html("
") + +@register.simple_tag +def row(extra_classes=""): + return format_html('
', extra_classes) + +@register.simple_tag +def col(extra_classes=""): + return format_html('
', extra_classes) + +@register.simple_tag +def endcol(): + return format_html("
") + +@register.inclusion_tag("blog/post-list.html") +def recent_posts(post): + posts = Post.objects.exclude(pk=post.pk)[:5] + return {"title": "Recent Posts", "posts": posts} + + + + + + + diff --git a/blog/test_post_api.py b/blog/test_post_api.py new file mode 100644 index 0000000000..1ee04a21d7 --- /dev/null +++ b/blog/test_post_api.py @@ -0,0 +1,114 @@ +from datetime import datetime + +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.utils import timezone +from pytz import UTC +from rest_framework.authtoken.models import Token +from rest_framework.test import APIClient + +from blog.models import Post + + + +class PostApiTestCase(TestCase): + + def setUp(self): + self.u1 = get_user_model().objects.create_user( + email="test@example.com", password="password" + + ) + + + self.u2 = get_user_model().objects.create_user( + + email="test2@example.com", password="password2" + + ) + + posts = [ + Post.objects.create( + author=self.u1, + published_at=timezone.now(), + title="Post 1 Title", + slug="post-1-slug", + summary="Post 1 Summary", + content="Post 1 Content", + ), + Post.objects.create( + author=self.u2, + published_at=timezone.now(), + title="Post 2 Title", + slug="post-2-slug", + summary="Post 2 Summary", + content="Post 2 Content", + ), + ] + + # let us look up the post info by ID + self.post_lookup = {p.id: p for p in posts} + + # override test client + self.client = APIClient() + token = Token.objects.create(user=self.u1) + self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key) + + + def test_post_list(self): + resp = self.client.get("/api/v1/posts/") + + #added to list results in json response for pagination + data = resp.json()["results"] + + self.assertEqual(len(data), 2) + + for post_dict in data: + post_obj = self.post_lookup[post_dict["id"]] + self.assertEqual(post_obj.title, post_dict["title"]) + self.assertEqual(post_obj.slug, post_dict["slug"]) + self.assertEqual(post_obj.summary, post_dict["summary"]) + self.assertEqual(post_obj.content, post_dict["content"]) + self.assertTrue( + post_dict["author"].endswith(f"/api/v1/users/{post_obj.author.email}") + ) + self.assertEqual( + post_obj.published_at, + datetime.strptime( + post_dict["published_at"], "%Y-%m-%dT%H:%M:%S.%fZ" + ).replace(tzinfo=UTC), + ) + + + def test_unauthenticated_post_create(self): + # unset credentials so we are an anonymous user + self.client.credentials() + post_dict = { + "title": "Test Post", + "slug": "test-post-3", + "summary": "Test Summary", + "content": "Test Content", + "author": "http://testserver/api/v1/users/test@example.com", + "published_at": "2021-01-10T09:00:00Z", + } + resp = self.client.post("/api/v1/posts/", post_dict) + self.assertEqual(resp.status_code, 401) + self.assertEqual(Post.objects.all().count(), 2) + + def test_post_create(self): + post_dict = { + "title": "Test Post", + "slug": "test-post-3", + "summary": "Test Summary", + "content": "Test Content", + "author": "http://testserver/api/v1/users/test@example.com", + "published_at": "2021-01-10T09:00:00Z", + } + resp = self.client.post("/api/v1/posts/", post_dict) + post_id = resp.json()["id"] + post = Post.objects.get(pk=post_id) + self.assertEqual(post.title, post_dict["title"]) + self.assertEqual(post.slug, post_dict["slug"]) + self.assertEqual(post.summary, post_dict["summary"]) + self.assertEqual(post.content, post_dict["content"]) + self.assertEqual(post.author, self.u1) + self.assertEqual(post.published_at, datetime(2021, 1, 10, 9, 0, 0, tzinfo=UTC)) diff --git a/blog/test_tag_api.py b/blog/test_tag_api.py new file mode 100644 index 0000000000..3d54b40ad2 --- /dev/null +++ b/blog/test_tag_api.py @@ -0,0 +1,49 @@ +from django.test import LiveServerTestCase +from requests.auth import HTTPBasicAuth +from rest_framework.test import RequestsClient + +from django.contrib.auth import get_user_model +from blog.models import Tag + + +class TagApiTestCase(LiveServerTestCase): + def setUp(self): + get_user_model().objects.create_user( + email="testuser@example.com", password="password" + ) + + self.tag_values = {"tag1", "tag2", "tag3", "tag4"} + for t in self.tag_values: + Tag.objects.create(value=t) + self.client = RequestsClient() + + def test_tag_list(self): + resp = self.client.get(self.live_server_url + "/api/v1/tags/") + self.assertEqual(resp.status_code, 200) + + #updated resonse Json to return results + data = resp.json()["results"] + + self.assertEqual(len(data), 4) + self.assertEqual(self.tag_values, {t["value"] for t in data}) + + def test_tag_create_basic_auth(self): + self.client.auth = HTTPBasicAuth("testuser@example.com", "password") + resp = self.client.post( + self.live_server_url + "/api/v1/tags/", {"value": "tag5"} + ) + self.assertEqual(resp.status_code, 201) + self.assertEqual(Tag.objects.all().count(), 5) + + def test_tag_create_token_auth(self): + token_resp = self.client.post( + self.live_server_url + "/api/v1/token-auth/", + {"username": "testuser@example.com", "password": "password"}, + ) + self.client.headers["Authorization"] = "Token " + token_resp.json()["token"] + + resp = self.client.post( + self.live_server_url + "/api/v1/tags/", {"value": "tag5"} + ) + self.assertEqual(resp.status_code, 201) + self.assertEqual(Tag.objects.all().count(), 5) diff --git a/blog/views.py b/blog/views.py new file mode 100644 index 0000000000..849117a80f --- /dev/null +++ b/blog/views.py @@ -0,0 +1,53 @@ +from django.utils import timezone +from blog.models import Post, Comment +from django.shortcuts import render, get_object_or_404 +from django.shortcuts import redirect +from blog.forms import CommentForm + +from django.urls import reverse + +# Create your views here. +def index(request): + posts = Post.objects.filter(published_at__lte=timezone.now()) + ''' + DATABASE QUERY OPTIMISATION...USE ONLY IF A PERMANENT QUERY SET + #.select_related("author") + #.defer("created_at", "modified_at") + #or the oppossite use + #.only("title", "summary", "content", "author", "published_at", "slug") + ''' + + return render(request, "blog/index.html", {"posts": posts}) + + +def post_detail(request, slug): + post = get_object_or_404(Post, slug=slug) + + if request.user.is_active: + if request.method == "POST": + comment_form = CommentForm(request.POST) + + if comment_form.is_valid(): + comment = comment_form.save(commit=False) + comment.content_object = post + comment.creator = request.user + comment.save() + return redirect(request.path_info) + else: + comment_form = CommentForm() + else: + comment_form = None + + #return render(request, "blog/post-detail.html", {"post": post}) + + return render(request, "blog/post-detail.html", {"post": post, "comment_form": comment_form} +) + +def get_ip(request): + from django.http import HttpResponse + return HttpResponse(request.META['REMOTE_ADDR']) + +def post_table(request): + return render( + request, "blog/post-table.html", {"post_list_url": reverse("post-list")} + ) diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000000..dcbc819ab1 Binary files /dev/null and b/db.sqlite3 differ diff --git a/manage.py b/manage.py index c66b327f71..9d249659c7 100644 --- a/manage.py +++ b/manage.py @@ -7,8 +7,12 @@ def main(): """Run administrative tasks.""" os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blango.settings') + os.environ.setdefault("DJANGO_CONFIGURATION", "Dev") #--added to read configurations + try: - from django.core.management import execute_from_command_line + #from django.core.management import execute_from_command_line + from configurations.management import execute_from_command_line + except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " @@ -20,3 +24,4 @@ def main(): if __name__ == '__main__': main() + diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000000..0e8bd41381 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,41 @@ + + + + + + + {% block title %}Welcome to Blango{% endblock %} + + + + +

Hello World

+ + + + + {% block content %} + + {% endblock %} + + + + + + + + \ No newline at end of file diff --git a/templates/blog/index.html b/templates/blog/index.html new file mode 100644 index 0000000000..6076722d7d --- /dev/null +++ b/templates/blog/index.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% load blog_extras %} +{% block content %} +

Blog Posts

+ + +{% for post in posts %} + +{% row "border-bottom" %} +
+

{{ post.title }}

+ + {% if post.hero_image %} + + {% endif %} + + + {% include "blog/post-byline.html" %} +

{{ post.summary }}

+

({{ post.content|wordcount }} words)Read More +

+
+{% endrow %} + +{% endfor %} +{% endblock %} + + + diff --git a/templates/blog/post-byline.html b/templates/blog/post-byline.html new file mode 100644 index 0000000000..c440349856 --- /dev/null +++ b/templates/blog/post-byline.html @@ -0,0 +1,2 @@ +{% load blog_extras %} +By {% author_details_tag %} on {{post.published_at|date:"M, d Y" }} \ No newline at end of file diff --git a/templates/blog/post-comments.html b/templates/blog/post-comments.html new file mode 100644 index 0000000000..17840deb3d --- /dev/null +++ b/templates/blog/post-comments.html @@ -0,0 +1,33 @@ +{% load blog_extras crispy_forms_tags %} + +

Comments

+ +{% for comment in post.comments.all %} +{% row "border-top pt-2" %} + {% col %} +
Posted by {{ comment.creator }} at {{ comment.created_at|date:"M, d Y h:i" }}
+ {% endcol %} +{% endrow %} +{% row "border-bottom" %} + {% col %} +

{{ comment.content }}

+ {% endcol %} +{% endrow %} +{% empty %} + {% row "border-top border-bottom" %} + {% col %} +

No comments.

+ {% endcol %} + {% endrow %} +{% endfor %} + +{% if request.user.is_active %} +{% row "mt-4" %} + {% col %} +

Add Comment

+ + {% crispy comment_form %} + + {% endcol %} +{% endrow %} +{% endif %} \ No newline at end of file diff --git a/templates/blog/post-detail.html b/templates/blog/post-detail.html new file mode 100644 index 0000000000..ca12a3424b --- /dev/null +++ b/templates/blog/post-detail.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} +{% load blog_extras %} +{% block content %} + + +

{{ post.title }}

+ +{% row %} +
+ {% include "blog/post-byline.html" %} +
+{% endrow %} + +{% row %} +{% if post.hero_image %} + {% row %} + {% col %} + + {% endcol %} + {% endrow %} +{% endif %} +{% endrow %} + + +{% row %} +
{{ post.content|safe }} +
+{% endrow %} + +{% if post.author.profile %} +{% row %} +{% col %} +

About the author

+

{{ post.author.profile.bio }}

+{% endcol %} +{% endrow %} +{% endif %} + +{% include "blog/post-comments.html" %} + + +{% row %} +{% col %} + {% recent_posts post %} +{% endcol %} +{% endrow %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/blog/post-list.html b/templates/blog/post-list.html new file mode 100644 index 0000000000..5343856bb4 --- /dev/null +++ b/templates/blog/post-list.html @@ -0,0 +1,6 @@ +

{{ title }}

+
    + {% for post in posts %} +
  • Read More
  • + {% endfor %} +
\ No newline at end of file diff --git a/templates/blog/post-table.html b/templates/blog/post-table.html new file mode 100644 index 0000000000..a3a5ca1224 --- /dev/null +++ b/templates/blog/post-table.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Post Table{% endblock %} + +{% block content %} + +
+
+
+
+
+ + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/rest_framework/api.html b/templates/rest_framework/api.html new file mode 100644 index 0000000000..54cb7bc898 --- /dev/null +++ b/templates/rest_framework/api.html @@ -0,0 +1,19 @@ +{% extends "rest_framework/base.html" %} +{% block title %}{% if name %}{{ name }} – {% endif %} Blango REST API{% endblock %} + + +{% block branding %} + + Blango REST API + +{% endblock %} + + +{% block style %} + {{ block.super }} + +{% endblock %} \ No newline at end of file