aboutsummaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorBobby <[email protected]>2024-12-15 14:28:53 -0500
committerBobby <[email protected]>2024-12-15 14:28:53 -0500
commit9f9025fe2f70ac500c01473a9659e2ecd1d11774 (patch)
treed4e2092afdd26b892cd2ff0445bdfa0e9a4c6713 /apps
parent05b5afe86134d4807acb8b06485a44e390822127 (diff)
downloadthatcomputerscientist-9f9025fe2f70ac500c01473a9659e2ecd1d11774.tar.xz
thatcomputerscientist-9f9025fe2f70ac500c01473a9659e2ecd1d11774.zip
migrated the blog
Diffstat (limited to 'apps')
-rw-r--r--apps/blog/__init__.py0
-rw-r--r--apps/blog/admin.py10
-rw-r--r--apps/blog/apps.py11
-rw-r--r--apps/blog/context_processors.py269
-rw-r--r--apps/blog/feed.py46
-rw-r--r--apps/blog/migrations/0001_initial.py63
-rw-r--r--apps/blog/migrations/0002_alter_post_date.py18
-rw-r--r--apps/blog/migrations/0003_post_post_image.py18
-rw-r--r--apps/blog/migrations/0004_alter_post_post_image.py18
-rw-r--r--apps/blog/migrations/0005_alter_post_post_image.py18
-rw-r--r--apps/blog/migrations/0006_remove_post_post_image.py17
-rw-r--r--apps/blog/migrations/0007_alter_post_body.py18
-rw-r--r--apps/blog/migrations/0008_alter_post_date.py18
-rw-r--r--apps/blog/migrations/0009_post_post_image.py18
-rw-r--r--apps/blog/migrations/0010_alter_post_date.py18
-rw-r--r--apps/blog/migrations/0011_alter_post_date.py18
-rw-r--r--apps/blog/migrations/0012_alter_post_date.py18
-rw-r--r--apps/blog/migrations/0013_post_views.py17
-rw-r--r--apps/blog/migrations/0014_anonymouscommentuser_alter_comment_user_and_more.py53
-rw-r--r--apps/blog/migrations/__init__.py0
-rw-r--r--apps/blog/models.py102
-rw-r--r--apps/blog/recommender.py41
-rw-r--r--apps/blog/tests.py3
-rw-r--r--apps/blog/urls.py50
-rw-r--r--apps/blog/views.py1034
-rw-r--r--apps/core/urls.py2
26 files changed, 1897 insertions, 1 deletions
diff --git a/apps/blog/__init__.py b/apps/blog/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/apps/blog/__init__.py
diff --git a/apps/blog/admin.py b/apps/blog/admin.py
new file mode 100644
index 00000000..dd35e8cb
--- /dev/null
+++ b/apps/blog/admin.py
@@ -0,0 +1,10 @@
+from django.contrib import admin
+
+# Register your models here.
+from .models import AnonymousCommentUser, Category, Comment, Post, Tag
+
+admin.site.register(Post)
+admin.site.register(Comment)
+admin.site.register(Category)
+admin.site.register(Tag)
+admin.site.register(AnonymousCommentUser)
diff --git a/apps/blog/apps.py b/apps/blog/apps.py
new file mode 100644
index 00000000..345259a5
--- /dev/null
+++ b/apps/blog/apps.py
@@ -0,0 +1,11 @@
+from django.apps import AppConfig
+
+
+class BlogConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "apps.blog"
+
+ # def ready(self):
+ # from jobs import updater
+
+ # updater.start()
diff --git a/apps/blog/context_processors.py b/apps/blog/context_processors.py
new file mode 100644
index 00000000..fe2b79aa
--- /dev/null
+++ b/apps/blog/context_processors.py
@@ -0,0 +1,269 @@
+import os
+import re
+
+import akismet
+import dotenv
+import requests
+from bs4 import BeautifulSoup
+from django.conf import settings
+from django.core.cache import cache
+from pygments import highlight
+from pygments.formatters import HtmlFormatter
+from pygments.lexers import get_lexer_by_name, guess_lexer
+import google.generativeai as genai
+
+from .models import Category, Comment, Post
+
+dotenv.load_dotenv()
+
+gemini_api_key = os.getenv("GEMINI_API_KEY")
+
+akismet_api = akismet.Akismet(
+ key=os.getenv("AKISMET_API_KEY"),
+ blog_url=(
+ "https://preview.thatcomputerscientist.com"
+ if settings.DEBUG
+ else "https://thatcomputerscientist.com"
+ ),
+)
+
+
+def check_spam(comment, post):
+ # spam = False
+ # akismet_data = {
+ # "comment_type": "comment",
+ # "comment_author": author,
+ # "comment_content": comment,
+ # "is_test": settings.DEBUG,
+ # }
+ # spam = akismet_api.comment_check(user_ip, user_agent, **akismet_data)
+
+ # if spam:
+ # return spam
+
+ # Now we check with Google Generative AI
+ if gemini_api_key is None:
+ return True
+ else:
+ genai.configure(api_key=gemini_api_key)
+
+ model = genai.GenerativeModel("gemini-pro")
+ print(comment)
+
+ input_prompt = f"Comment Processing Checker. This is for a personal blog site. Output only Y or N for the included text. Y if the comment seems like spam, or random gibberish or a bunch of letters which make no sense or looks like a coupon code or something. Only block spam, nothing else. If a comment contains cuss words, personal attacks, profanity or any possible harrasment, it is NOT spam (unless it contains spammy gibberish or links). This is a strict spam only filter. N if the comment is not spam. Do not access links. Just mark Y or N for the text. Post Title: {post.title}. \n Comment: {comment}. \n Judge if the comment belongs on the post or not. Random texts, links, and gibberish are considered spam. Trying to phish or promote shady links are also considered spam. Output single character - either Y or N only."
+
+ bn = [
+ {
+ "category": "HARM_CATEGORY_DANGEROUS",
+ "threshold": "BLOCK_NONE",
+ },
+ {
+ "category": "HARM_CATEGORY_HARASSMENT",
+ "threshold": "BLOCK_NONE",
+ },
+ {
+ "category": "HARM_CATEGORY_HATE_SPEECH",
+ "threshold": "BLOCK_NONE",
+ },
+ {
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
+ "threshold": "BLOCK_NONE",
+ },
+ {
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
+ "threshold": "BLOCK_NONE",
+ },
+ ]
+
+ response = model.generate_content(input_prompt, safety_settings=bn)
+
+ r_text = response.text
+
+ r_text = r_text.strip()
+
+ print(r_text)
+
+ return r_text
+
+
+def add_excerpt(post):
+ soup = BeautifulSoup(post.body, "html.parser")
+
+ # Create excerpt, count min 1000 characters and max upto next paragraph
+ excerpt = ""
+ for paragraph in soup.find_all("p"):
+ paragraph = "<p>" + str(paragraph.text) + "</p>"
+ excerpt += str(paragraph)
+
+ if len(excerpt) >= 1000:
+ break
+ return excerpt
+
+
+def add_num_comments(post):
+ num_comments = Comment.objects.filter(post=post).count()
+ return num_comments
+
+
+def recent_posts():
+ recent_posts = Post.objects.filter(is_public=True).order_by("-date")[:5]
+ for post in recent_posts:
+ post.excerpt = add_excerpt(post)
+ post.num_comments = add_num_comments(post)
+ return recent_posts
+
+
+def categories(request):
+ categories = Category.objects.all()[0:5]
+ return {"categories": categories}
+
+
+def archives(request):
+ archives = Post.objects.filter(is_public=True).dates("date", "month", order="DESC")[
+ 0:5
+ ]
+ return {"archives": archives}
+
+
+def avatar_list():
+ avatar_list = {}
+ directory = os.path.join(settings.BASE_DIR, "static", "images", "avatars")
+ for directory in os.listdir(directory):
+ # ignore hidden files
+ if directory.startswith("."):
+ continue
+ avatar_list[directory] = os.listdir(
+ os.path.join(settings.BASE_DIR, "static", "images", "avatars", directory)
+ )
+ # remove hidden files
+ for file in avatar_list[directory]:
+ if file.startswith("."):
+ avatar_list[directory].remove(file)
+ return avatar_list
+
+
+def highlight_code_blocks(code_block, language=None):
+ # replace &nbsp; with space
+ try:
+ cb = code_block.string
+ except:
+ cb = code_block
+ cb = cb.replace("\xa0", " ")
+
+ # guess the language as there is no data-lang attribute
+ if language:
+ try:
+ lexer = get_lexer_by_name(language.strip())
+ except:
+ lexer = get_lexer_by_name("text")
+ else:
+ try:
+ lexer = guess_lexer(cb)
+ except:
+ lexer = get_lexer_by_name("text")
+ # highlight the code
+ formatter = HtmlFormatter(noclasses=True, style="native", wrapcode=True)
+ highlighted_code = highlight(cb, lexer, formatter)
+
+ return highlighted_code
+
+
+def check_link_safety(link):
+ api_key = os.getenv("GOOGLE_SAFE_BROWSING_API_KEY")
+ api_url = "https://safebrowsing.googleapis.com/v4/threatMatches:find"
+ cache_key = f"link_safety:{link}"
+ cache_timeout = 60 * 60 * 24 * 7 # 7 days
+
+ # Check if the result is already cached
+ cached_result = cache.get(cache_key)
+ if cached_result is not None:
+ return cached_result
+
+ payload = {
+ "threatInfo": {
+ "threatTypes": [
+ "MALWARE",
+ "SOCIAL_ENGINEERING",
+ "UNWANTED_SOFTWARE",
+ "POTENTIALLY_HARMFUL_APPLICATION",
+ ],
+ "platformTypes": ["ANY_PLATFORM"],
+ "threatEntryTypes": ["URL"],
+ "threatEntries": [{"url": link}],
+ }
+ }
+
+ headers = {"Content-Type": "application/json"}
+
+ params = {"key": api_key, "alt": "json"}
+
+ response = requests.post(api_url, params=params, headers=headers, json=payload)
+ if response.status_code == 200:
+ # Successful API call
+ matches = response.json().get("matches", [])
+ # Cache the result
+ cache.set(cache_key, len(matches) == 0, cache_timeout)
+ return len(matches) == 0
+ else:
+ # Handle API error
+ print(f"Safe Browsing API error: {response.content}")
+
+ return False
+
+
+def comment_processor(comment):
+ # escape html tags
+ comment = re.sub(r"<", "&lt;", comment)
+ comment = re.sub(r">", "&gt;", comment)
+
+ # any text between ``` and ``` must be highlighted as code
+ code_blocks = re.findall(r"```(.+?)```", comment, re.DOTALL)
+ for code_block in code_blocks:
+ if code_block.startswith("lang-"):
+ language = code_block.split("\n")[0].replace("lang-", "")
+ code_block = code_block.replace("lang-" + language + "\n", "")
+ # comment = highlight_code_blocks(code_block.replace('&lt;', '<').replace('&gt;', '>'), language)
+ comment = comment.replace(
+ "```lang-" + language + "\n" + code_block + "```",
+ highlight_code_blocks(
+ code_block.replace("&lt;", "<").replace("&gt;", ">"), language
+ ),
+ )
+ else:
+ comment = comment.replace(
+ "```" + code_block + "```",
+ highlight_code_blocks(
+ code_block.replace("&lt;", "<").replace("&gt;", ">")
+ ),
+ )
+
+ # any http or https links must be converted to anchor tags
+ links = re.findall(r"(https?://[^\s]+)", comment)
+ for link in links:
+ # check if the link is safe
+ if check_link_safety(link):
+ comment = comment.replace(
+ link, '<a href="' + link + '" target="_blank">' + link + "</a>"
+ )
+ else:
+ # do not replace the link if it is not safe. Add a warning message after the link instead
+ comment = comment.replace(
+ link,
+ link
+ + '<span style="color: red"> (Seems unsafe! Proceed with caution)</span>',
+ )
+
+ # retain line breaks, for every newline character, add a <br> tag
+ comment = comment.replace("\n", "<br>")
+
+ # replace multiple <br> tags with a single <br> tag
+ comment = re.sub(r"<br>(\s*<br>)+", "<br><br>", comment)
+
+ comment = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", comment)
+ comment = re.sub(r"__(.+?)__", r"<i>\1</i>", comment)
+ comment = re.sub(r"~~(.+?)~~", r"<s>\1</s>", comment)
+
+ # remove any br tags at the end of the comment
+ comment = re.sub(r"<br>$", "", comment)
+
+ return comment
diff --git a/apps/blog/feed.py b/apps/blog/feed.py
new file mode 100644
index 00000000..0951267c
--- /dev/null
+++ b/apps/blog/feed.py
@@ -0,0 +1,46 @@
+import re
+
+import requests
+from django.conf import settings
+from django.contrib.syndication.views import Feed
+from django.utils import feedgenerator
+from django.utils.feedgenerator import Enclosure
+
+from .models import Post
+
+request_domain = settings.DEBUG and 'https://preview.thatcomputerscientist.com' or 'https://shi.foo'
+
+class RSSFeed(Feed):
+ title = 'Shifoo'
+ link = '/weblog'
+ description = 'RSS Feed for Shifoo\'s Weblog'
+ feed_type = feedgenerator.Rss201rev2Feed
+
+ def items(self):
+ return Post.objects.all().filter(is_public=True).order_by('-date')[:10]
+
+ def item_title(self, item):
+ return item.title
+
+ def item_description(self, item):
+ body = re.sub(r"[\x00-\x08\x0B-\x1F\x7F-\x9F]", "", str(item.body))
+ return body
+
+ def item_link(self, item):
+ return f'{request_domain}/weblog/{item.slug}'
+
+ def item_pubdate(self, item):
+ return item.date
+
+ def get_cl(self, url):
+ r = requests.head(url)
+ return str(r.headers['Content-Length'])
+
+ def item_enclosures(self, item):
+ return [
+ Enclosure(
+ url=f'{request_domain}/ignis/post_image/1200/{item.id}.gif',
+ length=self.get_cl(f'{request_domain}/ignis/post_image/1200/{item.id}.gif'),
+ mime_type='image/gif',
+ )
+ ]
diff --git a/apps/blog/migrations/0001_initial.py b/apps/blog/migrations/0001_initial.py
new file mode 100644
index 00000000..d6ef46c8
--- /dev/null
+++ b/apps/blog/migrations/0001_initial.py
@@ -0,0 +1,63 @@
+# Generated by Django 4.0.6 on 2022-09-19 00:31
+
+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='Category',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=50)),
+ ('slug', models.SlugField(unique=True)),
+ ('description', models.TextField(blank=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Tag',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=50)),
+ ('slug', models.SlugField(unique=True)),
+ ('description', models.TextField(blank=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Post',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=100)),
+ ('slug', models.SlugField(max_length=100, unique=True)),
+ ('body', models.TextField()),
+ ('date', models.DateTimeField(auto_now_add=True)),
+ ('is_public', models.BooleanField(default=False)),
+ ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category')),
+ ('tags', models.ManyToManyField(blank=True, to='blog.tag')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Comment',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('body', models.TextField()),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('edited', models.BooleanField(default=False)),
+ ('edited_at', models.DateTimeField(blank=True, null=True)),
+ ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.post')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ ]
diff --git a/apps/blog/migrations/0002_alter_post_date.py b/apps/blog/migrations/0002_alter_post_date.py
new file mode 100644
index 00000000..4a0a6697
--- /dev/null
+++ b/apps/blog/migrations/0002_alter_post_date.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.0.6 on 2022-11-12 20:32
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('blog', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='post',
+ name='date',
+ field=models.DateTimeField(),
+ ),
+ ]
diff --git a/apps/blog/migrations/0003_post_post_image.py b/apps/blog/migrations/0003_post_post_image.py
new file mode 100644
index 00000000..01d9751b
--- /dev/null
+++ b/apps/blog/migrations/0003_post_post_image.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.0.6 on 2022-11-18 17:23
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('blog', '0002_alter_post_date'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='post',
+ name='post_image',
+ field=models.ImageField(blank=True, upload_to='images/'),
+ ),
+ ]
diff --git a/apps/blog/migrations/0004_alter_post_post_image.py b/apps/blog/migrations/0004_alter_post_post_image.py
new file mode 100644
index 00000000..d926a40f
--- /dev/null
+++ b/apps/blog/migrations/0004_alter_post_post_image.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.0.6 on 2022-11-18 17:58
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('blog', '0003_post_post_image'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='post',
+ name='post_image',
+ field=models.TextField(blank=True),
+ ),
+ ]
diff --git a/apps/blog/migrations/0005_alter_post_post_image.py b/apps/blog/migrations/0005_alter_post_post_image.py
new file mode 100644
index 00000000..6e58db75
--- /dev/null
+++ b/apps/blog/migrations/0005_alter_post_post_image.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2022-12-31 17:13
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("blog", "0004_alter_post_post_image"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="post",
+ name="post_image",
+ field=models.ImageField(blank=True, upload_to="images/post-covers"),
+ ),
+ ]
diff --git a/apps/blog/migrations/0006_remove_post_post_image.py b/apps/blog/migrations/0006_remove_post_post_image.py
new file mode 100644
index 00000000..0913f97c
--- /dev/null
+++ b/apps/blog/migrations/0006_remove_post_post_image.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.1.4 on 2022-12-31 17:21
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("blog", "0005_alter_post_post_image"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="post",
+ name="post_image",
+ ),
+ ]
diff --git a/apps/blog/migrations/0007_alter_post_body.py b/apps/blog/migrations/0007_alter_post_body.py
new file mode 100644
index 00000000..f84c85f7
--- /dev/null
+++ b/apps/blog/migrations/0007_alter_post_body.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2023-01-26 01:11
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("blog", "0006_remove_post_post_image"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="post",
+ name="body",
+ field=models.TextField(blank=True),
+ ),
+ ]
diff --git a/apps/blog/migrations/0008_alter_post_date.py b/apps/blog/migrations/0008_alter_post_date.py
new file mode 100644
index 00000000..daa175db
--- /dev/null
+++ b/apps/blog/migrations/0008_alter_post_date.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2023-01-26 02:40
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("blog", "0007_alter_post_body"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="post",
+ name="date",
+ field=models.DateTimeField(auto_now_add=True),
+ ),
+ ]
diff --git a/apps/blog/migrations/0009_post_post_image.py b/apps/blog/migrations/0009_post_post_image.py
new file mode 100644
index 00000000..2e3da33a
--- /dev/null
+++ b/apps/blog/migrations/0009_post_post_image.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2023-01-26 03:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("blog", "0008_alter_post_date"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="post",
+ name="post_image",
+ field=models.ImageField(blank=True, upload_to="images//cover_images"),
+ ),
+ ]
diff --git a/apps/blog/migrations/0010_alter_post_date.py b/apps/blog/migrations/0010_alter_post_date.py
new file mode 100644
index 00000000..d3aa3b3f
--- /dev/null
+++ b/apps/blog/migrations/0010_alter_post_date.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2023-03-26 05:25
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("blog", "0009_post_post_image"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="post",
+ name="date",
+ field=models.DateField(blank=True, null=True),
+ ),
+ ]
diff --git a/apps/blog/migrations/0011_alter_post_date.py b/apps/blog/migrations/0011_alter_post_date.py
new file mode 100644
index 00000000..44e8fcde
--- /dev/null
+++ b/apps/blog/migrations/0011_alter_post_date.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2023-03-26 05:27
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("blog", "0010_alter_post_date"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="post",
+ name="date",
+ field=models.DateTimeField(auto_now_add=True),
+ ),
+ ]
diff --git a/apps/blog/migrations/0012_alter_post_date.py b/apps/blog/migrations/0012_alter_post_date.py
new file mode 100644
index 00000000..5138b39f
--- /dev/null
+++ b/apps/blog/migrations/0012_alter_post_date.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2023-03-26 05:31
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("blog", "0011_alter_post_date"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="post",
+ name="date",
+ field=models.DateTimeField(),
+ ),
+ ]
diff --git a/apps/blog/migrations/0013_post_views.py b/apps/blog/migrations/0013_post_views.py
new file mode 100644
index 00000000..42244f1f
--- /dev/null
+++ b/apps/blog/migrations/0013_post_views.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.1.4 on 2023-05-28 04:39
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("blog", "0012_alter_post_date"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="post",
+ name="views",
+ field=models.IntegerField(default=0),
+ ),
+ ]
diff --git a/apps/blog/migrations/0014_anonymouscommentuser_alter_comment_user_and_more.py b/apps/blog/migrations/0014_anonymouscommentuser_alter_comment_user_and_more.py
new file mode 100644
index 00000000..f05252b8
--- /dev/null
+++ b/apps/blog/migrations/0014_anonymouscommentuser_alter_comment_user_and_more.py
@@ -0,0 +1,53 @@
+# Generated by Django 4.1.4 on 2023-05-31 18:34
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("blog", "0013_post_views"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="AnonymousCommentUser",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=32)),
+ ("email", models.CharField(max_length=32)),
+ ("token", models.CharField(max_length=128, unique=True)),
+ ("avatar", models.CharField(blank=True, max_length=128)),
+ ],
+ ),
+ migrations.AlterField(
+ model_name="comment",
+ name="user",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ migrations.AddField(
+ model_name="comment",
+ name="anonymous_user",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ to="blog.anonymouscommentuser",
+ ),
+ ),
+ ]
diff --git a/apps/blog/migrations/__init__.py b/apps/blog/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/apps/blog/migrations/__init__.py
diff --git a/apps/blog/models.py b/apps/blog/models.py
new file mode 100644
index 00000000..f564cd75
--- /dev/null
+++ b/apps/blog/models.py
@@ -0,0 +1,102 @@
+import hashlib
+
+from django.conf import settings
+from django.db import models
+from django.utils.text import slugify
+
+UPLOAD_ROOT = 'images/'
+
+# Create your models here.
+class Category(models.Model):
+ name = models.CharField(max_length=50)
+ slug = models.SlugField(unique=True)
+ description = models.TextField(blank=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+
+ def save(self, *args, **kwargs):
+ if not self.slug or self.slug == '':
+ self.slug = slugify(self.name)
+ return super(Category, self).save(*args, **kwargs)
+
+ def __str__(self):
+ return self.name
+
+class Tag(models.Model):
+ name = models.CharField(max_length=50)
+ slug = models.SlugField(unique=True)
+ description = models.TextField(blank=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+
+ def save(self, *args, **kwargs):
+ if not self.slug or self.slug == '':
+ self.slug = slugify(self.name)
+ return super(Tag, self).save(*args, **kwargs)
+
+ def __str__(self):
+ return self.name
+
+class Post(models.Model):
+ title = models.CharField(max_length=100)
+ slug = models.SlugField(max_length=100, unique=True)
+ body = models.TextField(blank=True)
+ date = models.DateTimeField(auto_now_add=False)
+ post_image = models.ImageField(upload_to="{}/cover_images".format(UPLOAD_ROOT), blank=True)
+ author = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ on_delete=models.CASCADE,
+ )
+ category = models.ForeignKey(
+ 'Category',
+ on_delete=models.CASCADE,
+ )
+ tags = models.ManyToManyField('Tag', blank=True)
+ is_public = models.BooleanField(default=False)
+ views = models.IntegerField(default=0)
+
+ def save(self, *args, **kwargs):
+ if not self.slug or self.slug == '':
+ self.slug = slugify(self.title)
+ return super(Post, self).save(*args, **kwargs)
+
+ def __str__(self):
+ return str(self.title)
+
+class AnonymousCommentUser(models.Model):
+ name = models.CharField(max_length=32)
+ email = models.CharField(max_length=32)
+ token = models.CharField(max_length=128, unique=True)
+ avatar = models.CharField(max_length=128, blank=True)
+
+ @classmethod
+ def get_or_create(cls, email, token, avatar=''):
+ email_hash = hashlib.md5(email.encode('utf-8')).hexdigest()
+ token_hash = hashlib.sha256(token.encode('utf-8')).hexdigest()
+ return cls(email=email_hash, token=token_hash, avatar=avatar)
+
+ def __str__(self):
+ return self.name + "(" + self.email + ")"
+
+class Comment(models.Model):
+ post = models.ForeignKey(
+ 'Post',
+ on_delete=models.CASCADE,
+ )
+ user = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ on_delete=models.CASCADE,
+ blank=True,
+ null=True,
+ )
+ anonymous_user = models.ForeignKey(
+ 'AnonymousCommentUser',
+ on_delete=models.CASCADE,
+ blank=True,
+ null=True,
+ )
+ body = models.TextField()
+ created_at = models.DateTimeField(auto_now_add=True)
+ edited = models.BooleanField(default=False)
+ edited_at = models.DateTimeField(blank=True, null=True)
+
+ def __str__(self):
+ return self.body[:50] + '...' if len(self.body) > 50 else self.body
diff --git a/apps/blog/recommender.py b/apps/blog/recommender.py
new file mode 100644
index 00000000..280fa4b0
--- /dev/null
+++ b/apps/blog/recommender.py
@@ -0,0 +1,41 @@
+# This is a very simple recommender system that recommends posts based on the
+# current post user is reading.
+
+import numpy as np
+from bs4 import BeautifulSoup
+from sklearn.feature_extraction.text import TfidfVectorizer
+from sklearn.metrics.pairwise import cosine_similarity
+
+from .context_processors import add_excerpt, add_num_comments
+from .models import Post
+
+
+def next_read(post):
+ current_post = Post.objects.get(id=post.id)
+ posts = Post.objects.filter(is_public=True).exclude(id=current_post.id)
+
+ if len(posts) < 2:
+ return None
+
+ # Our method is very simple. First we compare the bodies of the posts to
+ # find the similarity between them. Then we sort the posts based on their
+ # similarity and return the post with the highest similarity.
+ #
+ # If no post has similarity > 0.5, we return the post with the highest
+ # number of views, preferably in the same category. If there is no post in
+ # the same category, we return the post with the highest number of views
+ # regardless of the category.
+
+ vectorizer = TfidfVectorizer(stop_words="english")
+ vectors = vectorizer.fit_transform(
+ [BeautifulSoup(post.body, "html.parser").text for post in posts]
+ )
+ current_vector = vectorizer.transform([current_post.body])
+ similarity = cosine_similarity(current_vector, vectors).flatten()
+ similarity = np.nan_to_num(similarity)
+ max_similarity = np.argmax(similarity)
+
+ post = posts[int(max_similarity)]
+ post.excerpt = add_excerpt(post)
+ post.num_comments = add_num_comments(post)
+ return post
diff --git a/apps/blog/tests.py b/apps/blog/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/apps/blog/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/apps/blog/urls.py b/apps/blog/urls.py
new file mode 100644
index 00000000..68b3ca6c
--- /dev/null
+++ b/apps/blog/urls.py
@@ -0,0 +1,50 @@
+from django.urls import path
+
+from . import views
+from .feed import RSSFeed
+
+app_name = "weblog"
+urlpatterns = [
+ path("", views.home, name="home"),
+ path("account", views.account, name="account"),
+ path("register", views.register, name="register"),
+ path("forgotpassword", views.forgotpassword, name="forgotpassword"),
+ path(
+ "forgotpassword/reset?uid=<str:uid>&token=<str:token>",
+ views.resetpassword,
+ name="resetpassword",
+ ),
+ path("search", views.search, name="search"),
+ path("weblog", views.articles, name="articles"),
+ path("weblog/<str:slug>", views.post, name="post"),
+ path("weblog/<str:slug>/comment", views.comment, name="comment"),
+ path("weblog/<str:slug>/anon_comment", views.anon_comment, name="anon_comment"),
+ path("weblog/<str:slug>/edit_comment", views.edit_comment, name="edit_comment"),
+ path(
+ "weblog/<str:slug>/anon_edit_comment",
+ views.anon_edit_comment,
+ name="anon_edit_comment",
+ ),
+ path(
+ "weblog/<str:slug>/delete_comment/<int:comment_id>",
+ views.delete_comment,
+ name="delete_comment",
+ ),
+ path(
+ "weblog/<str:slug>/anon_delete_comment/<int:comment_id>",
+ views.anon_delete_comment,
+ name="anon_delete_comment",
+ ),
+ path("archives", views.archives, name="archives"),
+ path("archives/<str:date>", views.articles, name="archive_posts"),
+ path("categories", views.categories, name="categories"),
+ path("categories/<str:cg>", views.articles, name="category_posts"),
+ path("tags", views.tags, name="tags"),
+ path("tags/<str:tag_slug>", views.tag_posts, name="tag_posts"),
+ path("~<str:username>", views.user_activity, name="user_activity"),
+ path("policy", views.policy, name="policy"),
+ path("socialify", views.socialify, name="socialify"),
+ path("rss/", RSSFeed(), name="rss_feed"),
+ path("anidata", views.anidata, name="anidata"),
+ path("anilist", views.anilist, name="anilist"),
+]
diff --git a/apps/blog/views.py b/apps/blog/views.py
new file mode 100644
index 00000000..fe9bc41f
--- /dev/null
+++ b/apps/blog/views.py
@@ -0,0 +1,1034 @@
+import hashlib
+import os
+import random
+import re
+import string
+from datetime import datetime
+from random import choice
+from string import ascii_letters, digits
+
+from datetime import timedelta
+from django.utils import timezone
+import requests
+from bs4 import BeautifulSoup
+from django.contrib import messages
+from django.contrib.auth.models import User
+from django.core.cache import cache
+from django.core.paginator import Paginator
+from django.http import Http404, HttpResponse, HttpResponseRedirect
+from django.shortcuts import redirect, render, reverse
+from django.views.decorators.clickjacking import xframe_options_sameorigin
+from dotenv import load_dotenv
+from haystack.query import SearchQuerySet
+from user_agents import parse
+
+from apps.administration.models import Announcement
+from users.accountFunctions import verify_token
+from users.forms import (
+ RegisterForm,
+ ResetPasswordForm,
+ UpdateUserDetailsForm,
+ ForgotPasswordForm,
+)
+from users.models import UserProfile
+from users.tokens import CaptchaTokenGenerator
+
+from .context_processors import (
+ add_excerpt,
+ add_num_comments,
+ avatar_list,
+ check_spam,
+ comment_processor,
+ highlight_code_blocks,
+ recent_posts,
+)
+from .models import AnonymousCommentUser, Category, Comment, Post, Tag
+from .recommender import next_read
+
+load_dotenv()
+
+
+def atoi(text):
+ return int(text) if text.isdigit() else text
+
+
+def natural_keys(text):
+ """
+ alist.sort(key=natural_keys) sorts in human order
+ http://nedbatchelder.com/blog/200712/human_sorting.html
+ (See Toothy's implementation in the comments)
+ """
+ return [atoi(c) for c in re.split(r"(\d+)", text)]
+
+
+# Create your views here.
+
+
+def home(request):
+ announcements = Announcement.objects.filter(is_public=True).order_by("-created_at")
+ announcements = announcements if len(announcements) > 0 else None
+ return render(
+ request,
+ "blog/home.html",
+ {"title": "Home", "posts": recent_posts(), "announcements": announcements},
+ )
+
+
+def tags(request):
+ tags = Tag.objects.all()
+ # add occurance count to each tag
+ for tag in tags:
+ tag.count = len(Post.objects.filter(tags__name__in=[tag.name], is_public=True))
+ tag.pxs = 10 + tag.count * 2 if tag.count < 10 else 30 + tag.count
+ tag.pxs = min(tag.pxs, 36)
+ tags = sorted(tags, key=lambda x: x.count, reverse=True)
+ tags = [tag for tag in tags if tag.count > 0]
+ return render(request, "blog/tags.html", {"title": "Tags", "tags": tags})
+
+
+def tag_posts(request, tag_slug):
+ try:
+ tag = Tag.objects.get(slug=tag_slug)
+ except Tag.DoesNotExist:
+ tag = {
+ "name": tag_slug,
+ "slug": tag_slug,
+ "count": 0,
+ }
+ return render(
+ request,
+ "blog/tagged.html",
+ {"title": "Posts Tagged With: " + tag_slug, "posts": None, "tag": tag},
+ )
+ posts = Post.objects.filter(tags__name__in=[tag.name], is_public=True).order_by(
+ "views"
+ )
+ for post in posts:
+ post.excerpt = add_excerpt(post)
+ post.num_comments = add_num_comments(post)
+ return render(
+ request,
+ "blog/tagged.html",
+ {"title": "Posts Tagged With: " + tag.name, "posts": posts, "tag": tag},
+ )
+
+
+def account(request):
+ user = request.user
+ avatarlist = avatar_list()
+ for key in avatarlist:
+ avatarlist[key] = [re.sub(r"\.gif$", "", string) for string in avatarlist[key]]
+ avatarlist[key].sort(key=natural_keys)
+ avatarlist = {k: avatarlist[k] for k in sorted(avatarlist)}
+
+ blinkies = [
+ re.sub(r"\.gif$", "", string) for string in os.listdir("static/images/blinkies")
+ ]
+ blinkies.sort(key=natural_keys)
+
+ if user.is_authenticated:
+ try:
+ user_profile = UserProfile.objects.get(user=user)
+ if not user_profile.avatar_url:
+ # Set a random avatar
+ avatar_dir = choice(list(avatarlist.keys()))
+ avatar_file = choice(avatarlist[avatar_dir])
+ user_profile.avatar_url = avatar_dir + "/" + avatar_file
+ user_profile.save()
+ except UserProfile.DoesNotExist:
+ # Create a new user profile and set a random avatar
+ user_profile = UserProfile.objects.create(user=user)
+ avatar_dir = choice(list(avatarlist.keys()))
+ avatar_file = choice(avatarlist[avatar_dir])
+ user_profile.avatar_url = avatar_dir + "/" + avatar_file
+ user_profile.save()
+
+ if request.GET.get("tab") == "details":
+ update_form = UpdateUserDetailsForm(
+ user=user,
+ initial={
+ "first_name": user.first_name,
+ "last_name": user.last_name,
+ "bio": user_profile.bio,
+ "is_public": user_profile.is_public,
+ "email_public": user_profile.email_public,
+ "location": user_profile.location,
+ },
+ )
+ else:
+ update_form = None
+
+ return render(
+ request,
+ "blog/account.html",
+ {
+ "title": "Account",
+ "user_profile": user_profile,
+ "avatarlist": avatarlist,
+ "update_form": update_form,
+ "blinkies": blinkies,
+ },
+ )
+ else:
+ # Redirect to login page
+ return redirect("blog:home")
+
+
+def register(request):
+ user = request.user
+ if user.is_authenticated:
+ return redirect("blog:account")
+ else:
+ random_string = "".join([choice(ascii_letters + digits) for n in range(6)])
+ captcha = CaptchaTokenGenerator().encrypt(random_string)
+ if request.method == "POST":
+ expected_captcha = CaptchaTokenGenerator().decrypt(
+ request.POST.get("expected_captcha")
+ )
+ form = RegisterForm(request.POST, expected_captcha=expected_captcha)
+ if form.is_valid():
+ form.save(request=request)
+ messages.success(
+ request,
+ "Account was created! Please check your email to verify your account.",
+ extra_tags="accountCreated",
+ )
+ return redirect("blog:register")
+ else:
+ return render(
+ request,
+ "blog/register.html",
+ {"title": "Register", "form": form, "captcha": captcha},
+ )
+ else:
+ form = RegisterForm(expected_captcha=random_string)
+ return render(
+ request,
+ "blog/register.html",
+ {"title": "Register", "form": form, "captcha": captcha},
+ )
+
+
+def forgotpassword(request):
+ user = request.user
+ if user.is_authenticated:
+ return redirect("blog:account")
+ else:
+ if request.method == "POST":
+ form = ForgotPasswordForm(request.POST)
+ if form.is_valid():
+ form.save(request)
+ messages.success(
+ request,
+ "An email has been sent to you with instructions on how to reset your password.",
+ extra_tags="passwordReset",
+ )
+ return redirect("blog:forgotpassword")
+ else:
+ return render(
+ request,
+ "blog/resetpass.html",
+ {"title": "Forgot Password", "form": form},
+ )
+ else:
+ form = ForgotPasswordForm()
+ return render(
+ request,
+ "blog/resetpass.html",
+ {"title": "Forgot Password", "form": form},
+ )
+
+
+def resetpassword(request, uid, token):
+ user = request.user
+ if user.is_authenticated:
+ return redirect("blog:account")
+ else:
+ if request.method == "POST":
+ form = ResetPasswordForm(request.POST)
+ if form.is_valid():
+ token_object = verify_token("resetpassword", uid, token)
+ if token_object is not None and token_object.verified:
+ user = User.objects.get(pk=token_object.user_id)
+ form.save(user)
+ messages.success(
+ request,
+ "Your password has been reset. You can now login with your new password.",
+ extra_tags="passwordReset",
+ )
+ token_object.delete()
+ return redirect("blog:resetpassword", uid=uid, token=token)
+ else:
+ messages.error(
+ request,
+ "Invalid or expired reset password link. Please try again.",
+ extra_tags="passwordReset",
+ )
+ return redirect("blog:forgotpassword")
+ else:
+ return render(
+ request,
+ "blog/resetpass_input.html",
+ {"title": "Reset Password", "form": form},
+ )
+ else:
+ form = ResetPasswordForm()
+ return render(
+ request,
+ "blog/resetpass_input.html",
+ {"title": "Reset Password", "form": form},
+ )
+
+
+def post(request, slug):
+ try:
+ post = Post.objects.get(slug=slug)
+
+ # Get the number of views for this post
+ x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
+ if x_forwarded_for:
+ ip = x_forwarded_for.split(",")[0]
+ else:
+ ip = request.META.get("REMOTE_ADDR")
+ user_agent_string = request.META.get("HTTP_USER_AGENT", "")
+ user_agent = parse(user_agent_string)
+ user_identifier = f"{ip}_{user_agent.browser.family}_{user_agent.browser.version_string}_{user_agent.os.family}_{user_agent.os.version_string}"
+ cache_key = f"post_view_count_{slug}_{user_identifier}"
+ view_count = cache.get(cache_key, 0)
+ if view_count == 0:
+ post.views += 1
+ post.save()
+ cache.set(cache_key, 1, 60 * 60 * 24 * 7) # 1 week
+
+ # code stored in .ql-syntax class
+ soup = BeautifulSoup(post.body, "html.parser")
+ code_blocks = soup.find_all("pre")
+ for code_block in code_blocks:
+ data_language = code_block.get("data-language")
+ if data_language == "true":
+ data_language = None
+ code_block.replace_with(
+ BeautifulSoup(
+ highlight_code_blocks(code_block, language=data_language),
+ "html.parser",
+ )
+ )
+
+ # float: right every other image
+ images = soup.find_all("img")
+ for i in range(len(images)):
+ if i % 2 != 0:
+ images[i][
+ "style"
+ ] = "float: right; margin-right: 0px; margin-left: 11px;"
+
+ # remove all paragraphs which are: "<p class="ql-align-justify"><br></p>"
+ for p in soup.find_all("p", class_="ql-align-justify"):
+ if p.find("br") is not None:
+ p.decompose()
+
+ # separate the body in two parts -> the first paragraph and the rest
+ first_paragraph = soup.find("p")
+ if first_paragraph is not None:
+ first_paragraph = str(first_paragraph)
+ first_paragraph = first_paragraph.replace("<p>", '<p class="subhead">')
+ soup.find("p").decompose()
+
+ post.first_paragraph = first_paragraph
+ post.body = str(soup)
+ post.views = "{:,}".format(post.views)
+
+ tags = post.tags.all()
+ comments = Comment.objects.filter(post=post)
+ for comment in comments:
+ if comment.user:
+ user_profile = UserProfile.objects.get(user=comment.user)
+ comment.avatar_url = user_profile.avatar_url
+ comment.processed_body = comment_processor(comment.body)
+
+ if comment.anonymous_user:
+ user_profile = comment.anonymous_user
+ comment.avatar_url = user_profile.avatar
+ comment.processed_body = comment_processor(comment.body)
+
+ if post.is_public:
+ # modify request.meta description (only text) and image
+ request.meta["description"] = BeautifulSoup(
+ first_paragraph, "html.parser"
+ ).get_text()
+ request.meta["image"] = (
+ "https://shi.foo/ignis/post_image/720/" + str(post.id) + ".gif"
+ )
+
+ read_next = next_read(post)
+
+ return render(
+ request,
+ "blog/post.html",
+ {
+ "title": post.title,
+ "post": post,
+ "tags": tags,
+ "comments": comments,
+ "view_count": view_count,
+ "read_next": read_next,
+ },
+ )
+ else:
+ if (
+ request.user.is_authenticated
+ and request.user.is_superuser
+ or request.user.is_staff
+ ):
+ return render(
+ request,
+ "blog/post.html",
+ {
+ "title": post.title,
+ "post": post,
+ "tags": tags,
+ "comments": comments,
+ "view_count": view_count,
+ },
+ )
+ else:
+ raise Http404
+ except Post.DoesNotExist:
+ raise Http404
+
+
+def comment(request, slug):
+ if request.method == "POST":
+ if request.user.is_authenticated:
+ try:
+ print(request.POST.get("comment"))
+ r_spam = check_spam(
+ comment=request.POST.get("comment"),
+ post=Post.objects.get(slug=slug),
+ )
+ if r_spam != "N":
+ messages.error(request, r_spam, extra_tags="spam")
+ return redirect(
+ reverse("blog:post", kwargs={"slug": slug}) + "#new-comment"
+ )
+
+ # then we continue
+ post = Post.objects.get(slug=slug)
+ if post.is_public:
+ comment = Comment.objects.create(
+ user=request.user, post=post, body=request.POST.get("comment")
+ )
+ return redirect(
+ reverse("blog:post", kwargs={"slug": slug})
+ + "#comment-"
+ + str(comment.id)
+ )
+ else:
+ if (
+ request.user.is_authenticated
+ and request.user.is_superuser
+ or request.user.is_staff
+ ):
+ Comment.objects.create(
+ user=request.user,
+ post=post,
+ body=request.POST.get("comment"),
+ )
+ return redirect(
+ reverse("blog:post", kwargs={"slug": slug})
+ + "#comment-"
+ + str(comment.id)
+ )
+ else:
+ return HttpResponse("Post not found!", status=404)
+ except Post.DoesNotExist:
+ return HttpResponse("Post not found!", status=404)
+
+ else:
+ return redirect("blog:home")
+ else:
+ return redirect("blog:home")
+
+
+def anon_comment(request, slug):
+ if request.method == "POST":
+ if request.user.is_authenticated:
+ # not allowed this is anonymous comment form
+ return redirect(reverse("blog:post", kwargs={"slug": slug}))
+ else:
+ anonymous_name = request.POST.get("anonymous-name")
+ anonymous_email = request.POST.get("anonymous-email")
+ anonymous_token, at = request.POST.get("anonymous-token"), request.POST.get(
+ "anonymous-token"
+ )
+ new_anonymous_token = request.POST.get("new-anonymous-token")
+ anonymous_comment = request.POST.get("anonymous-comment")
+ res_spam = check_spam(
+ comment=anonymous_comment, post=Post.objects.get(slug=slug)
+ )
+ if res_spam != "N":
+ messages.error(request, res_spam, extra_tags="spam")
+ return redirect(
+ reverse("blog:post", kwargs={"slug": slug}) + "#new-comment"
+ )
+
+ # now continue with the comment
+ if not anonymous_name:
+ messages.error(request, "Please enter a name!")
+ return redirect(reverse("blog:post", kwargs={"slug": slug}))
+ if not anonymous_comment:
+ messages.error(request, "Please enter a comment!")
+ return redirect(reverse("blog:post", kwargs={"slug": slug}))
+ if not anonymous_email:
+ anonymous_email = (
+ "".join(random.choice(string.ascii_lowercase) for i in range(10))
+ + "@anonymous.shi.foo"
+ )
+ if not anonymous_token:
+ anonymous_token = "".join(
+ random.choice(string.ascii_lowercase) for i in range(10)
+ )
+ at = anonymous_token
+
+ # generate a random avatar for the anonymous user
+ avatarlist = avatar_list()
+ for key in avatarlist:
+ avatarlist[key] = [
+ re.sub(r"\.gif$", "", string) for string in avatarlist[key]
+ ]
+ avatarlist[key].sort(key=natural_keys)
+ avatarlist = {k: avatarlist[k] for k in sorted(avatarlist)}
+ avatar_dir = choice(list(avatarlist.keys()))
+ avatar_file = choice(avatarlist[avatar_dir])
+ anonymous_avatar = avatar_dir + "/" + avatar_file
+ anonymous_token = hashlib.sha256(
+ anonymous_token.encode("utf-8")
+ ).hexdigest()
+ try:
+ anonymous_user = AnonymousCommentUser.objects.get(
+ email=anonymous_email, token=anonymous_token
+ )
+ except AnonymousCommentUser.DoesNotExist:
+ anonymous_user = AnonymousCommentUser.objects.create(
+ email=anonymous_email,
+ token=anonymous_token,
+ avatar=anonymous_avatar,
+ )
+ if new_anonymous_token:
+ at = new_anonymous_token
+ new_anonymous_token = hashlib.sha256(
+ new_anonymous_token.encode("utf-8")
+ ).hexdigest()
+ anonymous_user.token = new_anonymous_token
+ anonymous_user.save()
+
+ # update the anonymous user's name if it has changed
+ if anonymous_user.name != anonymous_name:
+ anonymous_user.name = anonymous_name
+ anonymous_user.save()
+
+ comment = Comment.objects.create(
+ anonymous_user=anonymous_user,
+ post=Post.objects.get(slug=slug),
+ body=anonymous_comment,
+ )
+
+ # redirect to the post with the comment but set the anonymous user cookie
+ response = redirect(
+ reverse("blog:post", kwargs={"slug": slug})
+ + "#comment-"
+ + str(comment.id)
+ )
+ response.set_cookie(
+ "anonymous_name", anonymous_user.name, max_age=60 * 60 * 24 * 365
+ )
+ response.set_cookie(
+ "anonymous_email", anonymous_user.email, max_age=60 * 60 * 24 * 365
+ )
+ response.set_cookie("anonymous_token", at, max_age=60 * 60 * 24 * 365)
+
+ return response
+
+ else:
+ return redirect("blog:home")
+
+
+def edit_comment(request, slug):
+ if request.method == "POST":
+ if request.user.is_authenticated:
+
+ try:
+ comment = Comment.objects.get(id=request.POST.get("comment_id"))
+ # check for spam first
+ user_ip = request.META.get("HTTP_X_FORWARDED_FOR")
+ if user_ip:
+ user_ip = user_ip.split(",")[0]
+ else:
+ user_ip = request.META.get("REMOTE_ADDR")
+ user_agent_string = request.META.get("HTTP_USER_AGENT", "")
+ user_agent = parse(user_agent_string)
+ # if check_spam(user_ip=user_ip, user_agent=user_agent, comment=request.POST.get('body'), author=request.user.username
+ res_spam = check_spam(
+ comment=request.POST.get("body"), post=comment.post
+ )
+ if res_spam != "N":
+ messages.error(request, request.POST.get("body"), extra_tags="spam")
+ return redirect(
+ reverse("blog:post", kwargs={"slug": slug})
+ + "#comment-"
+ + str(comment.id)
+ )
+ if comment.user == request.user:
+ comment.body = request.POST.get("body")
+ comment.edited = True
+ comment.edited_at = datetime.now()
+ comment.save()
+ return redirect(
+ reverse("blog:post", kwargs={"slug": slug})
+ + "#comment-"
+ + str(comment.id)
+ )
+ else:
+ return HttpResponse("Unauthorized!", status=401)
+ except Comment.DoesNotExist:
+ return HttpResponse("Comment not found!", status=404)
+ else:
+ return redirect("blog:home")
+ else:
+ return redirect("blog:home")
+
+
+def anon_edit_comment(request, slug):
+ if request.method == "POST":
+ if request.user.is_authenticated:
+ # not allowed this is anonymous comment form
+ return redirect(reverse("blog:post", kwargs={"slug": slug}))
+ else:
+
+ anonymous_token = request.COOKIES.get("anonymous_token")
+ if not anonymous_token:
+ return HttpResponse("Unauthorized!", status=401)
+ try:
+ anonymous_token = hashlib.sha256(
+ anonymous_token.encode("utf-8")
+ ).hexdigest()
+ comment = Comment.objects.get(id=request.POST.get("comment_id"))
+ # check for spam first
+ user_ip = request.META.get("HTTP_X_FORWARDED_FOR")
+ if user_ip:
+ user_ip = user_ip.split(",")[0]
+ else:
+ user_ip = request.META.get("REMOTE_ADDR")
+ user_agent_string = request.META.get("HTTP_USER_AGENT", "")
+ user_agent = parse(user_agent_string)
+ res_spam = check_spam(
+ comment=request.POST.get("body"), post=comment.post
+ )
+ if res_spam != "N":
+ # if check_spam(user_ip=user_ip, user_agent=user_agent, comment=request.POST.get('body'), author=comment.anonymous_user.name):
+ messages.error(request, request.POST.get("body"), extra_tags="spam")
+ return redirect(
+ reverse("blog:post", kwargs={"slug": slug})
+ + "#comment-"
+ + str(comment.id)
+ )
+ if comment.anonymous_user.token == anonymous_token:
+ comment.body = request.POST.get("body")
+ comment.edited = True
+ comment.edited_at = datetime.now()
+ comment.save()
+ return redirect(
+ reverse("blog:post", kwargs={"slug": slug})
+ + "#comment-"
+ + str(comment.id)
+ )
+ else:
+ return HttpResponse("Unauthorized!", status=401)
+ except Comment.DoesNotExist:
+ return HttpResponse("Comment not found!", status=404)
+ else:
+ return redirect("blog:home")
+
+
+def delete_comment(request, slug, comment_id):
+ if request.user.is_authenticated:
+ try:
+ comment = Comment.objects.get(id=comment_id)
+ if comment.user == request.user:
+ comment.delete()
+ return redirect(
+ reverse("blog:post", kwargs={"slug": slug}) + "#comments"
+ )
+ else:
+ return HttpResponse("Unauthorized!", status=401)
+ except Comment.DoesNotExist:
+ return HttpResponse("Comment not found!", status=404)
+ else:
+ return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
+
+
+def anon_delete_comment(request, slug, comment_id):
+ if request.user.is_authenticated:
+ # not allowed this is anonymous comment form
+ return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
+ else:
+ anonymous_token = request.COOKIES.get("anonymous_token")
+ if not anonymous_token:
+ return HttpResponse("Unauthorized!", status=401)
+ anonymous_token = hashlib.sha256(anonymous_token.encode("utf-8")).hexdigest()
+ try:
+ comment = Comment.objects.get(
+ id=comment_id, anonymous_user__token=anonymous_token
+ )
+ comment.delete()
+ return redirect(reverse("blog:post", kwargs={"slug": slug}) + "#comments")
+ except Comment.DoesNotExist:
+ return HttpResponse("Comment not found!", status=404)
+
+
+def search(request):
+ query = request.GET.get("q")
+ search_in = (
+ request.GET.getlist("search_in") if request.GET.get("search_in") else ["posts"]
+ )
+ sort_by = request.GET.get("sort_by") if request.GET.get("sort_by") else "relevance"
+ order = request.GET.get("order") if request.GET.get("order") else "ascending"
+ date_range = (
+ request.GET.get("date_range") if request.GET.get("date_range") else "any"
+ )
+ search_model_map = {
+ "posts": Post,
+ "users": User,
+ "comments": Comment,
+ }
+ now = timezone.now()
+
+ if query:
+ search_results = SearchQuerySet().filter(content=query)
+ if search_in:
+ search_results = search_results.models(
+ *[search_model_map[model] for model in search_in]
+ )
+
+ # search_results = [result.object for result in search_results]
+ posts = [
+ result.object
+ for result in search_results
+ if isinstance(result.object, Post)
+ ]
+ users = [
+ result.object
+ for result in search_results
+ if isinstance(result.object, User)
+ ]
+ comments = [
+ result.object
+ for result in search_results
+ if isinstance(result.object, Comment)
+ ]
+
+ # match-case sort_by
+ if sort_by == "relevance" and order == "ascending":
+ posts = sorted(posts, key=lambda post: post.views)
+ users = sorted(users, key=lambda user: user.username)
+ comments = sorted(comments, key=lambda comment: comment.id)
+ elif sort_by == "relevance" and order == "descending":
+ posts = sorted(posts, key=lambda post: post.views, reverse=True)
+ users = sorted(users, key=lambda user: user.username, reverse=True)
+ comments = sorted(comments, key=lambda comment: comment.id, reverse=True)
+ elif sort_by == "date" and order == "ascending":
+ posts = sorted(posts, key=lambda post: post.date)
+ users = sorted(users, key=lambda user: user.date_joined)
+ comments = sorted(comments, key=lambda comment: comment.created_at)
+ elif sort_by == "date" and order == "descending":
+ posts = sorted(posts, key=lambda post: post.date, reverse=True)
+ users = sorted(users, key=lambda user: user.date_joined, reverse=True)
+ comments = sorted(
+ comments, key=lambda comment: comment.created_at, reverse=True
+ )
+
+ # filter by date_range
+ if date_range == "past_day":
+ posts = [post for post in posts if post.date >= now - timedelta(days=1)]
+ users = [
+ user for user in users if user.date_joined >= now - timedelta(days=1)
+ ]
+ comments = [
+ comment
+ for comment in comments
+ if comment.created_at >= now - timedelta(days=1)
+ ]
+ elif date_range == "past_week":
+ posts = [post for post in posts if post.date >= now - timedelta(days=7)]
+ users = [
+ user for user in users if user.date_joined >= now - timedelta(days=7)
+ ]
+ comments = [
+ comment
+ for comment in comments
+ if comment.created_at >= now - timedelta(days=7)
+ ]
+ elif date_range == "past_month":
+ posts = [post for post in posts if post.date >= now - timedelta(days=30)]
+ users = [
+ user for user in users if user.date_joined >= now - timedelta(days=30)
+ ]
+ comments = [
+ comment
+ for comment in comments
+ if comment.created_at >= now - timedelta(days=30)
+ ]
+ elif date_range == "past_year":
+ posts = [post for post in posts if post.date >= now - timedelta(days=365)]
+ users = [
+ user for user in users if user.date_joined >= now - timedelta(days=365)
+ ]
+ comments = [
+ comment
+ for comment in comments
+ if comment.created_at >= now - timedelta(days=365)
+ ]
+ elif date_range == "any":
+ # no need to filter
+ pass
+
+ search_results = len(posts) + len(users) + len(comments)
+ else:
+ search_results = 0
+
+ # attach user profiles
+ for user in users:
+ user.profile = UserProfile.objects.get(user=user)
+
+ # parse comments
+ for comment in comments:
+ comment.body = comment_processor(comment.body)
+
+ return render(
+ request,
+ "blog/search.html",
+ {
+ "title": f"Search results for '{query}'",
+ "query": query,
+ "search_results": search_results,
+ "search_in": search_in,
+ "sort_by": sort_by,
+ "order": order,
+ "date_range": date_range,
+ "posts": posts,
+ "users": users,
+ "comments": comments,
+ },
+ )
+
+
+def articles(request, date=None, cg=None):
+ type = "articles"
+ page = request.GET.get("page") if request.GET.get("page") else 1
+ order_by = request.GET.get("order_by") if request.GET.get("order_by") else "date"
+ direction = request.GET.get("direction") if request.GET.get("direction") else "desc"
+ categories = Category.objects.all()
+ category = request.GET.get("category")
+ try:
+ page = int(page)
+ except:
+ page = 1
+
+ posts = Post.objects.filter(is_public=True)
+
+ if date:
+ date_month = date.split("_")[0] # month name like 'Decemeber'
+ date_year = date.split("_")[1] # year like '2019'
+ date_m = datetime.strptime(
+ date_month, "%B"
+ ).month # convert month name to month number
+ posts = Post.objects.filter(
+ is_public=True, date__month=date_m, date__year=date_year
+ )
+ type = "articles-archive"
+ date = date_month + " " + date_year
+
+ if cg:
+ cg = str.lower(cg)
+ if category and cg != category and category != "all":
+ return redirect(reverse("blog:categories") + "/{}".format(category))
+ category = cg
+ posts = Post.objects.filter(is_public=True, category__slug=cg)
+ type = "articles-category"
+
+ posts = (
+ posts.order_by("-" + order_by)
+ if direction == "desc"
+ else Post.objects.filter(is_public=True).order_by(order_by)
+ )
+ if category and category != "all":
+ posts = posts.filter(category__slug=category)
+ category_name = Category.objects.get(slug=category).name
+ else:
+ category = "all"
+ posts = Paginator(posts, 10)
+ num_pages = posts.num_pages
+ try:
+ posts = posts.page(page)
+ except:
+ posts = posts.page(num_pages)
+
+ for post in posts:
+ post.excerpt = add_excerpt(post)
+ post.num_comments = add_num_comments(post)
+ return render(
+ request,
+ "blog/articles.html",
+ {
+ "title": "Articles",
+ "posts": posts,
+ "num_pages": num_pages,
+ "page": page,
+ "order_by": order_by,
+ "direction": direction,
+ "categories": categories,
+ "category": category,
+ "category_name": category_name if category != "all" else "",
+ "type": type,
+ "date": date if date else "",
+ "cg": cg if cg else "",
+ },
+ )
+
+
+def user_activity(request, username):
+ try:
+ user = User.objects.get(username__iexact=username)
+ user_profile = UserProfile.objects.get(user=user)
+ if user_profile.is_public or user == request.user:
+ recent_comments = Comment.objects.filter(user=user).order_by("-created_at")[
+ :5
+ ]
+ else:
+ recent_comments = []
+
+ if user_profile.email_public:
+ user_email = user.email
+ else:
+ user_email = ""
+
+ for comment in recent_comments:
+ comment.body = comment_processor(comment.body)
+
+ return render(
+ request,
+ "blog/activity.html",
+ {
+ "title": "User Activity",
+ "activity_user": user,
+ "activity_user_profile": user_profile,
+ "activity_recent_comments": recent_comments,
+ "activity_user_email": user_email,
+ },
+ )
+ except User.DoesNotExist:
+ # return default 404 page
+ raise Http404
+
+
+def archives(request):
+ archives = Post.objects.filter(is_public=True).dates("date", "month", order="DESC")
+ return render(
+ request, "blog/archives.html", {"title": "Archives", "archives": archives}
+ )
+
+
+def categories(request):
+ categories = Category.objects.all()
+ return render(
+ request,
+ "blog/categories.html",
+ {"title": "Categories", "categories": categories},
+ )
+
+
+def policy(request):
+ return render(request, "blog/site_policy.html", {"title": "Site Policy"})
+
+
+def socialify(request):
+ url = request.GET.get("url") if request.GET.get("url") else None
+ if url:
+ # convert Github URL to repo owner/name
+ if "github.com" in url:
+ url = url.split("github.com/")[1]
+ url = url.split("/")
+ url = url[0] + "/" + url[1]
+ socialify_options = {
+ "theme": "Dark" if not request.GET.get("theme") else request.GET.get("theme"),
+ "font": "Inter" if not request.GET.get("font") else request.GET.get("font"),
+ "description": (
+ 0 if not request.GET.get("description") else request.GET.get("description")
+ ),
+ "forks": 0 if not request.GET.get("forks") else request.GET.get("forks"),
+ "issues": 0 if not request.GET.get("issues") else request.GET.get("issues"),
+ "language_1": (
+ 0 if not request.GET.get("language_1") else request.GET.get("language_1")
+ ),
+ "language_2": (
+ 0 if not request.GET.get("language_2") else request.GET.get("language_2")
+ ),
+ "name": 0 if not request.GET.get("name") else request.GET.get("name"),
+ "owner": 1 if not request.GET.get("owner") else request.GET.get("owner"),
+ "stargazers": (
+ 0 if not request.GET.get("stargazers") else request.GET.get("stargazers")
+ ),
+ "pulls": 0 if not request.GET.get("pulls") else request.GET.get("pulls"),
+ "pattern": (
+ "Plus" if not request.GET.get("pattern") else request.GET.get("pattern")
+ ),
+ }
+
+ for key, value in socialify_options.items():
+ if value == "on":
+ socialify_options[key] = 1
+ elif value == "off":
+ socialify_options[key] = 0
+
+ return render(
+ request,
+ "blog/socialify.html",
+ {"title": "Socialify", "options": socialify_options, "url": url},
+ )
+
+
+def anilist(request):
+ return render(request, "blog/anilist.html", {"title": "My Anime List"})
+
+
+@xframe_options_sameorigin
+def anidata(request):
+ malURL = "https://myanimelist.net/animelist/crvs"
+ MAL = requests.get(malURL)
+ MALContent = MAL.content
+ MALStatus = MAL.status_code
+
+ if MALStatus != 200:
+ MALContent = '<html><head><link rel="stylesheet" href="/static/css/styles.css"><style>img { width: 75%; display: block; margin: -20px auto 20px auto; } h1 { text-align: center; } p { text-align: center; } body {background: transparent !important;} </style></head><body><img src="/static/images/site/sad-failure.gif" alt="Sad Failure"><h1>MyAnimeList does not seem to respond at the moment.</h1><p>Maybe, we go <a href="https://myanimelist.net/animelist/crvs" target="_blank">knock on their door</a> instead?</p></body></html>'
+ else:
+ MALContent = MALContent.decode("utf-8")
+ MALParsed = BeautifulSoup(MALContent, "html.parser")
+ # remove script tags
+ for tag in MALParsed(["script", "meta", "noscript"]):
+ tag.extract()
+
+ # add myanimelist.net to relative links
+ for link in MALParsed.find_all("a"):
+ if link.get("href") and link.get("href")[0] == "/":
+ link["href"] = "https://myanimelist.net" + link["href"]
+
+ # make all links open in new tab
+ link["target"] = "_blank"
+
+ MALContent = MALParsed.prettify()
+
+ return render(
+ request,
+ "blog/anidata.html",
+ {"title": "My Anime List", "MALContent": MALContent},
+ )
diff --git a/apps/core/urls.py b/apps/core/urls.py
index 8612977a..db770dd0 100644
--- a/apps/core/urls.py
+++ b/apps/core/urls.py
@@ -2,7 +2,7 @@ from django.urls import path
from . import views
-app_name = "blog"
+app_name = "core"
urlpatterns = [
path("", views.home, name="home"),
]