Added news entries
API CI / api-tests (pull_request) Failing after 32s
CI / test (pull_request) Failing after 33s

This commit is contained in:
Tim Lorsbach
2026-10-21 09:53:58 +02:00
parent 5bbe316c24
commit 86c9ac887e
11 changed files with 204 additions and 24 deletions
+6 -1
View File
@@ -3,7 +3,7 @@ from django.contrib import admin
# Register your models here. # Register your models here.
from .models import ( from .models import (
PESCompound, PESCompound,
PESStructure PESStructure, NewsEntry
) )
@@ -15,5 +15,10 @@ class PESStructureAdmin(admin.ModelAdmin):
pass pass
class NewsEntryAdmin(admin.ModelAdmin):
pass
admin.site.register(PESCompound, PESCompoundAdmin) admin.site.register(PESCompound, PESCompoundAdmin)
admin.site.register(PESStructure, PESStructureAdmin) admin.site.register(PESStructure, PESStructureAdmin)
admin.site.register(NewsEntry, NewsEntryAdmin)
+25
View File
@@ -0,0 +1,25 @@
"""
Context processors for enviPy application.
Context processors automatically make variables available to all templates.
"""
from django.utils import timezone
from .models import NewsEntry
def news_entries_context(request):
"""
Provides package data for the search modal which is included globally
in framework_modern.html.
Returns:
dict: Context dictionary with reviewed and unreviewed packages
"""
qs = NewsEntry.objects.filter(publication_date__lte=timezone.now()).order_by('-publication_date')[:3]
return {
"news_entries": qs,
}
+29
View File
@@ -0,0 +1,29 @@
# Generated by Django 6.0.3 on 2026-10-16 07:50
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bayer', '0004_package_shareable'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='NewsEntry',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
('title', models.TextField(verbose_name='Title')),
('publication_date', models.DateTimeField(verbose_name='Date')),
('content', models.TextField(verbose_name='Content')),
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
]
@@ -0,0 +1,17 @@
# Generated by Django 6.0.3 on 2026-10-16 07:59
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('bayer', '0005_newsentry'),
]
operations = [
migrations.RemoveField(
model_name='newsentry',
name='url',
),
]
+23 -2
View File
@@ -1,10 +1,12 @@
from typing import List from typing import List
from uuid import uuid4
import urllib.parse import urllib.parse
import nh3 import nh3
from django.conf import settings as s from django.conf import settings as s
from django.db import models, transaction from django.db import models, transaction
from django.db.models import QuerySet from django.db.models import QuerySet
from django.urls import reverse from django.urls import reverse
from django.utils import timezone
from epdb.models import ( from epdb.models import (
EnviPathModel, EnviPathModel,
@@ -27,7 +29,7 @@ class Package(EnviPathModel):
class Classification(models.IntegerChoices): class Classification(models.IntegerChoices):
INTERNAL = 0, "Internal" INTERNAL = 0, "Internal"
RESTRICTED = 10 , "Restricted" RESTRICTED = 10, "Restricted"
SECRET = 20, "Secret" SECRET = 20, "Secret"
classification_level = models.IntegerField( classification_level = models.IntegerField(
@@ -161,7 +163,6 @@ class PESCompound(Compound):
normalized_structure=True, normalized_structure=True,
) )
cs = PESStructure.create( cs = PESStructure.create(
c, c,
pes_url, pes_url,
@@ -236,3 +237,23 @@ class PESStructure(CompoundStructure):
"image": f"{reverse('depict_pes')}?pesLink={urllib.parse.quote(self.pes_link)}", "image": f"{reverse('depict_pes')}?pesLink={urllib.parse.quote(self.pes_link)}",
"image_type": "png", "image_type": "png",
} }
class NewsEntry(models.Model):
uuid = models.UUIDField(
null=False, blank=False, verbose_name="UUID of this object", unique=True, default=uuid4
)
title = models.TextField(verbose_name="Title")
publication_date = models.DateTimeField(verbose_name="Date", default=timezone.now)
content = models.TextField(verbose_name="Content")
author = models.ForeignKey("epdb.User", on_delete=models.SET_NULL, blank=True, null=True)
@property
def url(self):
return f"{s.SERVER_URL}/news/{self.uuid}"
def save(self, *args, **kwargs):
if not self.url:
self.url = self._url()
super().save(*args, **kwargs)
+47
View File
@@ -0,0 +1,47 @@
{% extends "framework_modern.html" %}
{% block content %}
<div class="mx-auto max-w-4xl px-4 py-8">
{# Header / Title row #}
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-bold">{{ news_entry.title }}</h1>
</div>
{# Meta info card #}
<div class="card bg-base-100 shadow mb-6">
<div class="card-body">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<span class="text-sm font-semibold text-base-content/60">Published</span>
<p class="mt-1">{{ news_entry.publication_date|date:"N j, Y, P" }}</p>
</div>
<div>
<span class="text-sm font-semibold text-base-content/60">Author</span>
<p class="mt-1">
{% if news_entry.author %}
{{ news_entry.author.get_full_name|default:news_entry.author.username }}
{% else %}
<span class="text-base-content/40 italic">Unknown</span>
{% endif %}
</p>
</div>
</div>
</div>
</div>
{# Content card #}
<div class="card bg-base-100 shadow">
<div class="card-body prose max-w-none">
{{ news_entry.content|safe }}
</div>
</div>
{# Back link #}
<div class="mt-6">
<a href="javascript:history.back()" class="btn btn-ghost btn-sm">
← Back
</a>
</div>
</div>
{% endblock content %}
+1
View File
@@ -16,4 +16,5 @@ urlpatterns = [
v.create_pes_node, v.create_pes_node,
name="create pes node", name="create pes node",
), ),
re_path(rf"^news/(?P<news_entry_uuid>{UUID})$", v.news_detail, name="news detail"),
] ]
+13 -3
View File
@@ -4,12 +4,12 @@ import logging
import requests import requests
from django.conf import settings as s from django.conf import settings as s
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed
from django.shortcuts import redirect from django.shortcuts import redirect, render
from bayer.models import PESCompound from bayer.models import PESCompound, NewsEntry
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import Pathway, Node, Group from epdb.models import Pathway, Node, Group
from epdb.views import _anonymous_or_real, error from epdb.views import _anonymous_or_real, get_base_context, error
from utilities.decorators import package_permission_required from utilities.decorators import package_permission_required
Package = s.GET_PACKAGE_MODEL() Package = s.GET_PACKAGE_MODEL()
@@ -270,3 +270,13 @@ def visualize_pes(request):
if rep.get('type') == 'color': if rep.get('type') == 'color':
image_data = base64.b64decode(rep.get('base64').replace("data:image/png;base64,", "")) image_data = base64.b64decode(rep.get('base64').replace("data:image/png;base64,", ""))
return HttpResponse(image_data, content_type="image/png") return HttpResponse(image_data, content_type="image/png")
@package_permission_required()
def news_detail(request, news_entry_uuid):
current_user = _anonymous_or_real(request)
context = get_base_context(request, current_user)
context["entry"] = NewsEntry.objects.get(uuid=news_entry_uuid)
return render(request, "objects/news_entry.html", context)
+1
View File
@@ -114,6 +114,7 @@ TEMPLATES = [
"django.contrib.auth.context_processors.auth", "django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
"epdb.context_processors.package_context", "epdb.context_processors.package_context",
"bayer.context_processors.news_entries_context"
], ],
}, },
}, },
+2
View File
@@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional
import jwt import jwt
import nh3 import nh3
import requests import requests
from django.db import transaction
from django.conf import settings as s from django.conf import settings as s
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.cache import cache from django.core.cache import cache
@@ -2495,6 +2496,7 @@ class RegisterApplicationInSchema(Schema):
@router.post("/integration", response={200: Any, 403: Error}) @router.post("/integration", response={200: Any, 403: Error})
@transaction.atomic
def register_app(request, app: Form[RegisterApplicationInSchema]): def register_app(request, app: Form[RegisterApplicationInSchema]):
try: try:
user = request.user user = request.user
+31 -9
View File
@@ -260,15 +260,40 @@
</div> </div>
<!-- Community News Section --> <!-- Community News Section -->
<section class="bg-base-200 z-10 mx-8 py-16"> <section class="bg-base-200 z-10 mx-8 py-6">
<div class="mx-auto max-w-7xl px-4"> <div class="mx-auto max-w-7xl px-4">
<h2 class="h2 mb-8 text-left font-bold">Community Updates</h2> <h2 class="h2 mb-8 text-left font-bold">News</h2>
<div id="community-news-container" class="flex justify-center gap-4"> <div id="services-container" class="flex justify-center gap-4">
<!-- News cards will be populated here --> {% for ne in news_entries %}
<div id="loading" class="flex w-full justify-center"> <div class="card-body flex flex-col h-full justify-between p-5">
<span class="loading loading-spinner loading-lg"></span> <div>
<div class="flex items-center gap-2 mb-2">
<div class="bg-primary/10 text-primary rounded-lg p-1.5">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
</div> </div>
<h3 class="card-title text-base font-semibold tracking-tight">
{{ ne.title }} - {{ ne.publication_date|date }}
</h3>
</div>
<p class="text-sm text-base-content/60 leading-snug line-clamp-4">
{{ ne.content }}
</p>
</div>
<a href="{{ ne.url }}" class="btn btn-primary btn-sm w-full group-hover:btn-active mt-1">
Read Full
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</a>
</div>
{% endfor %}
</div> </div>
<div class="mt-6 text-right"> <div class="mt-6 text-right">
@@ -280,9 +305,6 @@
Read More Announcements Read More Announcements
</a> </a>
</div> </div>
<!-- Discourse API integration -->
<script src="{% static 'js/discourse-api.js' %}"></script>
</div> </div>
</section> </section>