Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
671456f
move views an template updates over
klpoland Sep 25, 2025
26122f7
add versioning manager in js, consolidate modal hooks
klpoland Dec 2, 2025
2b97734
add previous version, version char to int
klpoland Dec 2, 2025
4e84d31
add views, serializer field, template updates
klpoland Dec 2, 2025
fef4c69
restructure dataset list view/template for refreshing
klpoland Dec 4, 2025
aded861
refactor modal refs and list refreshing, clean up event listeners, bu…
klpoland Dec 4, 2025
70da53c
save new dataset version first
klpoland Dec 4, 2025
595bcb1
add version to dataset editor
klpoland Dec 11, 2025
7c1b9fe
pre-commit fixes
klpoland Dec 11, 2025
a61a8fe
re-order migrations
klpoland Jan 23, 2026
6ec1633
fix migration bugs, modal display issues
klpoland Jan 23, 2026
f405d6c
update jest tests
klpoland Jan 23, 2026
827ee31
address comments
klpoland Jan 30, 2026
a455149
add keyword url back
klpoland Jan 30, 2026
5d7798c
move keyword autocomplete JS class to dedicated file
klpoland Jan 30, 2026
7c6fe80
add tests
klpoland Jan 30, 2026
746841e
restore master changes in asset_access_control.py
klpoland Feb 12, 2026
21aa190
fix issues from review
klpoland Feb 12, 2026
763f22a
version control edge cases, duplicate counts on table refresh
klpoland Feb 13, 2026
c359e9e
fix refresh
klpoland Feb 13, 2026
14c0e29
linting
klpoland Feb 13, 2026
8ba6847
fix migration order
klpoland Mar 11, 2026
b897fb5
add tests for versioning
klpoland Mar 11, 2026
3735d05
fix publishing url not merged
klpoland Mar 11, 2026
99fad95
linting
klpoland Mar 13, 2026
28f79f6
fix test assertions, usage to align with refactored js
klpoland Mar 13, 2026
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
@@ -0,0 +1,33 @@
# Generated by Django 4.2.26

from django.db import migrations, models
import django.db.models.deletion

def convert_version_to_integer(apps, schema_editor):
"""Convert empty string versions to 1 before changing field type."""
Dataset = apps.get_model('api_methods', 'Dataset')
# Update all datasets with empty or invalid version strings to default value of 1
for dataset in Dataset.objects.all():
if not dataset.version or dataset.version.strip() == '':
dataset.version = '1'
dataset.save(update_fields=['version'])

class Migration(migrations.Migration):

dependencies = [
("api_methods", "0020_group_owner_as_member_and_individual_share_field"),
]

operations = [
migrations.AddField(
model_name='dataset',
name='previous_version',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='next_version', to='api_methods.dataset'),
),
migrations.RunPython(convert_version_to_integer, migrations.RunPython.noop),
migrations.AlterField(
model_name='dataset',
name='version',
field=models.IntegerField(default=1),
),
]
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0020_group_owner_as_member_and_individual_share_field
0021_dataset_previous_version_alter_dataset_version
20 changes: 19 additions & 1 deletion gateway/sds_gateway/api_methods/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,14 @@ class Dataset(BaseModel):
institutions = models.TextField(blank=True)
release_date = models.DateTimeField(blank=True, null=True)
repository = models.URLField(blank=True)
version = models.CharField(max_length=255, blank=True)
version = models.IntegerField(default=1)
previous_version = models.ForeignKey(
"self",
on_delete=models.PROTECT,
blank=True,
null=True,
related_name="next_version",
)
website = models.URLField(blank=True)
provenance = models.JSONField(blank=True, null=True)
citation = models.JSONField(blank=True, null=True)
Expand Down Expand Up @@ -1093,6 +1100,17 @@ def user_can_share(cls, user: "User", item_uuid: uuid.UUID, item_type: str) -> b
PermissionLevel.CO_OWNER,
]

@classmethod
def user_can_advance_version(
cls, user: "User", item_uuid: uuid.UUID, item_type: str
) -> bool:
"""Check if user can advance the version of the item."""
permission_level = cls.get_user_permission_level(user, item_uuid, item_type)
return permission_level in [
PermissionLevel.OWNER,
PermissionLevel.CO_OWNER,
]


class DEPRECATEDPostProcessedData(BaseModel):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class DatasetGetSerializer(serializers.ModelSerializer[Dataset]):
owner_email = serializers.SerializerMethodField()
permission_level = serializers.SerializerMethodField()
can_edit = serializers.SerializerMethodField()
can_advance_version = serializers.SerializerMethodField()
next_version = serializers.SerializerMethodField()

def get_authors(self, obj):
"""Return the full authors list using the model's get_authors_display method."""
Expand Down Expand Up @@ -162,10 +164,45 @@ def get_can_edit(self, obj):
).first()

if permission:
return permission.permission_level in ["co-owner", "contributor"]
return permission.permission_level in [
PermissionLevel.CO_OWNER,
PermissionLevel.CONTRIBUTOR,
]

return False

def get_can_advance_version(self, obj):
"""Check if the current user can advance the version of the dataset."""
request = self.context.get("request")
if not request or not hasattr(request, "user"):
return False

# Check if user is the owner
if obj.owner == request.user:
return True

# Check for shared permissions that allow advancing the version
permission = UserSharePermission.objects.filter(
shared_with=request.user,
item_type=ItemType.DATASET,
item_uuid=obj.uuid,
is_enabled=True,
is_deleted=False,
).first()

if permission:
return permission.permission_level == PermissionLevel.CO_OWNER

return False

def get_next_version(self, obj):
"""Get the next version of the dataset."""
next_version = None
if obj.next_version.exists():
next_version_obj = obj.next_version.first()
next_version = next_version_obj.version
return next_version

class Meta:
model = Dataset
fields = "__all__"
Expand Down
4 changes: 2 additions & 2 deletions gateway/sds_gateway/api_methods/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class DatasetFactory(DjangoModelFactory):
institutions: Fixed list ["Example University"]
release_date: Random datetime
repository: Random URL
version: Fixed value "1.0.0"
version: Integer number
website: Random URL
provenance: Fixed dict {"source": "test"}
citation: Fixed dict {"title": "Test Dataset"}
Expand Down Expand Up @@ -89,7 +89,7 @@ class DatasetFactory(DjangoModelFactory):
institutions = ["Example University"]
release_date = Faker("date_time")
repository = Faker("url")
version = "1.0.0"
version = 1
website = Faker("url")
provenance = {"source": "test"}
citation = {"title": "Test Dataset"}
Expand Down
98 changes: 18 additions & 80 deletions gateway/sds_gateway/static/js/actions/DetailsActionManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ class DetailsActionManager {
*/
constructor(config) {
this.permissions = config.permissions;
this.itemUuid = config.itemUuid;
this.itemType = config.itemType;
this.modalId = `${this.itemType}DetailsModal-${this.itemUuid}`;
this.initializeEventListeners();
}

Expand Down Expand Up @@ -69,7 +72,7 @@ class DetailsActionManager {
// Handle modal show events
document.addEventListener("show.bs.modal", (e) => {
const modal = e.target;
if (modal.id === "datasetDetailsModal") {
if (modal.id === this.modalId) {
this.handleDatasetDetailsModalShow(modal, e);
}
});
Expand All @@ -82,7 +85,7 @@ class DetailsActionManager {
async handleCaptureDetails(captureUuid) {
try {
// Show loading state
this.showModalLoading("captureDetailsModal");
window.DOMUtils.showModalLoading(this.modalId);

// Fetch capture details
const captureData = await window.APIClient.get(
Expand All @@ -93,11 +96,11 @@ class DetailsActionManager {
this.populateCaptureDetailsModal(captureData);

// Show modal
this.openModal("captureDetailsModal");
window.DOMUtils.openModal(this.modalId);
} catch (error) {
console.error("Error loading capture details:", error);
this.showModalError(
"captureDetailsModal",
window.DOMUtils.showModalError(
this.modalId,
"Failed to load capture details",
);
}
Expand All @@ -110,17 +113,17 @@ class DetailsActionManager {
* @param {Object} tree - File tree data
*/
async populateDatasetDetailsModal(datasetData, statistics, tree) {
const modal = document.getElementById("datasetDetailsModal");
const modal = document.getElementById(this.modalId);
if (!modal) return;

// Clear loading state and restore original modal content
this.clearModalLoading("datasetDetailsModal");
window.DOMUtils.clearModalLoading(this.modalId);

// Update basic information using the correct selectors from the template
this.updateElementText(
modal,
".dataset-details-name",
datasetData.name || "Untitled Dataset",
`${datasetData.name} (v${datasetData.version})` || "Untitled Dataset",
);
this.updateElementText(
modal,
Expand Down Expand Up @@ -183,7 +186,7 @@ class DetailsActionManager {
* @param {Object} captureData - Capture data
*/
populateCaptureDetailsModal(captureData) {
const modal = document.getElementById("captureDetailsModal");
const modal = document.getElementById(this.modalId);
if (!modal) return;

// Update basic information
Expand Down Expand Up @@ -645,71 +648,6 @@ class DetailsActionManager {
}
}

/**
* Clear modal loading state and restore original content
* @param {string} modalId - Modal ID
*/
clearModalLoading(modalId) {
const modal = document.getElementById(modalId);
if (!modal) return;

const modalBody = modal.querySelector(".modal-body");
if (modalBody?.dataset.originalContent) {
// Restore original content
modalBody.innerHTML = modalBody.dataset.originalContent;
// Clean up the stored content
delete modalBody.dataset.originalContent;
}
}

/**
* Show modal error
* @param {string} modalId - Modal ID
* @param {string} message - Error message
*/
async showModalError(modalId, message) {
const modal = document.getElementById(modalId);
if (!modal) return;

const modalBody = modal.querySelector(".modal-body");
if (modalBody) {
await window.DOMUtils.renderError(modalBody, message, {
format: "alert",
alert_type: "danger",
icon: "exclamation-triangle",
});
}

// Show modal even with error
this.openModal(modalId);
}

/**
* Open modal
* @param {string} modalId - Modal ID
*/
openModal(modalId) {
const modal = document.getElementById(modalId);
if (!modal) return;

const bootstrapModal = new bootstrap.Modal(modal);
bootstrapModal.show();
}

/**
* Close modal
* @param {string} modalId - Modal ID
*/
closeModal(modalId) {
const modal = document.getElementById(modalId);
if (!modal) return;

const bootstrapModal = bootstrap.Modal.getInstance(modal);
if (!bootstrapModal) return;

bootstrapModal.hide();
}

/**
* Handle dataset details modal show
* @param {Element} modal - Modal element
Expand All @@ -721,8 +659,8 @@ class DetailsActionManager {

if (!triggerElement) {
console.warn("No trigger element found for dataset details modal");
this.showModalError(
"datasetDetailsModal",
window.DOMUtils.showModalError(
this.modalId,
"Unable to load dataset details",
);
return;
Expand All @@ -733,7 +671,7 @@ class DetailsActionManager {

if (!datasetUuid) {
console.warn("No dataset UUID found on trigger element:", triggerElement);
this.showModalError("datasetDetailsModal", "Dataset UUID not found");
window.DOMUtils.showModalError(this.modalId, "Dataset UUID not found");
return;
}

Expand All @@ -748,7 +686,7 @@ class DetailsActionManager {
async loadDatasetDetailsForModal(datasetUuid) {
try {
// Show loading state
this.showModalLoading("datasetDetailsModal");
window.DOMUtils.showModalLoading(this.modalId);

// Fetch dataset details
const response = await window.APIClient.get(
Expand All @@ -764,8 +702,8 @@ class DetailsActionManager {
await this.populateDatasetDetailsModal(datasetData, statistics, tree);
} catch (error) {
console.error("Error loading dataset details:", error);
this.showModalError(
"datasetDetailsModal",
window.DOMUtils.showModalError(
this.modalId,
"Failed to load dataset details",
);
}
Expand Down
Loading
Loading