Skip to content

Commit

Permalink
Merge pull request #699 from alee/datacite_doi_registration
Browse files Browse the repository at this point in the history
add support for DOI registration on published, peer reviewed models
  • Loading branch information
alee authored Dec 5, 2024
2 parents 1604bc6 + 6c2725b commit 9d96cfc
Show file tree
Hide file tree
Showing 39 changed files with 3,266 additions and 352 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ SECRETS_DIR=${BUILD_DIR}/secrets
DB_PASSWORD_PATH=${SECRETS_DIR}/db_password
PGPASS_PATH=${SECRETS_DIR}/.pgpass
SECRET_KEY_PATH=${SECRETS_DIR}/django_secret_key
EXT_SECRETS=hcaptcha_secret github_client_secret orcid_client_secret discourse_api_key discourse_sso_secret mail_api_key
EXT_SECRETS=hcaptcha_secret github_client_secret orcid_client_secret discourse_api_key discourse_sso_secret mail_api_key datacite_api_password
GENERATED_SECRETS=$(DB_PASSWORD_PATH) $(PGPASS_PATH) $(SECRET_KEY_PATH)

ENVREPLACE := deploy/scripts/envreplace
Expand Down
5 changes: 4 additions & 1 deletion base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ services:
- ./deploy/elasticsearch.conf.d/log4j2.properties:/usr/share/elasticsearch/config/log4j2.properties
- esdata:/usr/share/elasticsearch/data
db:
image: postgis/postgis:15-3.4
image: postgis/postgis:16-3.4
secrets:
- db_password
volumes:
Expand All @@ -60,6 +60,7 @@ services:
build: django
image: comses/server
secrets:
- datacite_api_password
- db_password
- discourse_api_key
- discourse_sso_secret
Expand All @@ -85,6 +86,8 @@ services:
- ./.env

secrets:
datacite_api_password:
file: ./build/secrets/datacite_api_password
db_password:
file: ./build/secrets/db_password
discourse_api_key:
Expand Down
4 changes: 4 additions & 0 deletions deploy/conf/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ SENTRY_DSN=
GITHUB_CLIENT_ID=
ORCID_CLIENT_ID=

# datacite api settings
DATACITE_API_USERNAME=
DATACITE_DRY_RUN="true" # allowed values: "true" or "false"

# test
TEST_USER_ID=10000000
TEST_USERNAME=__test_user__
Expand Down
2 changes: 1 addition & 1 deletion django/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ RUN --mount=type=cache,target=/var/lib/apt,sharing=locked \
unrar-free \
unzip \
&& update-alternatives --install /usr/bin/python python /usr/bin/python3 1000 \
&& apt-get upgrade -y -o Dpkg::Options::="--force-confold" \
&& apt-get upgrade -q -y -o Dpkg::Options::="--force-confold" \
&& mkdir -p /etc/service/django \
&& touch /etc/service/django/run /etc/postgresql-backup-pre \
&& chmod a+x /etc/service/django/run /etc/postgresql-backup-pre \
Expand Down
46 changes: 38 additions & 8 deletions django/core/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ def _to_search_terms(query_params):
@staticmethod
def _to_filter_display_terms(query_params):
"""
Convert query parameters into a list of displayable filter terms.
Convert query parameters into a list of displayable filter terms (replaces underscores withs spaces, etc)
Args:
query_params (QueryDict): The query parameters.
Returns:
list: A list of display filter terms.
list: A list of displayable filter terms.
"""
filters = []
for key, values in query_params.lists():
Expand All @@ -47,16 +47,40 @@ def _to_filter_display_terms(query_params):
filters.extend(values)
elif key in ["published_before", "published_after"]:
try:
date = parser.isoparse(values[0]).date()
filters.append(f"{key.replace('_', ' ')} {date.isoformat()}")
publication_date = parser.isoparse(values[0]).date()
filters.append(
f"{key.replace('_', ' ')} {publication_date.isoformat()}"
)
except ValueError:
# FIXME: this default behavior duplicates what we want to do in the else clause below
filters.extend(v.replace("_", " ") for v in values)
else:
filters.extend(v.replace("_", " ") for v in values)
return filters

@classmethod
def limit_page_range(cls, page=1, count=max_result_window, size=page_size):
def limit_page_range(cls, page=1, count=None, size=None):
"""
Limits the page range based on the maximum result window and page size.
This method ensures that the page number and result count do not exceed
the configured maximum result window for Elasticsearch. It also clamps
the page number to a valid range.
Args:
page (int): The current page number. Defaults to 1.
count (int): The total number of results. Defaults to max_result_window.
size (int): The number of results per page. Defaults to page_size.
Returns:
tuple: A tuple containing:
- limited_count (int): The total number of results to be shown in the current page, clamped to max_result_window.
- limited_page_number (int): The clamped page number within the valid range.
"""
if count is None:
count = cls.max_result_window
if size is None:
size = cls.page_size
try:
es_settings = getattr(settings, "WAGTAILSEARCH_BACKENDS", {})
max_result_window = es_settings["default"]["INDEX_SETTINGS"]["settings"][
Expand All @@ -73,13 +97,19 @@ def limit_page_range(cls, page=1, count=max_result_window, size=page_size):
# limit the result count to max_result_window
limited_count = min(count, max_result_window)

# Clamp page to range [1, max_page_number]
# Clamp page to range [1, max_page]
try:
max_page_number = -(-limited_count // size)
limited_page_number = min(max(1, int(page)), max_page_number)
max_page = (limited_count + size - 1) // size
requested_page = max(1, int(page))
limited_page_number = min(requested_page, max_page)
except ValueError:
limited_page_number = 1

logger.debug(
"Clamping count to %s and requested page to %s",
limited_count,
limited_page_number,
)
return limited_count, limited_page_number

def get_paginated_response(self, data):
Expand Down
13 changes: 13 additions & 0 deletions django/core/settings/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"""

import os
import warnings
from elasticsearch.exceptions import ElasticsearchWarning
from enum import Enum
from pathlib import Path

Expand Down Expand Up @@ -62,6 +64,10 @@ def is_test(self):
# base directory is one level above the project directory
BASE_DIR = os.path.dirname(PROJECT_DIR)

# default to datacite sandbox and test mode
DATACITE_PREFIX = "10.82853"
DATACITE_TEST_MODE = True

DEBUG = True

DJANGO_VITE_DEV_MODE = True
Expand Down Expand Up @@ -212,6 +218,9 @@ def is_test(self):

ROOT_URLCONF = "core.urls"

# tune down elasticsearch complaints
warnings.simplefilter("ignore", category=ElasticsearchWarning)

# configure elasticsearch 7 wagtail backend
WAGTAILSEARCH_BACKENDS = {
"default": {
Expand Down Expand Up @@ -502,6 +511,10 @@ def is_test(self):
TEST_USER_ID = os.getenv("TEST_USER_ID", 1000000)
TEST_USERNAME = os.getenv("TEST_USERNAME", "__test_user__")

DATACITE_API_USERNAME = os.getenv("DATACITE_API_USERNAME", "comses")
DATACITE_DRY_RUN = os.getenv("DATACITE_DRY_RUN", "true")
DATACITE_API_PASSWORD = read_secret("datacite_api_password")

SOCIALACCOUNT_PROVIDERS = {
# https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/about-scopes-for-oauth-apps/
"github": {
Expand Down
4 changes: 4 additions & 0 deletions django/core/settings/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
# remove staging specific apps like fixture_magic
INSTALLED_APPS.remove("fixture_magic")

# datacite configuration
DATACITE_PREFIX = "10.25937"
DATACITE_TEST_MODE = False

DEBUG = False
DJANGO_VITE_DEV_MODE = False
DEPLOY_ENVIRONMENT = Environment.PRODUCTION
Expand Down
4 changes: 4 additions & 0 deletions django/core/settings/staging.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
DJANGO_VITE_DEV_MODE = False
DEPLOY_ENVIRONMENT = Environment.STAGING

# datacite sandbox configuration inherited from defaults should suffice
# DATACITE_PREFIX = "10.82853"
# DATACITE_TEST_MODE = True

# configure sentry
sentry_sdk.init(
dsn=SENTRY_DSN,
Expand Down
3 changes: 2 additions & 1 deletion django/core/settings/test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from os import path

from .defaults import *

from os import path

DEPLOY_ENVIRONMENT = Environment.TEST

Expand Down
80 changes: 41 additions & 39 deletions django/core/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
import shlex
import shutil
import subprocess
from datetime import date, timedelta

from datetime import date, timedelta
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.utils import timezone

Expand All @@ -17,6 +17,8 @@

logger = logging.getLogger(__name__)

User = get_user_model()


class ContentModelFactory(ABC):
def __init__(self, submitter):
Expand Down Expand Up @@ -57,6 +59,9 @@ def create(self, **overrides):
content.save()
return content

def get_or_create(self, **kwargs):
return self.model.objects.get_or_create(**kwargs)

def create_unsaved(self, **overrides):
kwargs = self.get_default_data()
kwargs.update(overrides)
Expand Down Expand Up @@ -95,65 +100,62 @@ def get_default_data(self):
}


def make_user(
username="test_user",
password="default.testing.password",
email="comses.test@mailinator.com",
def create_test_user(
username="test_user", email="comses.test@mailinator.com", **kwargs
):
factory = UserFactory()
return factory.create(username=username, password=password, email=email), factory
factory = UserFactory(username=username, email=email, **kwargs)
return factory.create(), factory


class UserFactory:
def __init__(self, **defaults):
if not defaults.get("password"):
defaults["password"] = "test"
self.id = 0
self.password = defaults.get("password")
self.defaults = {}
username = defaults.get("username")
if username:
self.defaults.update({"username": username})
email = defaults.get("email")
if email:
self.defaults.update({"email": email})
self.next_user_id = 0
self.password = defaults.pop("password", "testing-password")
self.defaults = defaults.copy()

def extract_password(self, overrides):
if overrides.get("password"):
if "password" in overrides:
return overrides.pop("password")
else:
return self.password

def get_default_data(self):
def get_default_data(self, **kwargs):
defaults = self.defaults.copy()
defaults["username"] = defaults.get("username", "submitter{}".format(self.id))
self.id += 1
# always generate a unique username for the next user
username = defaults["username"] = defaults.get(
"username", f"submitter{self.next_user_id}"
)
defaults.setdefault("email", f"{username}@example.com")
defaults.setdefault("username", username)
self.next_user_id += 1
defaults.update(kwargs)
return defaults

def create(self, **overrides):
user = self.create_unsaved(**overrides)
def create(self, **kwargs):
return self.get_or_create(**kwargs)

def get_or_create(self, **kwargs):
password = self.extract_password(kwargs)
user_data_with_defaults = self.get_default_data(**kwargs)
"""
if "email" not in kwargs:
kwargs.update(email=default_data.get('email'))
if "username" not in kwargs:
kwargs.update(username=default_data.get("username"))
"""
user, created = User.objects.get_or_create(**user_data_with_defaults)
user.set_password(password)
user.save()
return user

def create_unsaved(self, **overrides):
password = self.extract_password(overrides)
kwargs = self.get_default_data()
kwargs.update(overrides)
if not kwargs.get("email"):
kwargs["email"] = "{}@gmail.com".format(kwargs["username"])
user = User(**kwargs)
if password:
user.set_password(password)
return user


class BaseModelTestCase(TestCase):
def setUp(self):
self.user = self.create_user()

def create_user(self, username="test_user", password="test", **kwargs):
kwargs.setdefault("email", "testuser@mailinator.com")
return User.objects.create_user(username=username, password=password, **kwargs)
def create_user(self, **kwargs):
user, factory = create_test_user(**kwargs)
return user


def initialize_test_shared_folders():
Expand Down
Loading

0 comments on commit 9d96cfc

Please sign in to comment.