Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 33 additions & 0 deletions config/graphql/mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,36 @@ class Arguments:
id = graphene.String(required=True)


class AcceptCookieConsent(graphene.Mutation):
"""
Mutation to record when an authenticated user accepts cookie consent.
For anonymous users, this is handled via localStorage in the frontend.
"""

class Arguments:
pass

ok = graphene.Boolean()
message = graphene.String()

@login_required
def mutate(root, info):
try:
user = info.context.user
user.cookie_consent_accepted = True
user.cookie_consent_date = timezone.now()
user.save(update_fields=["cookie_consent_accepted", "cookie_consent_date"])

return AcceptCookieConsent(
ok=True, message="Cookie consent recorded successfully"
)
except Exception as e:
logger.error(f"Error recording cookie consent: {e}")
return AcceptCookieConsent(
ok=False, message=f"Failed to record cookie consent: {str(e)}"
)


class AddDocumentsToCorpus(graphene.Mutation):
class Arguments:
corpus_id = graphene.String(
Expand Down Expand Up @@ -3701,6 +3731,9 @@ class Mutation(graphene.ObjectType):
export_corpus = StartCorpusExport.Field() # Limited by user.is_usage_capped
delete_export = DeleteExport.Field()

# USER PREFERENCE MUTATIONS #################################################
accept_cookie_consent = AcceptCookieConsent.Field()

# ANALYSIS MUTATIONS #########################################################
start_analysis_on_doc = StartDocumentAnalysisMutation.Field()
delete_analysis = DeleteAnalysisMutation.Field()
Expand Down
55 changes: 50 additions & 5 deletions frontend/src/components/cookies/CookieConsent.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,55 @@
import { List, Modal, Header, Icon, Button } from "semantic-ui-react";
import { useMutation, useReactiveVar } from "@apollo/client";
import { toast } from "react-toastify";

import inverted_cookie_icon from "../../assets/icons/noun-cookie-2123093-FFFFFF.png";
import { showCookieAcceptModal } from "../../graphql/cache";
import { showCookieAcceptModal, authToken } from "../../graphql/cache";
import {
ACCEPT_COOKIE_CONSENT,
AcceptCookieConsentInputs,
AcceptCookieConsentOutputs,
} from "../../graphql/mutations";

export const CookieConsentDialog = () => {
const auth_token = useReactiveVar(authToken);
const isAuthenticated = Boolean(auth_token);

const [acceptCookieConsent, { loading }] = useMutation<
AcceptCookieConsentOutputs,
AcceptCookieConsentInputs
>(ACCEPT_COOKIE_CONSENT, {
onCompleted: (data) => {
if (data.acceptCookieConsent.ok) {
toast.success("Cookie consent recorded");
showCookieAcceptModal(false);
} else {
toast.error(
`Failed to record consent: ${data.acceptCookieConsent.message}`
);
// Still close the modal and set localStorage as fallback
localStorage.setItem("oc_cookieAccepted", "true");
showCookieAcceptModal(false);
}
},
onError: (error) => {
toast.error(`Error recording consent: ${error.message}`);
// Still close the modal and set localStorage as fallback
localStorage.setItem("oc_cookieAccepted", "true");
showCookieAcceptModal(false);
},
});

const handleAccept = () => {
if (isAuthenticated) {
// For authenticated users, call the mutation
acceptCookieConsent();
} else {
// For anonymous users, use localStorage only
localStorage.setItem("oc_cookieAccepted", "true");
showCookieAcceptModal(false);
}
};

return (
<Modal basic size="small" open>
<Header icon>
Expand Down Expand Up @@ -91,10 +137,9 @@ export const CookieConsentDialog = () => {
<Button
color="green"
inverted
onClick={() => {
localStorage.setItem("oc_cookieAccepted", "true");
showCookieAcceptModal(false);
}}
loading={loading}
disabled={loading}
onClick={handleAccept}
>
<Icon name="checkmark" /> Accept
</Button>
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/graphql/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,24 @@ export const START_EXPORT_CORPUS = gql`
}
`;

export interface AcceptCookieConsentInputs {}

export interface AcceptCookieConsentOutputs {
acceptCookieConsent: {
ok?: boolean;
message?: string;
};
}

export const ACCEPT_COOKIE_CONSENT = gql`
mutation {
acceptCookieConsent {
ok
message
}
}
`;

export interface StartImportCorpusInputs {
base64FileString: string;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 4.2.23 on 2025-10-19 15:19

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("users", "0017_alter_assignment_options_alter_userexport_options_and_more"),
]

operations = [
migrations.AddField(
model_name="user",
name="cookie_consent_accepted",
field=models.BooleanField(
default=False,
help_text="Whether the user has accepted cookie consent",
verbose_name="Cookie Consent Accepted",
),
),
migrations.AddField(
model_name="user",
name="cookie_consent_date",
field=models.DateTimeField(
blank=True,
help_text="When the user accepted cookie consent",
null=True,
verbose_name="Cookie Consent Date",
),
),
]
13 changes: 13 additions & 0 deletions opencontractserver/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,19 @@ class User(AbstractUser):
),
)

# Cookie consent tracking
cookie_consent_accepted = django.db.models.BooleanField(
"Cookie Consent Accepted",
default=False,
help_text="Whether the user has accepted cookie consent",
)
cookie_consent_date = django.db.models.DateTimeField(
"Cookie Consent Date",
blank=True,
null=True,
help_text="When the user accepted cookie consent",
)

def __str__(self):
return f"{self.username}: {self.email}"

Expand Down
Loading