Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1a70086
move views an template updates over
klpoland Sep 25, 2025
d9767ea
add versioning manager in js, consolidate modal hooks
klpoland Dec 2, 2025
7fd1bca
add previous version, version char to int
klpoland Dec 2, 2025
f0d11e9
add views, serializer field, template updates
klpoland Dec 2, 2025
3df2557
restructure dataset list view/template for refreshing
klpoland Dec 4, 2025
8b829da
refactor modal refs and list refreshing, clean up event listeners, bu…
klpoland Dec 4, 2025
913e267
save new dataset version first
klpoland Dec 4, 2025
98f6273
add version to dataset editor
klpoland Dec 11, 2025
a826333
pre-commit fixes
klpoland Dec 11, 2025
a40ead8
re-order migrations
klpoland Jan 23, 2026
d840f20
fix migration bugs, modal display issues
klpoland Jan 23, 2026
812391a
update jest tests
klpoland Jan 23, 2026
cb35d7f
address comments
klpoland Jan 30, 2026
71b5159
add keyword url back
klpoland Jan 30, 2026
4067f5b
move keyword autocomplete JS class to dedicated file
klpoland Jan 30, 2026
a0a9b74
add tests
klpoland Jan 30, 2026
aee587d
restore master changes in asset_access_control.py
klpoland Feb 12, 2026
f205c65
fix issues from review
klpoland Feb 12, 2026
f35f2e8
version control edge cases, duplicate counts on table refresh
klpoland Feb 13, 2026
8bdba89
fix refresh
klpoland Feb 13, 2026
4ce43a9
linting
klpoland Feb 13, 2026
9bb5659
fix migration order
klpoland Mar 11, 2026
2dad4bc
add tests for versioning
klpoland Mar 11, 2026
24d8d24
fix publishing url not merged
klpoland Mar 11, 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