diff --git a/bayer/admin.py b/bayer/admin.py index f0b257bb..9279fe7a 100644 --- a/bayer/admin.py +++ b/bayer/admin.py @@ -3,7 +3,7 @@ from django.contrib import admin # Register your models here. from .models import ( PESCompound, - PESStructure + PESStructure, NewsEntry ) @@ -15,5 +15,10 @@ class PESStructureAdmin(admin.ModelAdmin): pass +class NewsEntryAdmin(admin.ModelAdmin): + pass + + admin.site.register(PESCompound, PESCompoundAdmin) admin.site.register(PESStructure, PESStructureAdmin) +admin.site.register(NewsEntry, NewsEntryAdmin) diff --git a/bayer/context_processors.py b/bayer/context_processors.py new file mode 100644 index 00000000..530751b4 --- /dev/null +++ b/bayer/context_processors.py @@ -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, + } diff --git a/bayer/migrations/0005_newsentry.py b/bayer/migrations/0005_newsentry.py new file mode 100644 index 00000000..f5e4f777 --- /dev/null +++ b/bayer/migrations/0005_newsentry.py @@ -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)), + ], + ), + ] diff --git a/bayer/migrations/0006_remove_newsentry_url.py b/bayer/migrations/0006_remove_newsentry_url.py new file mode 100644 index 00000000..209b2b96 --- /dev/null +++ b/bayer/migrations/0006_remove_newsentry_url.py @@ -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', + ), + ] diff --git a/bayer/models.py b/bayer/models.py index 0e0d5b48..4d3b8c09 100644 --- a/bayer/models.py +++ b/bayer/models.py @@ -1,10 +1,12 @@ from typing import List +from uuid import uuid4 import urllib.parse import nh3 from django.conf import settings as s from django.db import models, transaction from django.db.models import QuerySet from django.urls import reverse +from django.utils import timezone from epdb.models import ( EnviPathModel, @@ -27,7 +29,7 @@ class Package(EnviPathModel): class Classification(models.IntegerChoices): INTERNAL = 0, "Internal" - RESTRICTED = 10 , "Restricted" + RESTRICTED = 10, "Restricted" SECRET = 20, "Secret" classification_level = models.IntegerField( @@ -109,7 +111,7 @@ class PESCompound(Compound): @staticmethod @transaction.atomic def create( - package: "Package", pes_data: dict, name: str = None, description: str = None, *args, **kwargs + package: "Package", pes_data: dict, name: str = None, description: str = None, *args, **kwargs ) -> "Compound": pes_url = pes_data["pes_url"] @@ -161,7 +163,6 @@ class PESCompound(Compound): normalized_structure=True, ) - cs = PESStructure.create( c, pes_url, @@ -218,13 +219,13 @@ class PESStructure(CompoundStructure): @transaction.atomic def add_structure( - self, - smiles: str, - name: str = None, - description: str = None, - default_structure: bool = False, - *args, - **kwargs, + self, + smiles: str, + name: str = None, + description: str = None, + default_structure: bool = False, + *args, + **kwargs, ) -> "CompoundStructure": raise ValueError("Not supported!") @@ -236,3 +237,23 @@ class PESStructure(CompoundStructure): "image": f"{reverse('depict_pes')}?pesLink={urllib.parse.quote(self.pes_link)}", "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) diff --git a/bayer/templates/objects/news_entry.html b/bayer/templates/objects/news_entry.html new file mode 100644 index 00000000..f838aff5 --- /dev/null +++ b/bayer/templates/objects/news_entry.html @@ -0,0 +1,47 @@ + +{% extends "framework_modern.html" %} + +{% block content %} +
+ {# Header / Title row #} +
+

{{ news_entry.title }}

+
+ + {# Meta info card #} +
+
+
+
+ Published +

{{ news_entry.publication_date|date:"N j, Y, P" }}

+
+
+ Author +

+ {% if news_entry.author %} + {{ news_entry.author.get_full_name|default:news_entry.author.username }} + {% else %} + Unknown + {% endif %} +

+
+
+
+
+ + {# Content card #} +
+
+ {{ news_entry.content|safe }} +
+
+ + {# Back link #} +
+ + ← Back + +
+
+{% endblock content %} \ No newline at end of file diff --git a/bayer/urls.py b/bayer/urls.py index 5f64cb86..6e9a5343 100644 --- a/bayer/urls.py +++ b/bayer/urls.py @@ -16,4 +16,5 @@ urlpatterns = [ v.create_pes_node, name="create pes node", ), + re_path(rf"^news/(?P{UUID})$", v.news_detail, name="news detail"), ] diff --git a/bayer/views.py b/bayer/views.py index 651731d7..2545f436 100644 --- a/bayer/views.py +++ b/bayer/views.py @@ -4,12 +4,12 @@ import logging import requests from django.conf import settings as s 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.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 Package = s.GET_PACKAGE_MODEL() @@ -270,3 +270,13 @@ def visualize_pes(request): if rep.get('type') == 'color': image_data = base64.b64decode(rep.get('base64').replace("data:image/png;base64,", "")) 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) \ No newline at end of file diff --git a/envipath/settings.py b/envipath/settings.py index f73913ef..e9b1af88 100644 --- a/envipath/settings.py +++ b/envipath/settings.py @@ -114,6 +114,7 @@ TEMPLATES = [ "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", "epdb.context_processors.package_context", + "bayer.context_processors.news_entries_context" ], }, }, diff --git a/epdb/legacy_api.py b/epdb/legacy_api.py index 09aee278..2f984548 100644 --- a/epdb/legacy_api.py +++ b/epdb/legacy_api.py @@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional import jwt import nh3 import requests +from django.db import transaction from django.conf import settings as s from django.contrib.auth import get_user_model from django.core.cache import cache @@ -2495,6 +2496,7 @@ class RegisterApplicationInSchema(Schema): @router.post("/integration", response={200: Any, 403: Error}) +@transaction.atomic def register_app(request, app: Form[RegisterApplicationInSchema]): try: user = request.user diff --git a/templates/index/index.html b/templates/index/index.html index 7e033567..0df2b502 100644 --- a/templates/index/index.html +++ b/templates/index/index.html @@ -260,15 +260,40 @@ -
+
-

Community Updates

+

News

-
- -
- -
+
+ {% for ne in news_entries %} +
+
+
+
+ + + + +
+

+ {{ ne.title }} - {{ ne.publication_date|date }} +

+
+

+ {{ ne.content }} +

+
+ + + Read Full + + + + + + +
+ {% endfor %}
@@ -280,9 +305,6 @@ Read More Announcements
- - -