Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
88318cd
Add feed
frcroth Feb 13, 2024
1066732
Improve feed styling
frcroth Feb 13, 2024
689022d
Merge branch 'main' into feed
frcroth Feb 14, 2024
fcfbbbb
Add pagination
frcroth Feb 14, 2024
e96a71c
Automatically redirect to feed
frcroth Feb 14, 2024
c0d565a
Render snippets as html
frcroth Feb 14, 2024
6791863
Add post and feed to test setup data
frcroth Feb 14, 2024
79ff01b
Use django builtin html truncating
frcroth Feb 14, 2024
ac39946
Fix lint
frcroth Feb 14, 2024
a8f5cf5
Add tests for feed
frcroth Feb 14, 2024
14bb3da
Add release action
frcroth Feb 14, 2024
e3f6ac2
Add truncate limit to env file
frcroth Feb 14, 2024
86b075b
Rename feed to newsfeed to avoid problems with builtin django feed
frcroth Feb 15, 2024
118f63e
Allow setting images for post accounts
frcroth Feb 15, 2024
02f5d2c
Rename PostAccount to NewsFeedAccount
frcroth Feb 20, 2024
95a97ff
Change api route, fix button alignment
frcroth Feb 20, 2024
247d3c9
Fix api routes in tests
frcroth Feb 20, 2024
06eabc0
Merge branch 'main' into feed
frcroth Feb 23, 2024
be9e8f8
Update translations
frcroth Feb 23, 2024
8abcb98
Make layout somewhat more mobile friendly
frcroth Feb 23, 2024
e808ca8
Fix preview when creating post
frcroth Feb 23, 2024
2cd09d8
Merge branch 'main' into feed
frcroth Feb 23, 2024
8bb5aed
Remove action, create script to publish currently running version as …
frcroth Feb 25, 2024
7708394
Optionally allow admin to specify tag for publishing release
frcroth Feb 25, 2024
994d996
Move command to appropriate app
frcroth Feb 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ TENCA_HASH_STORAGE_CLASS=myhpi.tenca_django.models.DjangoModelCachedDescriptionH
TENCA_TEST_LIST_DOMAIN=TENCA_WEB_UI_HOSTNAME

ANONYMOUS_IP_RANGE_GROUPS="127.0.0.1/32=Moderators,127.0.0.0/8=localhost"
REDIRECT_TO_FEED=True
FEED_TRUNCATE_LIMIT=400
RELEASE_POST_ACCOUNT_ID=2 # Will be 2 if the create test data script is used
1 change: 1 addition & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ python manage.py migrate
python manage.py compilestatic
python manage.py collectstatic --no-input
python manage.py compilemessages
python manage.py publish_release_post

exec "$@"
45 changes: 44 additions & 1 deletion myhpi/core/management/commands/create_test_data.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import random
from datetime import date

Expand All @@ -14,6 +15,7 @@
RootPage,
SecondLevelMenuItem,
)
from myhpi.feed.models import NewsFeed, NewsFeedAccount, Post
from myhpi.polls.models import PollList, RankedChoiceOption, RankedChoicePoll
from myhpi.tests.core.setup import create_collections, create_documents

Expand Down Expand Up @@ -327,6 +329,9 @@ def create_some_pages(users, groups, documents):
visible_for=[groups[0]],
)
)

# Create poll

option_alice = RankedChoiceOption(
name="Alice", description=generate_text(), poll=slash_1999_poll
)
Expand All @@ -341,6 +346,44 @@ def create_some_pages(users, groups, documents):
slash_1999_poll.options.add(option_bob)
slash_1999_poll.options.add(option_charlie)

# Create feed and posts

feed = root_page.add_child(
instance=NewsFeed(
title="Feed",
slug="feed",
show_in_menus=True,
is_public=False,
visible_for=[groups[0]],
)
)

the_potsdam_post = NewsFeedAccount.objects.create(
post_key="SECRET_TEST_KEY",
name="the Potsdam Post",
)

for i in range(10):
feed.add_child(
instance=Post(
title=f"Post {i}",
slug=f"post-{i}",
show_in_menus=False,
is_public=False,
post_account=the_potsdam_post,
body=generate_text(),
visible_for=[groups[0]],
first_published_at=datetime.datetime.now(datetime.UTC)
- datetime.timedelta(10)
+ datetime.timedelta(days=i),
)
)

myhpi_releases = NewsFeedAccount.objects.create(
post_key="SECRET_TEST_KEY_2",
name="myHPI Releases",
)


class Command(BaseCommand):
help = "Creates test data (user, pages, etc.) for myHPI."
Expand All @@ -354,6 +397,6 @@ def handle(self, *args, **options):
create_some_pages(users, groups, documents)
self.stdout.write(
self.style.SUCCESS(
'Test data created succesfully. Remember that you need to create a superuser manually with "python manage.py createsuperuser".'
'Test data created successfully. Remember that you need to create a superuser manually with "python manage.py createsuperuser".'
)
)
4 changes: 2 additions & 2 deletions myhpi/core/markdown/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
from myhpi.core.markdown.extensions import MinuteExtension


def render_markdown(text, context=None, with_abbreveations=True):
def render_markdown(text, context=None, with_abbreviations=True):
"""
Turn markdown into HTML.
"""
if context is None or not isinstance(context, dict):
context = {}
markdown_html, toc = _transform_markdown_into_html(text, with_abbreviations=with_abbreveations)
markdown_html, toc = _transform_markdown_into_html(text, with_abbreviations=with_abbreviations)
sanitised_markdown_html = _sanitise_markdown_html(markdown_html)
return mark_safe(sanitised_markdown_html), mark_safe(toc)

Expand Down
12 changes: 6 additions & 6 deletions myhpi/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ class BasePage(Page):
index.FilterField("is_public"),
]

@property
def last_edited_by(self):
if self.get_latest_revision():
user = self.get_latest_revision().user
return f"{user.first_name} {user.last_name}"


class InformationPageForm(WagtailAdminPageForm):
def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -83,12 +89,6 @@ class InformationPage(BasePage):

base_form_class = InformationPageForm

@property
def last_edited_by(self):
if self.get_latest_revision():
user = self.get_latest_revision().user
return f"{user.first_name} {user.last_name}"


class MinutesList(BasePage):
group = ForeignKey(Group, on_delete=models.PROTECT)
Expand Down
Empty file added myhpi/feed/__init__.py
Empty file.
Empty file.
Empty file.
77 changes: 77 additions & 0 deletions myhpi/feed/management/commands/publish_release_post.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import sys

import requests
from django.conf import settings
from django.contrib.auth.models import Group
from django.core.management import BaseCommand

from myhpi.feed.models import NewsFeed, NewsFeedAccount, Post


def get_release(tag):
r = requests.get(f"https://api.github.com/repos/fsr-de/myHPI/releases/tags/{tag}")
r.raise_for_status()
return r.json()


def publish_release_post(force_publish, tag=None):
if tag is None:
version = settings.MYHPI_VERSION
tag = f"v{version}"
release = get_release(tag)

feed = NewsFeed.objects.first()
if feed is None:
print("No feed found, not publishing release post", file=sys.stderr)
return

existing_posts = Post.objects.filter(title=release["name"])
if existing_posts.exists() and not force_publish:
print(
"Release post already exists, not publishing again. Use -f option to create a post anyway.",
file=sys.stderr,
)
return

account_id = settings.RELEASE_POST_ACCOUNT_ID
if account_id is None:
print("No account id found, not publishing release post", file=sys.stderr)
return
account = NewsFeedAccount.objects.filter(id=account_id).first()
if account is None:
print("No account found, not publishing release post", file=sys.stderr)
return

post = feed.add_child(
instance=Post(
title=release["name"],
body=release["body"],
post_account=account,
visible_for=Group.objects.all(),
is_public=True,
first_published_at=release["published_at"],
)
)
print(f"Published release post with id {post.id}", file=sys.stderr)


class Command(BaseCommand):
help = "Creates a feed post for the latest release of myHPI"

def add_arguments(self, parser):
parser.add_argument(
"--force",
"-f",
action="store_true",
help="Create a release post even if the release is already published.",
)

parser.add_argument(
"--tag",
"-t",
type=str,
help="Specify a tag to publish a release post for (defaults to currently installed version, typically latest tag).",
)

def handle(self, *args, **options):
publish_release_post(options["force"], options["tag"])
26 changes: 26 additions & 0 deletions myhpi/feed/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from django.http import HttpResponseRedirect

from myhpi import settings
from myhpi.feed.models import NewsFeed


class FeedRedirectMiddleware:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could also implement this inside the serve() of RootPage, that would only do the check when it is actually necessary and not with every request

Copy link
Copy Markdown
Contributor Author

@frcroth frcroth Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that would increase coupling. Currently disabling/removing the app is very easy, it is never mentioned / used in core. Your thoughts on this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, it would be okay to introduce some change to core, as myHPI (this application) is developed specifically for our myHPI website and not meant as a generic CMS. Hence, we don't need to be able to deactivate/remove the newsfeed app per installation and that small amount of coupling would be acceptable.

However, I'm not that familiar with Django patterns, so I can't really argue on whether there is even a need to replace this middleware or not.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lukasrad02 Felix and I had a conversation about the conflict of cool new features vs. size of codebase/maintainability. I made the argument that as long as fancy features are loosely coupled, future maintainer could easily remove them without the need to understand/update/fix them and therefore delay or block the maintenance of the core app.

The middleware is probably fine, as it doesn't do much. I just thought about it because we always struggle with pageload times and this is code that is not necessary on all pages but the root page.

Another way that we could pursue this is by adding some kind of "plugin" mechanism. Again citing ephios: we are sending out custom django signals to "plugins" (other django apps within the same repo or installed separately) and let them inject stuff at various places. e.g. there is a signal that says "hey plugin, give me some html to append to the homepage if you want to". This way, the additional code is only run when it is necessary.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with you two deciding on the best approach to do this. Especially when it comes to performance or plugins in Django, I would not be able to give any qualified arguments.

def __init__(self, get_response):
self.get_response = get_response
self.feed = NewsFeed.objects.first()

def __call__(self, request):
self.process_request(request)
return self.get_response(request)

def process_request(self, request):
user = request.user
if (
self.feed
and request.path == "/"
and user.is_authenticated
and self.feed.visible_for.intersection(user.groups.all()).exists()
and request.GET.get("no_redirect") != "true"
and settings.REDIRECT_TO_FEED
):
request.path = self.feed.slug
88 changes: 88 additions & 0 deletions myhpi/feed/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Generated by Django 4.2.9 on 2024-02-15 17:16

import django.db.models.deletion
import wagtail.users.models
from django.db import migrations, models

import myhpi.core.markdown.fields


class Migration(migrations.Migration):
initial = True

dependencies = [
("core", "0010_minutes_location"),
]

operations = [
migrations.CreateModel(
name="NewsFeed",
fields=[
(
"basepage_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.basepage",
),
),
],
options={
"abstract": False,
},
bases=("core.basepage",),
),
migrations.CreateModel(
name="NewsFeedAccount",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("post_key", models.CharField(max_length=255)),
("name", models.CharField(max_length=255)),
(
"icon",
models.ImageField(
blank=True, null=True, upload_to=wagtail.users.models.upload_avatar_to
),
),
],
),
migrations.CreateModel(
name="Post",
fields=[
(
"basepage_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.basepage",
),
),
("body", myhpi.core.markdown.fields.CustomMarkdownField()),
(
"post_account",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="posts",
to="feed.newsfeedaccount",
),
),
],
options={
"abstract": False,
},
bases=("core.basepage",),
),
]
Empty file.
72 changes: 72 additions & 0 deletions myhpi/feed/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from django.core.paginator import Paginator
from django.db import models
from wagtail.admin.panels import FieldPanel
from wagtail.snippets.models import register_snippet
from wagtail.users.models import UserProfile, upload_avatar_to

from myhpi import settings
from myhpi.core.markdown.fields import CustomMarkdownField
from myhpi.core.models import BasePage


class NewsFeed(BasePage):
parent_page_types = ["core.RootPage"]
subpage_types = ["Post"]
max_count = 1

def get_context(self, request, *args, **kwargs):
context = super().get_context(request, *args, **kwargs)
all_posts = self.get_children().live().order_by("-first_published_at").specific()
paginator = Paginator(all_posts, 10)
page = paginator.get_page(request.GET.get("page"))

# Currently, there is no filter for visibility on posts. That is, it is assumed that all post should be visible to students
context["page"] = page
context["posts"] = page.object_list
context["limit"] = settings.FEED_TRUNCATE_LIMIT
return context


class Post(BasePage):
parent_page_types = ["NewsFeed"]
subpage_types = []
body = CustomMarkdownField()
post_account = models.ForeignKey(
"NewsFeedAccount", related_name="posts", on_delete=models.SET_NULL, null=True, blank=True
)

content_panels = BasePage.content_panels + [
FieldPanel("body", classname="full"),
]

@property
def author(self):
if self.last_edited_by:
return self.last_edited_by
else:
return self.post_account

@property
def date(self):
if self.first_published_at:
return self.first_published_at.date()

@property
def image_url(self):
if self.post_account and self.post_account.icon:
return self.post_account.icon.url
# Currently does not use the wagtail user icon since it is not used anywhere else
else:
return "https://www.gravatar.com/avatar/b1a83a1baa8a1d45c905a59217a7d30a?s=140&d=mm"


# The NewsFeedAccount is used to create posts via API
@register_snippet
class NewsFeedAccount(models.Model):
post_key = models.CharField(max_length=255)
name = models.CharField(max_length=255)

icon = models.ImageField(upload_to=upload_avatar_to, null=True, blank=True)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the correct reference to also use all of the wagtail stuff for images?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same code that is used for uploading user avatars in Wagtail. Does not have to deal with collections etc., which makes sense because the post accounts should only be edited by admins anyway.


def __str__(self):
return self.name
Loading