Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -51,30 +51,30 @@ const DataRequestModal: FC<Props> = ({ isOpen, onClose, post }) => {
const { user } = useAuth();
const isLoggedOut = !user;

const [whitelistStatus, setWhitelistStatus] = useState({
is_whitelisted: false,
const [dataAccessStatus, setDataAccessStatus] = useState({
has_data_access: false,
view_deanonymized_data: false,
isLoaded: false,
});

useEffect(() => {
if (!isOpen || whitelistStatus.isLoaded) {
if (!isOpen || dataAccessStatus.isLoaded) {
return;
}
const fetchWhitelistStatus = async () => {
const fetchDataAccessStatus = async () => {
try {
const status = await ClientPostsApi.getWhitelistStatus({
const status = await ClientPostsApi.getDataAccessStatus({
post_id: post.id,
});
setWhitelistStatus({ ...status, isLoaded: true });
setDataAccessStatus({ ...status, isLoaded: true });
} catch (error) {
console.error("Error fetching whitelist status:", error);
console.error("Error fetching data access status:", error);
// Set as loaded even on error to avoid infinite retries
setWhitelistStatus((prev) => ({ ...prev, isLoaded: true }));
setDataAccessStatus((prev) => ({ ...prev, isLoaded: true }));
}
};
fetchWhitelistStatus();
}, [isOpen, whitelistStatus.isLoaded, post.id]);
fetchDataAccessStatus();
}, [isOpen, dataAccessStatus.isLoaded, post.id]);

const {
control,
Expand All @@ -93,23 +93,23 @@ const DataRequestModal: FC<Props> = ({ isOpen, onClose, post }) => {
include_user_data: true,
include_key_factors: false,
include_bots: undefined,
anonymized: !whitelistStatus.view_deanonymized_data,
anonymized: !dataAccessStatus.view_deanonymized_data,
},
});

const { minimize, include_bots, include_user_data } = watch();
const isDownloadDisabled = !minimize || !isNil(include_bots);

useEffect(() => {
if (whitelistStatus.isLoaded) {
if (dataAccessStatus.isLoaded) {
reset({
...watch(),
anonymized: !whitelistStatus.view_deanonymized_data,
anonymized: !dataAccessStatus.view_deanonymized_data,
});
}
}, [
whitelistStatus.isLoaded,
whitelistStatus.view_deanonymized_data,
dataAccessStatus.isLoaded,
dataAccessStatus.view_deanonymized_data,
watch,
reset,
]);
Expand Down Expand Up @@ -225,7 +225,7 @@ const DataRequestModal: FC<Props> = ({ isOpen, onClose, post }) => {
disabled={isLoggedOut}
/>
) : null}
{whitelistStatus.is_whitelisted && (
{dataAccessStatus.has_data_access && (
<>
{include_user_data ? (
<div className="flex flex-col gap-1">
Expand Down Expand Up @@ -256,7 +256,7 @@ const DataRequestModal: FC<Props> = ({ isOpen, onClose, post }) => {
)}
</div>
) : null}
{whitelistStatus.view_deanonymized_data && include_user_data ? (
{dataAccessStatus.view_deanonymized_data && include_user_data ? (
<CheckboxField
control={control}
name="anonymized"
Expand Down
10 changes: 5 additions & 5 deletions front_end/src/services/api/posts/posts.shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
PredictionFlowPost,
} from "@/types/post";
import { QuestionWithForecasts } from "@/types/question";
import { DataParams, Require, WhitelistStatus } from "@/types/utils";
import { DataAccessStatus, DataParams, Require } from "@/types/utils";
import { encodeQueryParams } from "@/utils/navigation";

export type PostsParams = PaginationParams & {
Expand Down Expand Up @@ -247,13 +247,13 @@ class PostsApi extends ApiService {
);
}

async getWhitelistStatus(params: {
async getDataAccessStatus(params: {
post_id?: number;
project_id?: number;
}): Promise<WhitelistStatus> {
}): Promise<DataAccessStatus> {
const queryParams = encodeQueryParams(params);
return await this.get<WhitelistStatus>(
`/get-whitelist-status/${queryParams}`
return await this.get<DataAccessStatus>(
`/get-data-access-status/${queryParams}`
);
}

Expand Down
4 changes: 2 additions & 2 deletions front_end/src/types/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type DataParams = {
include_key_factors?: boolean;
anonymized?: boolean;
};
export type WhitelistStatus = {
is_whitelisted: boolean;
export type DataAccessStatus = {
has_data_access: boolean;
view_deanonymized_data: boolean;
};
6 changes: 3 additions & 3 deletions misc/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.contrib import admin
from django.core.exceptions import ValidationError

from .models import Bulletin, SidebarItem, WhitelistUser
from .models import Bulletin, SidebarItem, UserDataAccess


@admin.register(Bulletin)
Expand Down Expand Up @@ -80,8 +80,8 @@ def content_type(self, obj: SidebarItem) -> str:
return ""


@admin.register(WhitelistUser)
class WhitelistUserAdmin(admin.ModelAdmin):
@admin.register(UserDataAccess)
class UserDataAccessAdmin(admin.ModelAdmin):
list_display = ("user", "created_at", "project", "post")
search_fields = ("user__username", "user__email", "project__name", "post__title")
autocomplete_fields = ("user", "project", "post")
172 changes: 172 additions & 0 deletions misc/migrations/0008_replace_whitelistuser_with_userdataaccess.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Generated by Django 5.1.15 on 2026-03-16 15:16

import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models


def copy_whitelistuser_to_userdataaccess(apps, schema_editor):
WhitelistUser = apps.get_model("misc", "WhitelistUser")
UserDataAccess = apps.get_model("misc", "UserDataAccess")
for entry in WhitelistUser.objects.all():
UserDataAccess.objects.create(
id=entry.id,
created_at=entry.created_at,
edited_at=entry.edited_at,
user=entry.user,
project=entry.project,
post=entry.post,
notes=entry.notes,
view_deanonymized_data=entry.view_deanonymized_data,
api_access_tier="restricted",
view_user_data=True,
)


def copy_userdataaccess_to_whitelistuser(apps, schema_editor):
UserDataAccess = apps.get_model("misc", "UserDataAccess")
WhitelistUser = apps.get_model("misc", "WhitelistUser")
for entry in UserDataAccess.objects.all():
WhitelistUser.objects.create(
id=entry.id,
created_at=entry.created_at,
edited_at=entry.edited_at,
user=entry.user,
project=entry.project,
post=entry.post,
notes=entry.notes,
view_deanonymized_data=entry.view_deanonymized_data,
)


class Migration(migrations.Migration):
dependencies = [
("misc", "0007_bulletin_post_bulletin_project"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="UserDataAccess",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
models.DateTimeField(
default=django.utils.timezone.now, editable=False
),
),
("edited_at", models.DateTimeField(editable=False, null=True)),
(
"api_access_tier",
models.CharField(
choices=[
("restricted", "Restricted"),
("benchmarking", "Benchmarking"),
("unrestricted", "Unrestricted"),
],
default="restricted",
help_text="Indicates the API access tier relevant to this data access entry.",
max_length=32,
),
),
(
"view_user_data",
models.BooleanField(
default=False,
help_text=(
"If True, the user can view user-level data (e.g., download datasets "
"with user-level information included). If False, the user can only access "
"aggregated data or anonymized user-level data."
),
),
),
(
"view_deanonymized_data",
models.BooleanField(
default=False,
help_text="If False, all downloaded data will be anonymized.",
),
),
(
"notes",
models.TextField(
blank=True,
help_text="Optional notes about the data access grant, e.g., reason for access. Please note any specific conditions.",
null=True,
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="data_accesses",
to=settings.AUTH_USER_MODEL,
),
),
(
"project",
models.ForeignKey(
blank=True,
help_text=(
"Optional. Scopes this entry to a specific project. "
"If neither project nor post is set while `view_user_data` is True, this entry "
"will apply globally with respect to viewing user data. "
"The API access tier will apply to this project if it exceeds the user's "
"base tier. If neither project nor post is set, the api_access_tier will be "
"taken from the User's base tier."
),
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="data_accesses",
to="projects.project",
),
),
(
"post",
models.ForeignKey(
blank=True,
help_text=(
"Optional. Scopes this entry to a specific post. "
"The API access tier will apply to this post if it exceeds the user's "
"base tier. If neither project nor post is set, the entry applies globally."
),
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="data_accesses",
to="posts.post",
),
),
],
options={
"abstract": False,
},
),
migrations.AlterUniqueTogether(
name="userdataaccess",
unique_together={("user", "project", "post")},
),
migrations.AddConstraint(
model_name="userdataaccess",
constraint=models.CheckConstraint(
condition=models.Q(project__isnull=True) | models.Q(post__isnull=True),
name="userdataaccess_project_or_post_not_both",
),
),
migrations.RunPython(
copy_whitelistuser_to_userdataaccess,
reverse_code=copy_userdataaccess_to_whitelistuser,
),
migrations.DeleteModel(
name="WhitelistUser",
),
]
Loading
Loading