forked from enviPath/enviPy
Compare commits
6
Commits
8dc0e5e7a3
...
feat/bb4g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
493990566a | ||
|
|
7a6d8d7aec | ||
|
|
2e2abf0287 | ||
|
|
86c9ac887e | ||
|
|
5bbe316c24 | ||
|
|
d63c40e041 |
+6
-1
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
@@ -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(
|
||||
@@ -161,7 +163,6 @@ class PESCompound(Compound):
|
||||
normalized_structure=True,
|
||||
)
|
||||
|
||||
|
||||
cs = PESStructure.create(
|
||||
c,
|
||||
pes_url,
|
||||
@@ -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)
|
||||
|
||||
@@ -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 %}
|
||||
@@ -16,4 +16,5 @@ urlpatterns = [
|
||||
v.create_pes_node,
|
||||
name="create pes node",
|
||||
),
|
||||
re_path(rf"^news/(?P<news_entry_uuid>{UUID})$", v.news_detail, name="news detail"),
|
||||
]
|
||||
|
||||
+27
-5
@@ -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()
|
||||
@@ -18,7 +18,13 @@ Package = s.GET_PACKAGE_MODEL()
|
||||
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.
|
||||
|
||||
@@ -32,6 +38,9 @@ def has_secret_group(user):
|
||||
bool: True if the user belongs to at least one secret group,
|
||||
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()
|
||||
|
||||
|
||||
@@ -196,6 +205,10 @@ def get_application_token(prod: bool) -> str:
|
||||
|
||||
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():
|
||||
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
|
||||
if not has_secret_group(user):
|
||||
if not has_secret_pes_allowance(user):
|
||||
headers["app-classification-level-restriction"] = "restrict-pes-secret-structure-access"
|
||||
|
||||
|
||||
@@ -257,3 +270,12 @@ 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")
|
||||
|
||||
|
||||
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
@@ -29,6 +29,8 @@ class SamplingAlgorithm(enum.Enum):
|
||||
class BB4GConfig(EnviPyModel):
|
||||
sampling_algorithm: SamplingAlgorithm = SamplingAlgorithm.EXACT
|
||||
cutoff: int = -5
|
||||
model: str = "rat"
|
||||
mode: str | None = None
|
||||
|
||||
class UI:
|
||||
title = "BB4G Configuration"
|
||||
@@ -44,6 +46,18 @@ class BB4GConfig(EnviPyModel):
|
||||
order=2,
|
||||
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
|
||||
@@ -127,7 +141,7 @@ class BB4G(Classifier):
|
||||
def run(self, eP: EnviPyDTO, *args, **kwargs) -> RunResult:
|
||||
|
||||
# Ensure Service is running
|
||||
self.start()
|
||||
# self.start()
|
||||
|
||||
smiles = [c.smiles for c in eP.get_compounds()]
|
||||
preds = self._post(smiles)
|
||||
@@ -167,6 +181,8 @@ class BB4G(Classifier):
|
||||
"smiles": smi,
|
||||
"sampling_alg": self.config.sampling_algorithm.value,
|
||||
"cutoff": self.config.cutoff,
|
||||
"model": self.config.model,
|
||||
"mode": self.config.mode,
|
||||
}
|
||||
|
||||
retries = 0
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.contrib import messages
|
||||
|
||||
from .models import (
|
||||
AdditionalInformation,
|
||||
AppDetails,
|
||||
ClassifierPluginModel,
|
||||
Compound,
|
||||
CompoundStructure,
|
||||
@@ -40,6 +41,10 @@ class AdditionalInformationAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class AppDetailsAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"username",
|
||||
@@ -282,3 +287,4 @@ admin.site.register(Setting, SettingAdmin)
|
||||
admin.site.register(Scenario, ScenarioAdmin)
|
||||
admin.site.register(ExternalDatabase, ExternalDatabaseAdmin)
|
||||
admin.site.register(ExternalIdentifier, ExternalIdentifierAdmin)
|
||||
admin.site.register(AppDetails, AppDetailsAdmin)
|
||||
|
||||
+90
-3
@@ -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
|
||||
@@ -43,7 +44,7 @@ from .models import (
|
||||
Scenario,
|
||||
SimpleAmbitRule,
|
||||
User,
|
||||
UserPackagePermission,
|
||||
UserPackagePermission, AppDetails,
|
||||
)
|
||||
from .views import delete_with_log, get_remote_address
|
||||
|
||||
@@ -122,7 +123,6 @@ def validate_token(token: str) -> dict:
|
||||
class MSBearerTokenAuth(HttpBearer):
|
||||
|
||||
def authenticate(self, request, token):
|
||||
|
||||
auth_log.info(f"Authentication request by {get_remote_address(request)}")
|
||||
|
||||
if token is None:
|
||||
@@ -130,6 +130,11 @@ class MSBearerTokenAuth(HttpBearer):
|
||||
|
||||
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():
|
||||
auth_log.info(f"Authentication request by {get_remote_address(request)} failed!")
|
||||
return None
|
||||
@@ -564,7 +569,7 @@ class UpdatePackage(Schema):
|
||||
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]):
|
||||
try:
|
||||
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)
|
||||
else:
|
||||
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)
|
||||
return HttpResponse(status=200)
|
||||
@@ -598,6 +606,9 @@ def update_package(request, package_uuid, pack: Form[UpdatePackage]):
|
||||
grantee = GroupManager.get_group_lp(pack.ppsURI)
|
||||
else:
|
||||
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)
|
||||
return HttpResponse(status=200)
|
||||
@@ -2473,3 +2484,79 @@ def export(request, q: Query[PackageExportInSchema]):
|
||||
return 403, {
|
||||
"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}"
|
||||
}
|
||||
@@ -220,11 +220,14 @@ class UserManager(object):
|
||||
u.save()
|
||||
|
||||
if add_to_group:
|
||||
if Group.objects.filter(public=True, name="enviPath Users").exists():
|
||||
g = Group.objects.get(public=True, name="enviPath Users")
|
||||
g.user_member.add(u)
|
||||
g.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
|
||||
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -84,6 +84,17 @@ class User(AbstractUser):
|
||||
USERNAME_FIELD = "email"
|
||||
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):
|
||||
return self.username
|
||||
|
||||
@@ -103,6 +114,25 @@ class User(AbstractUser):
|
||||
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):
|
||||
"""
|
||||
API authentication token for users.
|
||||
|
||||
@@ -1294,6 +1294,9 @@ def package(request, package_uuid):
|
||||
)
|
||||
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)
|
||||
|
||||
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]):
|
||||
if "user" in 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:
|
||||
grantee = GroupManager.get_group_lp(grantee_url)
|
||||
|
||||
|
||||
@@ -260,15 +260,31 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<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">
|
||||
<!-- News cards will be populated here -->
|
||||
<div id="loading" class="flex w-full justify-center">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
<div id="services-container" class="flex justify-center gap-4">
|
||||
{% for ne in news_entries %}
|
||||
<div class="card-body flex flex-col h-full justify-between p-5">
|
||||
<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 class="mt-6 text-right">
|
||||
@@ -280,9 +296,6 @@
|
||||
Read More Announcements
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Discourse API integration -->
|
||||
<script src="{% static 'js/discourse-api.js' %}"></script>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user