diff --git a/blango/__pycache__/__init__.cpython-36.pyc b/blango/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000..a13d8b5dbf Binary files /dev/null and b/blango/__pycache__/__init__.cpython-36.pyc differ diff --git a/blango/__pycache__/settings.cpython-36.pyc b/blango/__pycache__/settings.cpython-36.pyc new file mode 100644 index 0000000000..a3b8accf37 Binary files /dev/null and b/blango/__pycache__/settings.cpython-36.pyc differ diff --git a/blango/__pycache__/urls.cpython-36.pyc b/blango/__pycache__/urls.cpython-36.pyc new file mode 100644 index 0000000000..4f25590de6 Binary files /dev/null and b/blango/__pycache__/urls.cpython-36.pyc differ diff --git a/blango/__pycache__/wsgi.cpython-36.pyc b/blango/__pycache__/wsgi.cpython-36.pyc new file mode 100644 index 0000000000..b430092578 Binary files /dev/null and b/blango/__pycache__/wsgi.cpython-36.pyc differ diff --git a/blango/settings.py b/blango/settings.py index f9209bef27..d40f036828 100644 --- a/blango/settings.py +++ b/blango/settings.py @@ -1,7 +1,7 @@ """ Django settings for blango project. -Generated by 'django-admin startproject' using Django 3.2.7. +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/ @@ -10,7 +10,13 @@ https://docs.djangoproject.com/en/3.2/ref/settings/ """ +import os from pathlib import Path +from configurations import Configuration +from configurations import values +from datetime import timedelta + +# class Dev: # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -20,13 +26,19 @@ # 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-' +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! DEBUG = True -ALLOWED_HOSTS = [] - +ALLOWED_HOSTS = ['*'] +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 @@ -37,16 +49,31 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.contrib.sites', + 'blog', + 'crispy_forms', + 'crispy_bootstrap5', + 'debug_toolbar', + 'blango_auth', + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + 'allauth.socialaccount.providers.google', + 'rest_framework.authtoken', + 'drf_yasg', + 'django_filters', + 'versatileimagefield', ] MIDDLEWARE = [ + 'debug_toolbar.middleware.DebugToolbarMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', +# 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', +# 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'blango.urls' @@ -54,7 +81,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + "DIRS": [BASE_DIR / "templates"], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -99,14 +126,21 @@ }, ] +PASSWORD_HASHERS = [ + 'django.contrib.auth.hashers.Argon2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', + 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', +] + + # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' - +TIME_ZONE = "UTC" USE_I18N = True USE_L10N = True @@ -123,3 +157,88 @@ # 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" + +# CACHES = { +# "default": { +# "BACKEND": "django.core.cache.backends.db.DatabaseCache", +# "LOCATION": "my_cache_table", +# } +# } +INTERNAL_IPS = ["192.168.10.226"] +AUTH_USER_MODEL = "blango_auth.User" +ACCOUNT_ACTIVATION_DAYS = 7 +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +# + +# class Prod(Dev): +# DEBUG = False +# SECRET_KEY = values.SecretValue() +# ALLOWED_HOSTS = values.ListValue(["localhost", "0.0.0.0", ".codio.io"]) +# DATABASES = { +# "default": dj_database_url.config(default=f"sqlite:///{BASE_DIR}/db.sqlite3"), +# "alternative": dj_database_url.config( +# "ALTERNATIVE_DATABASE_URL", +# default=f"sqlite:///{BASE_DIR}/alternative_db.sqlite3", +# ), +# } + +SITE_ID = 1 + +ACCOUNT_USER_MODEL_USERNAME_FIELD = None +ACCOUNT_EMAIL_REQUIRED = True +ACCOUNT_USERNAME_REQUIRED = False +ACCOUNT_AUTHENTICATION_METHOD = "email" +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": [ + # "rest_framework.permissions.IsAuthenticated", + "rest_framework.authentication.BasicAuthentication", + "rest_framework.authentication.SessionAuthentication", + "rest_framework.authentication.TokenAuthentication", + "rest_framework_simplejwt.authentication.JWTAuthentication" + # 'rest_framework.permissions.IsAuthenticated' + ], + # "DEFAULT_PERMISSION_CLASSES": [ + # "rest_framework.permissions.IsAuthenticatedOrReadOnly" + # ], + + "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", + }, + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 100, + "DEFAULT_FILTER_BACKENDS": [ + "django_filters.rest_framework.DjangoFilterBackend", + "rest_framework.filters.OrderingFilter" + ], + + +} + + +SWAGGER_SETTINGS = { + "SECURITY_DEFINITIONS": { + "Token": {"type": "apiKey", "name": "Authorization", "in": "header"}, + "Basic": {"type": "basic"}, + } +} + +SIMPLE_JWT = { +"ACCESS_TOKEN_LIFETIME": timedelta(days=1), +"REFRESH_TOKEN_LIFETIME": timedelta(days=7), +} + +MEDIA_ROOT = BASE_DIR / "media" +MEDIA_URL = "/media/" + diff --git a/blango/templates/base.html b/blango/templates/base.html new file mode 100644 index 0000000000..6a1e1a602f --- /dev/null +++ b/blango/templates/base.html @@ -0,0 +1,16 @@ + + + + + + + Hello, world! + + +

Hello, world!

+ {% block content %} + + {% endblock %} + + + diff --git a/blango/templates/blog/index.html b/blango/templates/blog/index.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blango/templates/rest_framework/api.html b/blango/templates/rest_framework/api.html new file mode 100644 index 0000000000..24eacc607c --- /dev/null +++ b/blango/templates/rest_framework/api.html @@ -0,0 +1,17 @@ +{% 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 %} diff --git a/blango/urls.py b/blango/urls.py index cde05802f9..6738a83097 100644 --- a/blango/urls.py +++ b/blango/urls.py @@ -1,3 +1,5 @@ +import blog.views + """blango URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: @@ -13,9 +15,87 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +import debug_toolbar +from django.conf import settings from django.contrib import admin -from django.urls import path +from django.urls import path, include + +import blog.views +import blango_auth.views +from django_registration.backends.activation.views import RegistrationView +from blango_auth.forms import BlangoRegistrationForm +from django.conf.urls.static import static + + +import logging + +logger = logging.getLogger(__name__) + +from django.urls import re_path + + +from drf_yasg import openapi +from drf_yasg.views import get_schema_view + +schema_view = get_schema_view( + openapi.Info( + title="Blango API", + default_version="v1", + description="API for Blango Blog", + ), + public=True, +) + + + urlpatterns = [ path('admin/', admin.site.urls), + path("", blog.views.index), + path("ip/", blog.views.get_ip), + path("accounts/", include("django.contrib.auth.urls")), + path("accounts/profile/", blango_auth.views.profile, name="profile"), + path('accounts/', include('django_registration.backends.activation.urls')), + path( + "accounts/register/", + RegistrationView.as_view(form_class=BlangoRegistrationForm), + name="django_registration_register", + ), + path("accounts/", include("django_registration.backends.activation.urls")), + path("accounts/", include("allauth.urls")), + path("accounts/", include("allauth.urls")), + path("accounts/", include("allauth.urls")), + # for function based view below + # path("api/v1/", include("blog.api_urls")), + path("api/v1/", include("blog.api.urls")), + path("post-table/", blog.views.post_table, name="blog-post-table"), + + + # path("post//", blog.views.post_detail, name="blog-post-detail") ] + +# urlpatterns += [ +# # ... +# re_path( +# r"^swagger(?P\.json|\.yaml)$", +# schema_view.without_ui(cache_timeout=0), +# name="schema-json", +# ), +# # ... +# ] +urlpatterns += [ + # ... + path( + r"^swagger/$", + schema_view.with_ui("swagger", cache_timeout=0), + name="schema-swagger-ui", + ), + # ... +] + + + +if settings.DEBUG: + urlpatterns += [ + path("__debug__/", include(debug_toolbar.urls)), + ]+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/blango/wsgi.py b/blango/wsgi.py index 83565cf12c..a246900546 100644 --- a/blango/wsgi.py +++ b/blango/wsgi.py @@ -12,5 +12,7 @@ from django.core.wsgi import get_wsgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blango.settings') +os.environ.setdefault("DJANGO_CONFIGURATION", "Prod") + 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-36.pyc b/blango_auth/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000..64a7ea8fd5 Binary files /dev/null and b/blango_auth/__pycache__/__init__.cpython-36.pyc differ diff --git a/blango_auth/__pycache__/admin.cpython-36.pyc b/blango_auth/__pycache__/admin.cpython-36.pyc new file mode 100644 index 0000000000..12f42e315e Binary files /dev/null and b/blango_auth/__pycache__/admin.cpython-36.pyc differ diff --git a/blango_auth/__pycache__/apps.cpython-36.pyc b/blango_auth/__pycache__/apps.cpython-36.pyc new file mode 100644 index 0000000000..5f447e5c5b Binary files /dev/null and b/blango_auth/__pycache__/apps.cpython-36.pyc differ diff --git a/blango_auth/__pycache__/forms.cpython-36.pyc b/blango_auth/__pycache__/forms.cpython-36.pyc new file mode 100644 index 0000000000..fa32d56099 Binary files /dev/null and b/blango_auth/__pycache__/forms.cpython-36.pyc differ diff --git a/blango_auth/__pycache__/models.cpython-36.pyc b/blango_auth/__pycache__/models.cpython-36.pyc new file mode 100644 index 0000000000..cb36dcadf6 Binary files /dev/null and b/blango_auth/__pycache__/models.cpython-36.pyc differ diff --git a/blango_auth/__pycache__/tests.cpython-36.pyc b/blango_auth/__pycache__/tests.cpython-36.pyc new file mode 100644 index 0000000000..e330fe69b3 Binary files /dev/null and b/blango_auth/__pycache__/tests.cpython-36.pyc differ diff --git a/blango_auth/__pycache__/views.cpython-36.pyc b/blango_auth/__pycache__/views.cpython-36.pyc new file mode 100644 index 0000000000..60b3d7acc6 Binary files /dev/null and b/blango_auth/__pycache__/views.cpython-36.pyc differ diff --git a/blango_auth/admin.py b/blango_auth/admin.py new file mode 100644 index 0000000000..a2fd9c75eb --- /dev/null +++ b/blango_auth/admin.py @@ -0,0 +1,41 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from blango_auth.models import User +from django.utils.translation import gettext_lazy as _ + +# Register your models here. +#admin.site.register(User, UserAdmin) + + +class BlangoUserAdmin(UserAdmin): + fieldsets = ( + (None, {"fields": ("email", "password")}), + (_("Personal info"), {"fields": ("first_name", "last_name")}), + ( + _("Permissions"), + { + "fields": ( + "is_active", + "is_staff", + "is_superuser", + "groups", + "user_permissions", + ) + }, + ), + (_("Important dates"), {"fields": ("last_login", "date_joined")}), + ) + add_fieldsets = ( + ( + None, + { + "classes": ("wide",), + "fields": ("email", "password1", "password2"), + }, + ), + ) + list_display = ("email", "first_name", "last_name", "is_staff") + search_fields = ("email", "first_name", "last_name") + ordering = ("email",) + +admin.site.register(User, BlangoUserAdmin) 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/forms.py b/blango_auth/forms.py new file mode 100644 index 0000000000..80c9376fa5 --- /dev/null +++ b/blango_auth/forms.py @@ -0,0 +1,15 @@ +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Submit +from django_registration.forms import RegistrationForm + +from blango_auth.models import User + + +class BlangoRegistrationForm(RegistrationForm): + class Meta(RegistrationForm.Meta): + model = User + + def __init__(self, *args, **kwargs): + super(BlangoRegistrationForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.add_input(Submit("submit", "Register")) diff --git a/blango_auth/migrations/0001_initial.py b/blango_auth/migrations/0001_initial.py new file mode 100644 index 0000000000..bbac34291a --- /dev/null +++ b/blango_auth/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 3.2.6 on 2024-01-02 01:25 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +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/0002_auto_20240102_0232.py b/blango_auth/migrations/0002_auto_20240102_0232.py new file mode 100644 index 0000000000..bcff315ddb --- /dev/null +++ b/blango_auth/migrations/0002_auto_20240102_0232.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.6 on 2024-01-02 02:32 + +import blango_auth.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blango_auth', '0001_initial'), + ] + + operations = [ + migrations.AlterModelManagers( + name='user', + managers=[ + ('objects', blango_auth.models.BlangoUserManager()), + ], + ), + migrations.RemoveField( + model_name='user', + name='username', + ), + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(max_length=254, unique=True, verbose_name='email address'), + ), + ] 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-36.pyc b/blango_auth/migrations/__pycache__/0001_initial.cpython-36.pyc new file mode 100644 index 0000000000..9a39432c92 Binary files /dev/null and b/blango_auth/migrations/__pycache__/0001_initial.cpython-36.pyc differ diff --git a/blango_auth/migrations/__pycache__/0002_auto_20240102_0232.cpython-36.pyc b/blango_auth/migrations/__pycache__/0002_auto_20240102_0232.cpython-36.pyc new file mode 100644 index 0000000000..5e56708559 Binary files /dev/null and b/blango_auth/migrations/__pycache__/0002_auto_20240102_0232.cpython-36.pyc differ diff --git a/blango_auth/migrations/__pycache__/__init__.cpython-36.pyc b/blango_auth/migrations/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000..609d6de542 Binary files /dev/null and b/blango_auth/migrations/__pycache__/__init__.cpython-36.pyc differ diff --git a/blango_auth/models.py b/blango_auth/models.py new file mode 100644 index 0000000000..d651038b8d --- /dev/null +++ b/blango_auth/models.py @@ -0,0 +1,53 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser +from django.utils.translation import gettext_lazy as _ + +# Create your models here. + + +from django.db import models +from django.contrib.auth.models import AbstractUser, UserManager + +class BlangoUserManager(UserManager): + def _create_user(self, email, password, **extra_fields): + if not email: + raise ValueError("Email must be set") + email = self.normalize_email(email) + user = self.model(email=email, **extra_fields) + user.set_password(password) + user.save(using=self._db) + return user + + def create_user(self, email, password=None, **extra_fields): + extra_fields.setdefault("is_staff", False) + extra_fields.setdefault("is_superuser", False) + return self._create_user(email, password, **extra_fields) + + def create_superuser(self, email, password, **extra_fields): + extra_fields.setdefault("is_staff", True) + extra_fields.setdefault("is_superuser", True) + + if extra_fields.get("is_staff") is not True: + raise ValueError("Superuser must have is_staff=True.") + if extra_fields.get("is_superuser") is not True: + raise ValueError("Superuser must have is_superuser=True.") + + return self._create_user(email, password, **extra_fields) + + + +class User(AbstractUser): + username = None + email = models.EmailField( + _("email address"), + unique=True, + ) + + objects = BlangoUserManager() + + USERNAME_FIELD = "email" + REQUIRED_FIELDS = [] + + def __str__(self): + return self.email + diff --git a/blango_auth/templates/blango_auth/django_registration/activateion_email_body.txt b/blango_auth/templates/blango_auth/django_registration/activateion_email_body.txt new file mode 100644 index 0000000000..ed9a366a2c --- /dev/null +++ b/blango_auth/templates/blango_auth/django_registration/activateion_email_body.txt @@ -0,0 +1,11 @@ +{{ scheme }}://{{ request.get_host }}{% url "django_registration_activate" activation_key %} +Hi, + +You registered for Blango, but you need to activate your account within {{ expiration_days }} days. + +To do that, please visit this page: + +{{ scheme }}://{{ request.get_host }}{% url "django_registration_activate" activation_key %} + +Thanks, +The Blango Team diff --git a/blango_auth/templates/blango_auth/django_registration/activateion_error.message b/blango_auth/templates/blango_auth/django_registration/activateion_error.message new file mode 100644 index 0000000000..07a3f7adbb --- /dev/null +++ b/blango_auth/templates/blango_auth/django_registration/activateion_error.message @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% load crispy_forms_tags blog_extras %} +{% block title %}Activation Failed{% endblock %} +{% block content %} +{% row "justify-content-center" %} + {% col "col-md-6" %} +

Activation Failed

+

Sorry, we couldn't activate your account.

+

{{ activation_error.message }}

+ {% endcol %} +{% endrow %} +{% endblock %} diff --git a/blango_auth/templates/blango_auth/django_registration/activation_complete.txt b/blango_auth/templates/blango_auth/django_registration/activation_complete.txt new file mode 100644 index 0000000000..0aa403fca1 --- /dev/null +++ b/blango_auth/templates/blango_auth/django_registration/activation_complete.txt @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% load crispy_forms_tags blog_extras %} +{% block title %}Activation Complete{% endblock %} +{% block content %} +{% row "justify-content-center" %} + {% col "col-md-6" %} +

Activation Complete

+

Your account is now activated! You can now log in and use Blango.

+

Log In

+ {% endcol %} +{% endrow %} +{% endblock %} diff --git a/blango_auth/templates/blango_auth/django_registration/activation_email_subject.txt b/blango_auth/templates/blango_auth/django_registration/activation_email_subject.txt new file mode 100644 index 0000000000..28cb171627 --- /dev/null +++ b/blango_auth/templates/blango_auth/django_registration/activation_email_subject.txt @@ -0,0 +1 @@ +Activate your Blango account! You have {{ expiration_days }} days! diff --git a/blango_auth/templates/blango_auth/django_registration/registration_closed.html b/blango_auth/templates/blango_auth/django_registration/registration_closed.html new file mode 100644 index 0000000000..e52f0ee429 --- /dev/null +++ b/blango_auth/templates/blango_auth/django_registration/registration_closed.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% load crispy_forms_tags blog_extras %} +{% block title %}Registration Closed{% endblock %} +{% block content %} +{% row "justify-content-center" %} + {% col "col-md-6" %} +

Registration is currently closed

+ {% endcol %} +{% endrow %} +{% endblock %} diff --git a/blango_auth/templates/blango_auth/django_registration/registration_complete.html b/blango_auth/templates/blango_auth/django_registration/registration_complete.html new file mode 100644 index 0000000000..9050bc8b19 --- /dev/null +++ b/blango_auth/templates/blango_auth/django_registration/registration_complete.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} +{% load crispy_forms_tags blog_extras %} +{% block title %}Registration Successful{% endblock %} +{% block content %} +{% row "justify-content-center" %} + {% col "col-md-6" %} +

Your registration was successful

+

Check your email to validate your account.

+ {% endcol %} +{% endrow %} +{% endblock %} diff --git a/blango_auth/templates/blango_auth/django_registration/registration_form.html b/blango_auth/templates/blango_auth/django_registration/registration_form.html new file mode 100644 index 0000000000..962ef2c759 --- /dev/null +++ b/blango_auth/templates/blango_auth/django_registration/registration_form.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} +{% load crispy_forms_tags blog_extras %} +{% block title %}Register for Blango{% endblock %} +{% block content %} +{% row "justify-content-center" %} + {% col "col-md-6" %} +

Register for Blango

+ {% crispy form %} + {% endcol %} +{% endrow %} +{% endblock %} diff --git a/blango_auth/templates/blango_auth/profile.html b/blango_auth/templates/blango_auth/profile.html new file mode 100644 index 0000000000..c5cf4aadb7 --- /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 %} diff --git a/blango_auth/templates/registration/login.html b/blango_auth/templates/registration/login.html new file mode 100644 index 0000000000..bc845a8814 --- /dev/null +++ b/blango_auth/templates/registration/login.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% load crispy_forms_tags blog_extras %} +{% block title %}Log In to Blango{% endblock %} +{% block content %} + + {% 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 %} + + + +
+ {% csrf_token %} + {{ form|crispy }} + + +
+ +

Lost password?

+

+ Log in with Google +

+ + +{% endblock %} 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..84fb12280d --- /dev/null +++ b/blango_auth/views.py @@ -0,0 +1,7 @@ +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-36.pyc b/blog/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000..f591ca21b5 Binary files /dev/null and b/blog/__pycache__/__init__.cpython-36.pyc differ diff --git a/blog/__pycache__/admin.cpython-36.pyc b/blog/__pycache__/admin.cpython-36.pyc new file mode 100644 index 0000000000..af514f04f9 Binary files /dev/null and b/blog/__pycache__/admin.cpython-36.pyc differ diff --git a/blog/__pycache__/api_urls.cpython-36.pyc b/blog/__pycache__/api_urls.cpython-36.pyc new file mode 100644 index 0000000000..ab3dad7b8e Binary files /dev/null and b/blog/__pycache__/api_urls.cpython-36.pyc differ diff --git a/blog/__pycache__/api_views.cpython-36.pyc b/blog/__pycache__/api_views.cpython-36.pyc new file mode 100644 index 0000000000..1bd581b173 Binary files /dev/null and b/blog/__pycache__/api_views.cpython-36.pyc differ diff --git a/blog/__pycache__/apps.cpython-36.pyc b/blog/__pycache__/apps.cpython-36.pyc new file mode 100644 index 0000000000..48678c5c77 Binary files /dev/null and b/blog/__pycache__/apps.cpython-36.pyc differ diff --git a/blog/__pycache__/models.cpython-36.pyc b/blog/__pycache__/models.cpython-36.pyc new file mode 100644 index 0000000000..db50908f38 Binary files /dev/null and b/blog/__pycache__/models.cpython-36.pyc differ diff --git a/blog/__pycache__/test_post_api.cpython-36.pyc b/blog/__pycache__/test_post_api.cpython-36.pyc new file mode 100644 index 0000000000..1404be17af Binary files /dev/null and b/blog/__pycache__/test_post_api.cpython-36.pyc differ diff --git a/blog/__pycache__/test_tag_api.cpython-36.pyc b/blog/__pycache__/test_tag_api.cpython-36.pyc new file mode 100644 index 0000000000..46e410192e Binary files /dev/null and b/blog/__pycache__/test_tag_api.cpython-36.pyc differ diff --git a/blog/__pycache__/tests.cpython-36.pyc b/blog/__pycache__/tests.cpython-36.pyc new file mode 100644 index 0000000000..429c4b600a Binary files /dev/null and b/blog/__pycache__/tests.cpython-36.pyc differ diff --git a/blog/__pycache__/views.cpython-36.pyc b/blog/__pycache__/views.cpython-36.pyc new file mode 100644 index 0000000000..017151da60 Binary files /dev/null and b/blog/__pycache__/views.cpython-36.pyc differ diff --git a/blog/admin.py b/blog/admin.py new file mode 100644 index 0000000000..02c9b965f8 --- /dev/null +++ b/blog/admin.py @@ -0,0 +1,16 @@ +from django.contrib import admin +from blog.models import Tag, Post, Comment, AuthorProfile + + +# Register your models here. +admin.site.register(Tag) + +class PostAdmin(admin.ModelAdmin): + prepopulated_fields = {"slug": ("title",)} + +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-36.pyc b/blog/api/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000..f0594d0680 Binary files /dev/null and b/blog/api/__pycache__/__init__.cpython-36.pyc differ diff --git a/blog/api/__pycache__/filters.cpython-36.pyc b/blog/api/__pycache__/filters.cpython-36.pyc new file mode 100644 index 0000000000..ce068c7819 Binary files /dev/null and b/blog/api/__pycache__/filters.cpython-36.pyc differ diff --git a/blog/api/__pycache__/permission.cpython-36.pyc b/blog/api/__pycache__/permission.cpython-36.pyc new file mode 100644 index 0000000000..2820775988 Binary files /dev/null and b/blog/api/__pycache__/permission.cpython-36.pyc differ diff --git a/blog/api/__pycache__/serializers.cpython-36.pyc b/blog/api/__pycache__/serializers.cpython-36.pyc new file mode 100644 index 0000000000..624f160b57 Binary files /dev/null and b/blog/api/__pycache__/serializers.cpython-36.pyc differ diff --git a/blog/api/__pycache__/throttling.cpython-36.pyc b/blog/api/__pycache__/throttling.cpython-36.pyc new file mode 100644 index 0000000000..f1bd8fe3a6 Binary files /dev/null and b/blog/api/__pycache__/throttling.cpython-36.pyc differ diff --git a/blog/api/__pycache__/urls.cpython-36.pyc b/blog/api/__pycache__/urls.cpython-36.pyc new file mode 100644 index 0000000000..1a9fd37916 Binary files /dev/null and b/blog/api/__pycache__/urls.cpython-36.pyc differ diff --git a/blog/api/__pycache__/views.cpython-36.pyc b/blog/api/__pycache__/views.cpython-36.pyc new file mode 100644 index 0000000000..a0b30597eb Binary files /dev/null and b/blog/api/__pycache__/views.cpython-36.pyc differ diff --git a/blog/api/filters.py b/blog/api/filters.py new file mode 100644 index 0000000000..592f87c848 --- /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/permission.py b/blog/api/permission.py new file mode 100644 index 0000000000..3eab033d21 --- /dev/null +++ b/blog/api/permission.py @@ -0,0 +1,9 @@ +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 diff --git a/blog/api/serializers.py b/blog/api/serializers.py new file mode 100644 index 0000000000..d35a939516 --- /dev/null +++ b/blog/api/serializers.py @@ -0,0 +1,73 @@ +from rest_framework import serializers +from blog.models import Post, Tag, Comment + +from blango_auth.models import User +from versatileimagefield.serializers import VersatileImageFieldSerializer + + +class PostSerializer(serializers.ModelSerializer): + hero_image = VersatileImageFieldSerializer( + sizes=[ + ("full_size", "url"), + ("thumbnail", "thumbnail__100x100"), + ("square_crop", "crop__200x200"), + ], + read_only=True, + ) + class Meta: + model = Post + #fields = "__all__" + readonly = ["modified_at", "created_at"] + exclude = ["ppoi"] + + + +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 PostDetailSerializer(PostSerializer): + comments = CommentSerializer(many=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 + + + + +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__" diff --git a/blog/api/throttling.py b/blog/api/throttling.py new file mode 100644 index 0000000000..f4eda89760 --- /dev/null +++ b/blog/api/throttling.py @@ -0,0 +1,17 @@ +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" diff --git a/blog/api/urls.py b/blog/api/urls.py new file mode 100644 index 0000000000..12bc557c89 --- /dev/null +++ b/blog/api/urls.py @@ -0,0 +1,75 @@ +from django.urls import path, include +from rest_framework.urlpatterns import format_suffix_patterns +from rest_framework.authtoken import views +from blog.api.views import PostList, PostDetail, UserDetail +from django.urls import path, include, re_path +from drf_yasg import openapi +from drf_yasg.views import get_schema_view +from rest_framework.routers import DefaultRouter +from blog.api.views import PostList, PostDetail, UserDetail, TagViewSet +from blog.api.views import UserDetail, TagViewSet, PostViewSet +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView + + +import os + + +router = DefaultRouter() +router.register("tags", TagViewSet) +router.register("posts", PostViewSet) + + +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("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) +urlpatterns += [ + path("auth/", include("rest_framework.urls")), + path("token-auth/", views.obtain_auth_token), + path("jwt/", TokenObtainPairView.as_view(), name="jwt_obtain_pair"), + path("jwt/refresh/", TokenRefreshView.as_view(), name="jwt_refresh"), + + 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", + ), +] + + +urlpatterns += [ + path("auth/", include("rest_framework.urls")), + # ... other patterns omitted + path("", include(router.urls)), + path( + "posts/by-time//", + PostViewSet.as_view({"get": "list"}), + name="posts-by-time", + ), + + path("posts/", PostList.as_view(), name="api_post_list"), + path("posts/", PostDetail.as_view(), name="api_post_detail"), +] + + + diff --git a/blog/api/views.py b/blog/api/views.py new file mode 100644 index 0000000000..03db868835 --- /dev/null +++ b/blog/api/views.py @@ -0,0 +1,186 @@ +from rest_framework import generics + +from blog.api.serializers import PostSerializer, UserSerializer +from blog.models import Post +from blog.api.permission import AuthorModifyOrReadOnly +from blango_auth.models import User +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAdminUser +from blog.api.serializers import PostSerializer, UserSerializer, PostDetailSerializer +from rest_framework import generics, viewsets +from django.db.models import Q +from django.utils import timezone +import django_filters.rest_framework + +from blog.api.serializers import ( + PostSerializer, + UserSerializer, + PostDetailSerializer, + TagSerializer, +) +from blog.models import Post, Tag + +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 +from datetime import timedelta +from django.http import Http404 +from blog.api.filters import PostFilterSet + + + + + +class PostList(generics.ListCreateAPIView): + queryset = Post.objects.all() + authentication_classes = [SessionAuthentication] + permission_classes = [IsAdminUser] + serializer_class = PostSerializer + +# class PostDetail(generics.RetrieveUpdateDestroyAPIView): +# queryset = Post.objects.all() +# permission_classes = [AuthorModifyOrReadOnly] +# serializer_class = PostSerializer +class PostDetail(generics.RetrieveUpdateDestroyAPIView): + permission_classes = [AuthorModifyOrReadOnly ] + queryset = Post.objects.all() + serializer_class = PostDetailSerializer + + +class UserDetail(generics.RetrieveAPIView): + queryset = User.objects.all() + authentication_classes = [SessionAuthentication] + serializer_class = UserSerializer + + +class TagViewSet(viewsets.ModelViewSet): + queryset = Tag.objects.all() + serializer_class = TagSerializer + def posts(self, request, pk=None): + tag = self.get_object() + 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) + post_serializer = PostSerializer( + tag.posts, many=True, context={"request": request} + ) + return Response(post_serializer.data) + + + + +class PostViewSet(viewsets.ModelViewSet): + permission_classes = [SessionAuthentication] + filterset_class = PostFilterSet + ordering_fields = ["published_at", "author", "title", "slug"] + # filter_backends = [django_filters.rest_framework.DjangoFilterBackend] + # filterset_fields = ["author", "tags"] + queryset = Post.objects.all() + + + 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 + + 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'" + ) + + + def get_serializer_class(self): + if self.action in ("list", "create"): + return PostSerializer + return PostDetailSerializer + +from rest_framework.decorators import action +from rest_framework.response import Response + + +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() + post_serializer = PostSerializer( + tag.posts, many=True, context={"request": request} + ) + return Response(post_serializer.data) + + +@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) + serializer = PostSerializer(posts, many=True, context={"request": request}) + return Response(serializer.data) + + + +@method_decorator(cache_page(120)) +@method_decorator(vary_on_headers("Authorization", "Cookie")) +def list(self, *args, **kwargs): + return super(PostViewSet, self).list(*args, **kwargs) + +@method_decorator(cache_page(300)) +def get(self, *args, **kwargs): + return super(UserDetail, self).get(*args, *kwargs) + + +@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) + + +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) + + 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) + + serializer = PostSerializer(posts, many=True, context={"request": request}) + return Response(serializer.data) diff --git a/blog/api_urls.py b/blog/api_urls.py new file mode 100644 index 0000000000..888fa20846 --- /dev/null +++ b/blog/api_urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from blog.api_views import post_list, post_detail +from rest_framework.urlpatterns import format_suffix_patterns + +urlpatterns = [ + path("posts/", post_list, name="api_post_list"), + path("posts//", post_detail, name="api_post_detail"), +] + +urlpatterns = format_suffix_patterns(urlpatterns) \ No newline at end of file diff --git a/blog/api_views.py b/blog/api_views.py new file mode 100644 index 0000000000..06f62f754b --- /dev/null +++ b/blog/api_views.py @@ -0,0 +1,44 @@ +from http import HTTPStatus + +from django.urls import reverse +from rest_framework.decorators import api_view +from rest_framework.response import Response + +from blog.api.serializers import PostSerializer +from blog.models import Post + + +@api_view(["GET", "POST"]) +def post_list(request, format =None): + if request.method == "GET": + posts = Post.objects.all() + return Response({"data": PostSerializer(posts, many=True).data}) + elif request.method == "POST": + serializer = PostSerializer(data=request.data) + if serializer.is_valid(): + post = serializer.save() + return Response( + status=HTTPStatus.CREATED, + headers={"Location": reverse("api_post_detail", args=(post.pk,))}, + ) + return Response(serializer.errors, status=HTTPStatus.BAD_REQUEST) + + +@api_view(["GET", "PUT", "DELETE"]) +def post_detail(request, pk, format=None): + try: + post = Post.objects.get(pk=pk) + except Post.DoesNotExist: + return Response(status=HTTPStatus.NOT_FOUND) + + if request.method == "GET": + return Response(PostSerializer(post).data) + elif request.method == "PUT": + serializer = PostSerializer(post, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(status=HTTPStatus.NO_CONTENT) + return Response(serializer.errors, status=HTTPStatus.BAD_REQUEST) + elif request.method == "DELETE": + post.delete() + return Response(status=HTTPStatus.NO_CONTENT) 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/migrations/0001_initial.py b/blog/migrations/0001_initial.py new file mode 100644 index 0000000000..3e1850049b --- /dev/null +++ b/blog/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# Generated by Django 3.2.5 on 2023-12-22 01:01 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +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..68776ce631 --- /dev/null +++ b/blog/migrations/0002_comment.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.5 on 2023-12-23 15:21 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('contenttypes', '0002_remove_content_type_name'), + ('blog', '0001_initial'), + ] + + 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()), + ('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_auto_20231223_1542.py b/blog/migrations/0003_auto_20231223_1542.py new file mode 100644 index 0000000000..7b056999cc --- /dev/null +++ b/blog/migrations/0003_auto_20231223_1542.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.5 on 2023-12-23 15:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0002_comment'), + ] + + operations = [ + migrations.RemoveField( + model_name='post', + name='author', + ), + migrations.RemoveField( + model_name='post', + name='content', + ), + migrations.RemoveField( + model_name='post', + name='created_at', + ), + migrations.RemoveField( + model_name='post', + name='modified_at', + ), + migrations.RemoveField( + model_name='post', + name='published_at', + ), + migrations.RemoveField( + model_name='post', + name='slug', + ), + migrations.RemoveField( + model_name='post', + name='summary', + ), + migrations.RemoveField( + model_name='post', + name='tags', + ), + migrations.RemoveField( + model_name='post', + name='title', + ), + ] diff --git a/blog/migrations/0004_auto_20231226_1443.py b/blog/migrations/0004_auto_20231226_1443.py new file mode 100644 index 0000000000..e009b3ec1b --- /dev/null +++ b/blog/migrations/0004_auto_20231226_1443.py @@ -0,0 +1,84 @@ +# Generated by Django 3.2.5 on 2023-12-26 14:43 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('blog', '0003_auto_20231223_1542'), + ] + + operations = [ + migrations.AddField( + model_name='comment', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='comment', + name='modified_at', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='post', + name='author', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='post', + name='content', + field=models.TextField(default='content'), + ), + migrations.AddField( + model_name='post', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='post', + name='modified_at', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='post', + name='published_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='post', + name='slug', + field=models.SlugField(default='test'), + ), + migrations.AddField( + model_name='post', + name='summary', + field=models.TextField(default='summary', max_length=500), + ), + migrations.AddField( + model_name='post', + name='tags', + field=models.ManyToManyField(related_name='posts', to='blog.Tag'), + ), + migrations.AddField( + model_name='post', + name='title', + field=models.TextField(default='title', max_length=100), + ), + migrations.AlterField( + model_name='comment', + name='content', + field=models.TextField(default='content'), + ), + migrations.AlterField( + model_name='comment', + name='object_id', + field=models.PositiveIntegerField(default=1), + ), + ] diff --git a/blog/migrations/0005_alter_post_published_at.py b/blog/migrations/0005_alter_post_published_at.py new file mode 100644 index 0000000000..11d7cf0dfa --- /dev/null +++ b/blog/migrations/0005_alter_post_published_at.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2024-01-01 13:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0004_auto_20231226_1443'), + ] + + operations = [ + migrations.AlterField( + model_name='post', + name='published_at', + field=models.DateTimeField(blank=True, db_index=True, null=True), + ), + ] diff --git a/blog/migrations/0005_alter_post_slug.py b/blog/migrations/0005_alter_post_slug.py new file mode 100644 index 0000000000..90dae7a311 --- /dev/null +++ b/blog/migrations/0005_alter_post_slug.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.6 on 2024-01-02 09:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0004_auto_20231226_1443'), + ] + + operations = [ + migrations.AlterField( + model_name='post', + name='slug', + field=models.SlugField(unique=True), + ), + ] diff --git a/blog/migrations/0006_auto_20240101_1321.py b/blog/migrations/0006_auto_20240101_1321.py new file mode 100644 index 0000000000..7376c8344f --- /dev/null +++ b/blog/migrations/0006_auto_20240101_1321.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.23 on 2024-01-01 13:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0004_auto_20231226_1443'), + # ('blog', '0005_alter_post_published_at'), + ] + + operations = [ + 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, default=1), + ), + ] diff --git a/blog/migrations/0007_authorprofile.py b/blog/migrations/0007_authorprofile.py new file mode 100644 index 0000000000..61699c74d1 --- /dev/null +++ b/blog/migrations/0007_authorprofile.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.6 on 2024-01-02 02:18 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), +# ('blog', '0006_auto_20240101_1321'), + ('blog', '0004_auto_20231226_1443'), + ] + + operations = [ + 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/0008_merge_20240102_0948.py b/blog/migrations/0008_merge_20240102_0948.py new file mode 100644 index 0000000000..f094093e47 --- /dev/null +++ b/blog/migrations/0008_merge_20240102_0948.py @@ -0,0 +1,16 @@ +# Generated by Django 3.2.6 on 2024-01-02 09:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0005_alter_post_published_at'), + ('blog', '0005_alter_post_slug'), + ('blog', '0006_auto_20240101_1321'), + ('blog', '0007_authorprofile'), + ] + + operations = [ + ] diff --git a/blog/migrations/0009_alter_post_slug.py b/blog/migrations/0009_alter_post_slug.py new file mode 100644 index 0000000000..050c2b8bed --- /dev/null +++ b/blog/migrations/0009_alter_post_slug.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.6 on 2024-01-05 01:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0008_merge_20240102_0948'), + ] + + operations = [ + migrations.AlterField( + model_name='post', + name='slug', + field=models.SlugField(default='test', unique=True), + ), + ] diff --git a/blog/migrations/0010_alter_tag_value.py b/blog/migrations/0010_alter_tag_value.py new file mode 100644 index 0000000000..975552137e --- /dev/null +++ b/blog/migrations/0010_alter_tag_value.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.6 on 2024-01-06 09:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0009_alter_post_slug'), + ] + + operations = [ + migrations.AlterField( + model_name='tag', + name='value', + field=models.TextField(max_length=100, unique=True), + ), + ] diff --git a/blog/migrations/0011_auto_20240115_1411.py b/blog/migrations/0011_auto_20240115_1411.py new file mode 100644 index 0000000000..961283f599 --- /dev/null +++ b/blog/migrations/0011_auto_20240115_1411.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.6 on 2024-01-15 14:11 + +from django.db import migrations +import versatileimagefield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0010_alter_tag_value'), + ] + + operations = [ + migrations.AlterModelOptions( + name='tag', + options={'ordering': ['value']}, + ), + 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-36.pyc b/blog/migrations/__pycache__/0001_initial.cpython-36.pyc new file mode 100644 index 0000000000..61acf3be88 Binary files /dev/null and b/blog/migrations/__pycache__/0001_initial.cpython-36.pyc differ diff --git a/blog/migrations/__pycache__/0002_comment.cpython-36.pyc b/blog/migrations/__pycache__/0002_comment.cpython-36.pyc new file mode 100644 index 0000000000..05e6c9d8e6 Binary files /dev/null and b/blog/migrations/__pycache__/0002_comment.cpython-36.pyc differ diff --git a/blog/migrations/__pycache__/0003_auto_20231223_1542.cpython-36.pyc b/blog/migrations/__pycache__/0003_auto_20231223_1542.cpython-36.pyc new file mode 100644 index 0000000000..1869f1604c Binary files /dev/null and b/blog/migrations/__pycache__/0003_auto_20231223_1542.cpython-36.pyc differ diff --git a/blog/migrations/__pycache__/0004_auto_20231223_1549.cpython-36.pyc b/blog/migrations/__pycache__/0004_auto_20231223_1549.cpython-36.pyc new file mode 100644 index 0000000000..94bab465c5 Binary files /dev/null and b/blog/migrations/__pycache__/0004_auto_20231223_1549.cpython-36.pyc differ diff --git a/blog/migrations/__pycache__/0004_auto_20231226_1443.cpython-36.pyc b/blog/migrations/__pycache__/0004_auto_20231226_1443.cpython-36.pyc new file mode 100644 index 0000000000..503c8dca7e Binary files /dev/null and b/blog/migrations/__pycache__/0004_auto_20231226_1443.cpython-36.pyc differ diff --git a/blog/migrations/__pycache__/0005_alter_post_published_at.cpython-36.pyc b/blog/migrations/__pycache__/0005_alter_post_published_at.cpython-36.pyc new file mode 100644 index 0000000000..871e764e53 Binary files /dev/null and b/blog/migrations/__pycache__/0005_alter_post_published_at.cpython-36.pyc differ diff --git a/blog/migrations/__pycache__/0005_alter_post_slug.cpython-36.pyc b/blog/migrations/__pycache__/0005_alter_post_slug.cpython-36.pyc new file mode 100644 index 0000000000..af1223f86f Binary files /dev/null and b/blog/migrations/__pycache__/0005_alter_post_slug.cpython-36.pyc differ diff --git a/blog/migrations/__pycache__/0006_auto_20240101_1321.cpython-36.pyc b/blog/migrations/__pycache__/0006_auto_20240101_1321.cpython-36.pyc new file mode 100644 index 0000000000..88aae2a4e6 Binary files /dev/null and b/blog/migrations/__pycache__/0006_auto_20240101_1321.cpython-36.pyc differ diff --git a/blog/migrations/__pycache__/0007_authorprofile.cpython-36.pyc b/blog/migrations/__pycache__/0007_authorprofile.cpython-36.pyc new file mode 100644 index 0000000000..d60d890721 Binary files /dev/null and b/blog/migrations/__pycache__/0007_authorprofile.cpython-36.pyc differ diff --git a/blog/migrations/__pycache__/0008_merge_20240102_0948.cpython-36.pyc b/blog/migrations/__pycache__/0008_merge_20240102_0948.cpython-36.pyc new file mode 100644 index 0000000000..29bf485665 Binary files /dev/null and b/blog/migrations/__pycache__/0008_merge_20240102_0948.cpython-36.pyc differ diff --git a/blog/migrations/__pycache__/0009_alter_post_slug.cpython-36.pyc b/blog/migrations/__pycache__/0009_alter_post_slug.cpython-36.pyc new file mode 100644 index 0000000000..ef40dc905b Binary files /dev/null and b/blog/migrations/__pycache__/0009_alter_post_slug.cpython-36.pyc differ diff --git a/blog/migrations/__pycache__/0010_alter_tag_value.cpython-36.pyc b/blog/migrations/__pycache__/0010_alter_tag_value.cpython-36.pyc new file mode 100644 index 0000000000..dee540ec3b Binary files /dev/null and b/blog/migrations/__pycache__/0010_alter_tag_value.cpython-36.pyc differ diff --git a/blog/migrations/__pycache__/0011_auto_20240115_1411.cpython-36.pyc b/blog/migrations/__pycache__/0011_auto_20240115_1411.cpython-36.pyc new file mode 100644 index 0000000000..c03d698b58 Binary files /dev/null and b/blog/migrations/__pycache__/0011_auto_20240115_1411.cpython-36.pyc differ diff --git a/blog/migrations/__pycache__/__init__.cpython-36.pyc b/blog/migrations/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000..530f9cb814 Binary files /dev/null and b/blog/migrations/__pycache__/__init__.cpython-36.pyc differ diff --git a/blog/models.py b/blog/models.py new file mode 100644 index 0000000000..8cab32ab01 --- /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 +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes.fields import GenericForeignKey +from versatileimagefield.fields import VersatileImageField, PPOIField + +# Create your models here. +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(default="content") + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField(default=1, 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, default =1) + created_at = models.DateTimeField(auto_now_add=True) + hero_image = VersatileImageField( + upload_to="hero_images", ppoi_field="ppoi", null=True, blank=True + ) + ppoi = PPOIField(null=True, blank=True) + modified_at = models.DateTimeField(auto_now=True) + published_at = models.DateTimeField(blank=True, null=True) + title = models.TextField(max_length=100, default="title") + slug = models.SlugField(default = "test", unique=True) + summary = models.TextField(max_length=500, default="summary") + content = models.TextField(default="content") + tags = models.ManyToManyField(Tag, related_name="posts") + comments = GenericRelation(Comment) + published_at = models.DateTimeField(blank=True, null=True, db_index=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.js b/blog/static/blog/blog.js new file mode 100644 index 0000000000..c21366bf49 --- /dev/null +++ b/blog/static/blog/blog.js @@ -0,0 +1,376 @@ +// react based + +['/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) + }) +}) +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: [] + } + }) + }) + } + + +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: true, + data: { + results: [ + { + id: 15, + tags: [ + 'django', 'react' + ], + 'hero_image': { + 'thumbnail': '/media/__sized__/hero_images/data_insights-thumbnail-100x100.png', + 'full_size': '/media/hero_images/data_insights.png' + }, + 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 +) + +// ReactDOM.render( +// React.createElement(PostTable), +// domContainer +// ) + + + +// class ClickButton extends React.Component { +// state = { +// wasClicked: false +// } + +// handleClick () { +// this.setState( +// {wasClicked: true} +// ) +// } + +// 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 +// ) +// } +// } + +// const domContainer = document.getElementById('react_root') +// ReactDOM.render( +// React.createElement(ClickButton), +// domContainer +// ) + + +// using jsx + + // render () { + // let buttonText + + // if (this.state.wasClicked) + // buttonText = 'Clicked!' + // else + // buttonText = 'Click Me' + + // return + // } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +// alert('Hello, world!') +// const theNumber = 1 +// //let yourName = 'Thiru' + +// if (theNumber === 2) { +// let yourName = 'valluvan' +// 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 = 'Thiru' // 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 related + +// 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('Thiru') // 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('Thiru 2 Seconds') +// dg2.greet() + +// const dg1 = new DelayedGreeter('Thiru 1 Second', 1000) +// dg1.greet() + + +// 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) + + diff --git a/blog/templates/base.html b/blog/templates/base.html new file mode 100644 index 0000000000..f37ee2e448 --- /dev/null +++ b/blog/templates/base.html @@ -0,0 +1,34 @@ + + + + + + + {% block title %}Welcome to Blango{% endblock %} + + + +
+ + + {% block content %} + + {% endblock %} +
+ + + + diff --git a/blog/templates/blog/index.html b/blog/templates/blog/index.html new file mode 100644 index 0000000000..7da2955fd4 --- /dev/null +++ b/blog/templates/blog/index.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + + + +{% block content %} +{% load blog_extras %} +

Blog Posts

+ {% for post in posts %} +
+
+

{{ post.title }}

+ By {{ post.author|author_details:request.user }} on {{ post.published_at|date:"M, d Y" }} +

{{ post.summary }}

+

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

+ {% if post.hero_image %} + + {% endif %} + +
+
+ {% endfor %} + +{% endblock %} \ No newline at end of file diff --git a/blog/templates/blog/post-table.html b/blog/templates/blog/post-table.html new file mode 100644 index 0000000000..49e1a3c74d --- /dev/null +++ b/blog/templates/blog/post-table.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% load static %} +{% block content %} +{% block title %}Post Table{% endblock %} + +
+
+
+
+
+ + + + + + + +{% endblock %} 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-36.pyc b/blog/templatetags/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000..ef4778ea80 Binary files /dev/null and b/blog/templatetags/__pycache__/__init__.cpython-36.pyc differ diff --git a/blog/templatetags/__pycache__/blog_extras.cpython-36.pyc b/blog/templatetags/__pycache__/blog_extras.cpython-36.pyc new file mode 100644 index 0000000000..4bee88e944 Binary files /dev/null and b/blog/templatetags/__pycache__/blog_extras.cpython-36.pyc differ diff --git a/blog/templatetags/blog_extras.py b/blog/templatetags/blog_extras.py new file mode 100644 index 0000000000..068c458bbe --- /dev/null +++ b/blog/templatetags/blog_extras.py @@ -0,0 +1,36 @@ +from django import template +register = template.Library() +from django.utils.html import format_html + +from django.contrib.auth import get_user_model +user_model = get_user_model() +import logging + +logger = logging.getLogger(__name__) + +@register.filter +def author_details(author, current_user): + # logger.debug("Loaded %d recent posts for post %d", len(author), author.id) + + if not isinstance(author, user_model): + # return empty string as safe default + return "" + + 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) + + diff --git a/blog/test_post_api.py b/blog/test_post_api.py new file mode 100644 index 0000000000..79124fa215 --- /dev/null +++ b/blog/test_post_api.py @@ -0,0 +1,105 @@ +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/") + data = resp.json()["results"] + self.assertEqual(len(data), 1) + + for post_dict in data: + print(post_dict) + 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(), 1) + + 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..f4f6f3f0f3 --- /dev/null +++ b/blog/test_tag_api.py @@ -0,0 +1,46 @@ +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) + 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/tests.py b/blog/tests.py new file mode 100644 index 0000000000..7ce503c2dd --- /dev/null +++ b/blog/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/blog/urls.py b/blog/urls.py new file mode 100644 index 0000000000..4a842e5787 --- /dev/null +++ b/blog/urls.py @@ -0,0 +1,7 @@ +# # other imports +# import blog.views + +# urlpatterns = [ +# # other patterns +# path("", blog.views.index) +# ] diff --git a/blog/views.py b/blog/views.py new file mode 100644 index 0000000000..a3cf2163a6 --- /dev/null +++ b/blog/views.py @@ -0,0 +1,55 @@ +from django.shortcuts import render +# from .models import Post +from django.utils import timezone +from blog.models import Post + +import logging +logger = logging.getLogger(__name__) +from blog.models import Post +from django.views.decorators.cache import cache_page +from django.views.decorators.vary import vary_on_headers +from django.utils import timezone +from django.urls import reverse + +# Create your views here. + +# def index(request): +# return render(request, "blog/index.html") +# @cache_page(300) +# @vary_on_headers("Cookie") +# def index(request): +# from django.http import HttpResponse +# return HttpResponse(str(request.user).encode("ascii")) +# posts = Post.objects.filter(published_at__lte=timezone.now()) +# logger.debug("Got %d posts", len(posts)) +# return render(request, "blog/index.html", {"posts": posts}) + + +def index(request): + posts = Post.objects.filter(published_at__lte=timezone.now()).select_related("author") + + # posts = ( + # Post.objects.filter(published_at__lte=timezone.now()) + # .select_related("author") + # .defer("created_at", "modified_at") +# ) + + + + + # logger.debug("Got %d posts", (posts)) + return render(request, "blog/index.html", {"posts": posts}) + + +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") + + +def post_table(request): + return render( + request, "blog/post-table.html", {"post_list_url": reverse("post-list")} + ) diff --git a/data.json b/data.json new file mode 100644 index 0000000000..6fcbb977e4 --- /dev/null +++ b/data.json @@ -0,0 +1 @@ +[{"model": "blog.comment", "pk": 1, "fields": {"creator": 1, "content": "What a great post!", "content_type": 8, "object_id": 1, "created_at": "2023-12-26T14:43:20.297Z", "modified_at": "2023-12-26T14:43:20.312Z"}}, {"model": "blog.comment", "pk": 2, "fields": {"creator": 1, "content": "I like myself!", "content_type": 4, "object_id": 1, "created_at": "2023-12-26T14:43:20.297Z", "modified_at": "2023-12-26T14:43:20.312Z"}}, {"model": "blog.comment", "pk": 3, "fields": {"creator": 1, "content": "Thank you for reading my post!", "content_type": 8, "object_id": 1, "created_at": "2024-01-01T13:47:03.001Z", "modified_at": "2024-01-01T13:47:03.001Z"}}, {"model": "blog.comment", "pk": 4, "fields": {"creator": 2, "content": "Thank you for reading my post!", "content_type": 8, "object_id": 2, "created_at": "2024-01-01T13:47:03.001Z", "modified_at": "2024-01-01T13:47:03.001Z"}}, {"model": "blog.tag", "pk": 1, "fields": {"value": "django"}}, {"model": "blog.tag", "pk": 2, "fields": {"value": "example"}}, {"model": "blog.post", "pk": 1, "fields": {"author": 1, "created_at": "2023-12-26T14:47:03.829Z", "modified_at": "2023-12-26T14:47:03.829Z", "title": "title", "slug": "test", "summary": "summary", "content": "content", "published_at": null, "tags": [1, 2]}}, {"model": "blog.post", "pk": 2, "fields": {"author": 2, "created_at": "2023-12-26T14:47:52.578Z", "modified_at": "2023-12-26T14:47:52.578Z", "title": "Thiru", "slug": "Thiru", "summary": "If you don’t already have some test Blog data added, you should log to the Django admin and add a couple of posts, and another user as an extra author. This will come in handy later.", "content": "Provided you’ve done this and have some test data, when you start the Django dev server and navigate to the main page you should see something like this:", "published_at": null, "tags": [1, 2]}}, {"model": "blog.post", "pk": 3, "fields": {"author": 2, "created_at": "2024-01-01T13:48:31.575Z", "modified_at": "2024-01-01T13:48:31.575Z", "title": "title", "slug": "test", "summary": "summary", "content": "content", "published_at": "2024-01-01T13:48:20Z", "tags": [1, 2]}}, {"model": "auth.user", "pk": 1, "fields": {"password": "pbkdf2_sha256$260000$Vb3Nzqn4v5Fh1KC2VbVR6J$J/F+J8DRBt0BgTmbdH8Ah0sVxtny3E0uXRhPHgPjcy8=", "last_login": null, "is_superuser": true, "username": "codio", "first_name": "", "last_name": "", "email": "", "is_staff": true, "is_active": true, "date_joined": "2023-12-22T00:57:16.353Z", "groups": [], "user_permissions": []}}, {"model": "auth.user", "pk": 2, "fields": {"password": "argon2$argon2id$v=19$m=102400,t=2,p=8$OUhTUkNXc0tVZ3Y2bFhlWGJmUm1xMg$UUWqV9jKQG5J0/a41CEIAg", "last_login": "2024-01-01T13:47:51.013Z", "is_superuser": true, "username": "thiru", "first_name": "Thiruvalluvan", "last_name": "Murugesan", "email": "m.thiruvalluvar@gmail.com", "is_staff": true, "is_active": true, "date_joined": "2023-12-22T00:57:50Z", "groups": [], "user_permissions": []}}] \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000000..a3aa0361fd Binary files /dev/null and b/db.sqlite3 differ diff --git a/manage.py b/manage.py index c66b327f71..8fbae3230d 100644 --- a/manage.py +++ b/manage.py @@ -7,6 +7,8 @@ def main(): """Run administrative tasks.""" os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blango.settings') + os.environ.setdefault("DJANGO_CONFIGURATION", "Dev") + try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/media/__sized__/hero_images/data_insights-thumbnail-100x100.png b/media/__sized__/hero_images/data_insights-thumbnail-100x100.png new file mode 100644 index 0000000000..e39bb9f151 Binary files /dev/null and b/media/__sized__/hero_images/data_insights-thumbnail-100x100.png differ diff --git a/media/__sized__/hero_images/data_insights-thumbnail-300x300.png b/media/__sized__/hero_images/data_insights-thumbnail-300x300.png new file mode 100644 index 0000000000..cd25311326 Binary files /dev/null and b/media/__sized__/hero_images/data_insights-thumbnail-300x300.png differ diff --git a/media/hero_images/data_insights.png b/media/hero_images/data_insights.png new file mode 100644 index 0000000000..8056834d73 Binary files /dev/null and b/media/hero_images/data_insights.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..93cec39d90 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,55 @@ +argon2-cffi==20.1.0 +asgiref==3.4.1 +asn1crypto==0.24.0 +certifi==2018.1.18 +cffi==1.14.6 +chardet==3.0.4 +confusable-homoglyphs==3.2.0 +crispy-bootstrap5==0.4 +cryptography==3.4.8 +decorator==4.1.2 +defusedxml==0.7.1 +dj-database-url==0.5.0 +Django==3.2.6 +django-allauth==0.45.0 +django-configurations==2.2 +django-crispy-forms==1.12.0 +django-debug-toolbar==3.2.2 +django-registration==3.2 +djangorestframework==3.12.4 +drf-yasg==1.21.7 +idna==2.6 +inflection==0.5.1 +ipython==5.5.0 +ipython_genutils==0.2.0 +keyring==10.6.0 +keyrings.alt==3.0 +netifaces==0.10.4 +oauthlib==3.1.1 +packaging==21.3 +pexpect==4.2.1 +pickleshare==0.7.4 +prompt-toolkit==1.0.15 +pycparser==2.20 +pycrypto==2.6.1 +Pygments==2.2.0 +PyGObject==3.26.1 +PyJWT==2.1.0 +pyparsing==3.1.1 +python-apt==1.6.5+ubuntu0.3 +python3-openid==3.2.0 +pytz==2021.1 +pyxdg==0.25 +PyYAML==6.0.1 +requests==2.18.4 +requests-oauthlib==1.3.0 +SecretStorage==2.3.1 +simplegeneric==0.8.1 +six==1.16.0 +sqlparse==0.4.1 +ssh-import-id==5.7 +traitlets==4.3.2 +typing-extensions==3.10.0.1 +uritemplate==4.1.1 +urllib3==1.22 +wcwidth==0.1.7