Compare commits

..
6 Commits
Author SHA1 Message Date
Tim Lorsbach 493990566a adjusted bb4g for dev 2026-10-21 12:00:02 +02:00
Tim Lorsbach 7a6d8d7aec ... 2026-10-21 11:59:53 +02:00
Tim Lorsbach 2e2abf0287 ...
API CI / api-tests (pull_request) Failing after 32s
CI / test (pull_request) Failing after 33s
2026-10-21 11:59:11 +02:00
Tim Lorsbach 86c9ac887e Added news entries
API CI / api-tests (pull_request) Failing after 32s
CI / test (pull_request) Failing after 33s
2026-10-21 09:53:58 +02:00
Tim Lorsbach 5bbe316c24 ...
API CI / api-tests (pull_request) Failing after 0s
CI / test (pull_request) Failing after 35s
2026-10-07 15:00:19 +02:00
Tim Lorsbach d63c40e041 Machine users 2026-10-07 14:59:57 +02:00
22 changed files with 494 additions and 35 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',
),
]
+31 -10
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(
@@ -109,7 +111,7 @@ class PESCompound(Compound):
@staticmethod @staticmethod
@transaction.atomic @transaction.atomic
def create( 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": ) -> "Compound":
pes_url = pes_data["pes_url"] pes_url = pes_data["pes_url"]
@@ -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,
@@ -218,13 +219,13 @@ class PESStructure(CompoundStructure):
@transaction.atomic @transaction.atomic
def add_structure( def add_structure(
self, self,
smiles: str, smiles: str,
name: str = None, name: str = None,
description: str = None, description: str = None,
default_structure: bool = False, default_structure: bool = False,
*args, *args,
**kwargs, **kwargs,
) -> "CompoundStructure": ) -> "CompoundStructure":
raise ValueError("Not supported!") raise ValueError("Not supported!")
@@ -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"),
] ]
+27 -5
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()
@@ -18,7 +18,13 @@ Package = s.GET_PACKAGE_MODEL()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def has_secret_group(user): def has_pes_allowance(user):
if user.principal_type == user.PrincipalType.APP:
return user.app_details.pes_allowance != user.app_details.PESAllowance.NONE
return True
def has_secret_pes_allowance(user):
""" """
Determines if the specified user belongs to any secret group. Determines if the specified user belongs to any secret group.
@@ -32,6 +38,9 @@ def has_secret_group(user):
bool: True if the user belongs to at least one secret group, bool: True if the user belongs to at least one secret group,
False otherwise. False otherwise.
""" """
if user.principal_type == user.PrincipalType.APP:
return user.app_details.pes_allowance == user.app_details.PESAllowance.SECRET
return Group.objects.filter(secret=True, user_member=user).exists() return Group.objects.filter(secret=True, user_member=user).exists()
@@ -196,6 +205,10 @@ def get_application_token(prod: bool) -> str:
def fetch_pes(request, pes_url, user) -> dict: def fetch_pes(request, pes_url, user) -> dict:
if not has_pes_allowance(user):
logger.info(f"User {user.username} does not have permission to fetch PESs")
raise ValueError(f"User {user.username} does not have permission to fetch PESs")
for k, v in s.PES_API_MAPPING.items(): for k, v in s.PES_API_MAPPING.items():
if pes_url.startswith(k): if pes_url.startswith(k):
@@ -215,7 +228,7 @@ def fetch_pes(request, pes_url, user) -> dict:
} }
# Restrict request if user is not part of any secret group # Restrict request if user is not part of any secret group
if not has_secret_group(user): if not has_secret_pes_allowance(user):
headers["app-classification-level-restriction"] = "restrict-pes-secret-structure-access" headers["app-classification-level-restriction"] = "restrict-pes-secret-structure-access"
@@ -257,3 +270,12 @@ 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")
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)
+17 -1
View File
@@ -29,6 +29,8 @@ class SamplingAlgorithm(enum.Enum):
class BB4GConfig(EnviPyModel): class BB4GConfig(EnviPyModel):
sampling_algorithm: SamplingAlgorithm = SamplingAlgorithm.EXACT sampling_algorithm: SamplingAlgorithm = SamplingAlgorithm.EXACT
cutoff: int = -5 cutoff: int = -5
model: str = "rat"
mode: str | None = None
class UI: class UI:
title = "BB4G Configuration" title = "BB4G Configuration"
@@ -44,6 +46,18 @@ class BB4GConfig(EnviPyModel):
order=2, order=2,
placeholder="If unset defaults to -5" placeholder="If unset defaults to -5"
) )
model = UIConfig(
widget=WidgetType.TEXT,
label="BB4G Model",
order=3,
placeholder="If unset defaults to rat"
)
mode = UIConfig(
widget=WidgetType.TEXT,
label="BB4G Mode",
order=4,
placeholder="If unset defaults to None"
)
# Once stable these will be exposed by enviPy-plugins lib # Once stable these will be exposed by enviPy-plugins lib
@@ -127,7 +141,7 @@ class BB4G(Classifier):
def run(self, eP: EnviPyDTO, *args, **kwargs) -> RunResult: def run(self, eP: EnviPyDTO, *args, **kwargs) -> RunResult:
# Ensure Service is running # Ensure Service is running
self.start() # self.start()
smiles = [c.smiles for c in eP.get_compounds()] smiles = [c.smiles for c in eP.get_compounds()]
preds = self._post(smiles) preds = self._post(smiles)
@@ -167,6 +181,8 @@ class BB4G(Classifier):
"smiles": smi, "smiles": smi,
"sampling_alg": self.config.sampling_algorithm.value, "sampling_alg": self.config.sampling_algorithm.value,
"cutoff": self.config.cutoff, "cutoff": self.config.cutoff,
"model": self.config.model,
"mode": self.config.mode,
} }
retries = 0 retries = 0
+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"
], ],
}, },
}, },
+6
View File
@@ -6,6 +6,7 @@ from django.contrib import messages
from .models import ( from .models import (
AdditionalInformation, AdditionalInformation,
AppDetails,
ClassifierPluginModel, ClassifierPluginModel,
Compound, Compound,
CompoundStructure, CompoundStructure,
@@ -40,6 +41,10 @@ class AdditionalInformationAdmin(admin.ModelAdmin):
pass pass
class AppDetailsAdmin(admin.ModelAdmin):
pass
class UserAdmin(admin.ModelAdmin): class UserAdmin(admin.ModelAdmin):
list_display = [ list_display = [
"username", "username",
@@ -282,3 +287,4 @@ admin.site.register(Setting, SettingAdmin)
admin.site.register(Scenario, ScenarioAdmin) admin.site.register(Scenario, ScenarioAdmin)
admin.site.register(ExternalDatabase, ExternalDatabaseAdmin) admin.site.register(ExternalDatabase, ExternalDatabaseAdmin)
admin.site.register(ExternalIdentifier, ExternalIdentifierAdmin) admin.site.register(ExternalIdentifier, ExternalIdentifierAdmin)
admin.site.register(AppDetails, AppDetailsAdmin)
+90 -3
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
@@ -43,7 +44,7 @@ from .models import (
Scenario, Scenario,
SimpleAmbitRule, SimpleAmbitRule,
User, User,
UserPackagePermission, UserPackagePermission, AppDetails,
) )
from .views import delete_with_log, get_remote_address from .views import delete_with_log, get_remote_address
@@ -122,7 +123,6 @@ def validate_token(token: str) -> dict:
class MSBearerTokenAuth(HttpBearer): class MSBearerTokenAuth(HttpBearer):
def authenticate(self, request, token): def authenticate(self, request, token):
auth_log.info(f"Authentication request by {get_remote_address(request)}") auth_log.info(f"Authentication request by {get_remote_address(request)}")
if token is None: if token is None:
@@ -130,6 +130,11 @@ class MSBearerTokenAuth(HttpBearer):
claims = validate_token(token) claims = validate_token(token)
if claims.get("roles") is None or claims.get("roles") == [] or "envipath_registered_user" not in claims.get(
"roles"):
auth_log.error(f"API authentication attempt by {get_remote_address(request)} failed due to missing role")
return None
if not User.objects.filter(uuid=claims['oid']).exists(): if not User.objects.filter(uuid=claims['oid']).exists():
auth_log.info(f"Authentication request by {get_remote_address(request)} failed!") auth_log.info(f"Authentication request by {get_remote_address(request)} failed!")
return None return None
@@ -564,7 +569,7 @@ class UpdatePackage(Schema):
write: str | None = None write: str | None = None
@router.post("/package/{uuid:package_uuid}", response={200: PackageSchema | Any, 400: Error}) @router.post("/package/{uuid:package_uuid}", response={200: PackageSchema | Any, 400: Error, 403: Error})
def update_package(request, package_uuid, pack: Form[UpdatePackage]): def update_package(request, package_uuid, pack: Form[UpdatePackage]):
try: try:
p = get_package_for_write(request.user, package_uuid) p = get_package_for_write(request.user, package_uuid)
@@ -590,6 +595,9 @@ def update_package(request, package_uuid, pack: Form[UpdatePackage]):
grantee = GroupManager.get_group_lp(pack.ppsURI) grantee = GroupManager.get_group_lp(pack.ppsURI)
else: else:
grantee = UserManager.get_user_lp(pack.ppsURI) grantee = UserManager.get_user_lp(pack.ppsURI)
if grantee.principal_type == request.user.PrincipalType.APP:
if not request.user.is_superuser:
return 403, {"message": "You are not allowed to grant permissions to an app!"}
PackageManager.grant_read(request.user, p, grantee) PackageManager.grant_read(request.user, p, grantee)
return HttpResponse(status=200) return HttpResponse(status=200)
@@ -598,6 +606,9 @@ def update_package(request, package_uuid, pack: Form[UpdatePackage]):
grantee = GroupManager.get_group_lp(pack.ppsURI) grantee = GroupManager.get_group_lp(pack.ppsURI)
else: else:
grantee = UserManager.get_user_lp(pack.ppsURI) grantee = UserManager.get_user_lp(pack.ppsURI)
if grantee.principal_type == request.user.PrincipalType.APP:
if not request.user.is_superuser:
return 403, {"message": "You are not allowed to grant permissions to an app!"}
PackageManager.grant_write(request.user, p, grantee) PackageManager.grant_write(request.user, p, grantee)
return HttpResponse(status=200) return HttpResponse(status=200)
@@ -2473,3 +2484,79 @@ def export(request, q: Query[PackageExportInSchema]):
return 403, { return 403, {
"message": f"Exporting Package with id {q.package_uuid} failed due to insufficient rights!" "message": f"Exporting Package with id {q.package_uuid} failed due to insufficient rights!"
} }
class RegisterApplicationInSchema(Schema):
oid: str
client_id: str
name: str
secret: bool = False
data_pool_uuid: str | None = None
pes: str | None
@router.post("/integration", response={200: Any, 403: Error})
@transaction.atomic
def register_app(request, app: Form[RegisterApplicationInSchema]):
try:
user = request.user
if user.is_superuser:
user_name = f"APP_{app.oid}"
user_email = f"{app.oid}@bayer.com"
u = UserManager.create_user(user_name, user_email, None, uuid=app.oid, is_active=True, add_to_group=False)
# Adjust objects
u.principal_type = User.PrincipalType.APP
if app.pes:
pes_choice = next(
choice for choice in AppDetails.PESAllowance
if choice.label.lower() == app.pes.lower()
)
if pes_choice is None:
pes_choice = AppDetails.PESAllowance.NONE
else:
pes_choice = AppDetails.PESAllowance.NONE
if app.data_pool_uuid is not None:
if not Group.objects.filter(uuid=app.data_pool_uuid, secret=True).exists():
return 400, {"message": f"Group with id {app.data_pool_uuid} does not exist!"}
data_pool = Group.objects.get(uuid=app.data_pool_uuid, secret=True)
else:
data_pool = None
app_details = AppDetails.objects.create(
owner=user,
client_id=app.client_id,
display_name=app.name,
secret_allowed=app.secret,
data_pool=data_pool,
pes_allowance=pes_choice,
)
u.app_details = app_details
u.save()
# Add the integration to the "All enviPath Users" Group
all_envipath_users = Group.objects.get(name="All enviPath Users")
if not all_envipath_users.user_member.contains(u):
all_envipath_users.user_member.add(u)
if app.secret:
if app.data_pool_uuid is None or app.data_pool_uuid.strip() == "":
return 400, {"message": "Secret is only allowed if data pool is specified!"}
default_package = u.default_package
default_package.data_pool = Group.objects.get(uuid=app.data_pool_uuid, secret=True)
default_package.classification_level = default_package.Classification.SECRET
default_package.save()
return 200, {"message": "Application registered successfully"}
else:
raise ValueError("Only superusers can register apps")
except ValueError as e:
return 403, {
"message": f"Registering application failed due to {e}"
}
+8 -5
View File
@@ -220,11 +220,14 @@ class UserManager(object):
u.save() u.save()
if add_to_group: if add_to_group:
g = Group.objects.get(public=True, name="enviPath Users") if Group.objects.filter(public=True, name="enviPath Users").exists():
g.user_member.add(u) g = Group.objects.get(public=True, name="enviPath Users")
g.save() g.user_member.add(u)
u.default_group = g g.save()
u.save() u.default_group = g
u.save()
else:
logger.info(f"add_to_group was set to True but no public group named 'enviPath Users' exists.")
return u return u
@@ -0,0 +1,34 @@
# Generated by Django 6.0.3 on 2026-10-07 08:08
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epdb', '0030_auto_20260814_0741'),
]
operations = [
migrations.AddField(
model_name='user',
name='principal_type',
field=models.IntegerField(choices=[(0, 'Internal'), (10, 'Restricted')], default=0),
),
migrations.CreateModel(
name='AppDetails',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('client_id', models.TextField(verbose_name='Client ID')),
('display_name', models.TextField(verbose_name='Display Name')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='user',
name='app_details',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.appdetails'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 6.0.3 on 2026-10-07 08:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epdb', '0031_user_principal_type_appdetails_user_app_details'),
]
operations = [
migrations.AlterField(
model_name='user',
name='principal_type',
field=models.IntegerField(choices=[(0, 'User'), (10, 'App')], default=0),
),
]
@@ -0,0 +1,17 @@
# Generated by Django 6.0.3 on 2026-10-07 08:57
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('epdb', '0032_alter_user_principal_type'),
]
operations = [
migrations.RemoveField(
model_name='user',
name='app_details',
),
]
+19
View File
@@ -0,0 +1,19 @@
# Generated by Django 6.0.3 on 2026-10-07 10:14
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epdb', '0033_remove_user_app_details'),
]
operations = [
migrations.AddField(
model_name='user',
name='app_details',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.appdetails'),
),
]
@@ -0,0 +1,24 @@
# Generated by Django 6.0.3 on 2026-10-07 10:26
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epdb', '0034_user_app_details'),
]
operations = [
migrations.AddField(
model_name='appdetails',
name='data_pool',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.group'),
),
migrations.AddField(
model_name='appdetails',
name='secret_allowed',
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 6.0.3 on 2026-10-07 10:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epdb', '0035_appdetails_data_pool_appdetails_secret_allowed'),
]
operations = [
migrations.AddField(
model_name='appdetails',
name='pes_allowance',
field=models.IntegerField(choices=[(0, 'None'), (10, 'Non Secret'), (20, 'Secret')], default=0),
),
]
+30
View File
@@ -84,6 +84,17 @@ class User(AbstractUser):
USERNAME_FIELD = "email" USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["username"] REQUIRED_FIELDS = ["username"]
class PrincipalType(models.IntegerChoices):
USER = 0, "User"
APP = 10 , "App"
principal_type = models.IntegerField(
choices=PrincipalType,
default=PrincipalType.USER,
)
app_details = models.OneToOneField("epdb.AppDetails", on_delete=models.CASCADE, null=True, blank=True)
def get_name(self): def get_name(self):
return self.username return self.username
@@ -103,6 +114,25 @@ class User(AbstractUser):
return self.default_setting return self.default_setting
class AppDetails(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE)
client_id = models.TextField(blank=False, null=False, verbose_name="Client ID")
display_name = models.TextField(blank=False, null=False, verbose_name="Display Name")
secret_allowed = models.BooleanField(default=False)
data_pool = models.ForeignKey("epdb.Group", null=True, blank=True, on_delete=models.SET_NULL)
class PESAllowance(models.IntegerChoices):
NONE = 0, "None"
NON_SECRET = 10 , "Non Secret"
SECRET = 20, "Secret"
pes_allowance = models.IntegerField(
choices=PESAllowance,
default=PESAllowance.NONE,
)
class APIToken(TimeStampedModel): class APIToken(TimeStampedModel):
""" """
API authentication token for users. API authentication token for users.
+6
View File
@@ -1294,6 +1294,9 @@ def package(request, package_uuid):
) )
users = users.filter(is_active=True).exclude(username="anonymous") users = users.filter(is_active=True).exclude(username="anonymous")
if not current_user.is_superuser:
users = users.exclude(principal_type=User.PrincipalType.APP)
group_perms = GroupPackagePermission.objects.filter(package=current_package) group_perms = GroupPackagePermission.objects.filter(package=current_package)
if current_package.classification_level == current_package.Classification.SECRET: if current_package.classification_level == current_package.Classification.SECRET:
@@ -1386,6 +1389,9 @@ def package(request, package_uuid):
elif any([grantee_url, read, write, owner]): elif any([grantee_url, read, write, owner]):
if "user" in grantee_url: if "user" in grantee_url:
grantee = UserManager.get_user_lp(grantee_url) grantee = UserManager.get_user_lp(grantee_url)
if grantee.principal_type == current_user.PrincipalType.APP:
if not request.user.is_superuser:
return error(request, "You are not allowed to grant permissions to an app!")
else: else:
grantee = GroupManager.get_group_lp(grantee_url) grantee = GroupManager.get_group_lp(grantee_url)
+23 -10
View File
@@ -260,15 +260,31 @@
</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> <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>
<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>
</div>
{% endfor %}
</div> </div>
<div class="mt-6 text-right"> <div class="mt-6 text-right">
@@ -280,9 +296,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>