diff --git a/.github/workflows/frontend_format.yml b/.github/workflows/frontend_format.yml
index d4bb660..c24f9e9 100644
--- a/.github/workflows/frontend_format.yml
+++ b/.github/workflows/frontend_format.yml
@@ -5,6 +5,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
+ - name: Install Prettier
+ run: npm install prettier@3.1.0
- name: Run Prettier
run: npx prettier --check "**/*.css" "**/*.js" --trailing-comma es5
- name: Install djlint
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index accb0b5..d402719 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -5,10 +5,10 @@ repos:
- id: black
- repo: https://github.com/pre-commit/mirrors-prettier
- rev: v2.3.2
+ rev: v3.1.0
hooks:
- id: prettier
- additional_dependencies: ["prettier@2.3.2"]
+ additional_dependencies: ["prettier@3.1.0"]
files: "\\.(js|jsx|ts|tsx|css|scss|less|json|yaml|yml|md)$"
- repo: https://github.com/djlint/djLint
diff --git a/nginx/Dockerfile b/nginx/Dockerfile
index fab7a1c..297f632 100644
--- a/nginx/Dockerfile
+++ b/nginx/Dockerfile
@@ -1,3 +1,5 @@
FROM nginx:1.25.2
COPY ./nginx.conf /etc/nginx/nginx.conf
-COPY ./vim.conf.template /etc/nginx/templates/vim.conf.template
\ No newline at end of file
+COPY ./vim.conf.template /etc/nginx/templates/vim.conf.template
+COPY ./vim.simssa.ca.crt /etc/nginx/templates/vim.simssa.ca.crt
+COPY ./vim.simssa.ca.key /etc/nginx/templates/vim.simssa.ca.key
\ No newline at end of file
diff --git a/nginx/vim.conf.template b/nginx/vim.conf.template
index c40b7a6..c1a9c24 100644
--- a/nginx/vim.conf.template
+++ b/nginx/vim.conf.template
@@ -11,13 +11,20 @@
server {
- listen 80;
- client_max_body_size 4G;
- server_name ${HOST_NAME};
+ listen 80;
+ listen 443 ssl;
+ client_max_body_size 4G;
+ server_name ${HOST_NAME} 127.0.0.1 localhost;
- location /static/ {
- root /virtual-instrument-museum/;
- }
+ # SSL Configuration
+ ssl_certificate /etc/nginx/templates/vim.simssa.ca.crt;
+ ssl_certificate_key /etc/nginx/templates/vim.simssa.ca.key;
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers HIGH:!aNULL:!MD5;
+
+ location /static/ {
+ root /virtual-instrument-museum/;
+ }
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # pass client address to upstream server
diff --git a/web-app/django/VIM/apps/instruments/management/commands/download_imgs.py b/web-app/django/VIM/apps/instruments/management/commands/download_imgs.py
index 0aaf4d8..9c158fb 100644
--- a/web-app/django/VIM/apps/instruments/management/commands/download_imgs.py
+++ b/web-app/django/VIM/apps/instruments/management/commands/download_imgs.py
@@ -16,7 +16,7 @@ class Command(BaseCommand):
OUTPUT_DIR = os.path.join(
settings.STATIC_ROOT, "instruments", "images", "instrument_imgs"
)
- CSV_PATH = "startup_data/all_instruments_16aug_2024.csv"
+ CSV_PATH = "startup_data/all_instruments_11oct_2024.csv"
help = "Download images and create thumbnails for instruments"
diff --git a/web-app/django/VIM/apps/instruments/management/commands/import_instruments.py b/web-app/django/VIM/apps/instruments/management/commands/import_instruments.py
index 31aee1f..0328ded 100644
--- a/web-app/django/VIM/apps/instruments/management/commands/import_instruments.py
+++ b/web-app/django/VIM/apps/instruments/management/commands/import_instruments.py
@@ -15,7 +15,7 @@ class Command(BaseCommand):
NOTE: For now, this script only imports instrument names in English and French. It
also only imports a set of previously-curated instruments that have images available.
- This list of instruments is stored in startup_data/vim_instruments_with_images-15sept.csv
+ This list of instruments is stored in startup_data/all_instruments_with_16aug_2024.csv
"""
help = "Imports instrument objects"
@@ -48,6 +48,20 @@ def parse_instrument_data(
ins_names: dict[str, str] = {
value["language"]: value["value"] for key, value in ins_labels.items()
}
+
+ # Get available instrument descriptions
+ ins_descriptions: dict = instrument_data["descriptions"]
+ ins_descs: dict[str, str] = {
+ value["language"]: value["value"] for key, value in ins_descriptions.items()
+ }
+
+ # Get available instrument aliases
+ ins_aliases: dict = instrument_data["aliases"]
+ ins_alias: dict[str, list[str]] = {
+ key: [value["value"] for value in values]
+ for key, values in ins_aliases.items()
+ }
+
# Get Hornbostel-Sachs and MIMO classifications, if available
ins_hbs: Optional[list[dict]] = instrument_data["claims"].get("P1762")
ins_mimo: Optional[list[dict]] = instrument_data["claims"].get("P3763")
@@ -62,6 +76,8 @@ def parse_instrument_data(
parsed_data: dict[str, str | dict[str, str]] = {
"wikidata_id": instrument_id,
"ins_names": ins_names,
+ "ins_descs": ins_descs,
+ "ins_alias": ins_alias,
"hornbostel_sachs_class": hbs_class,
"mimo_class": mimo_class,
}
@@ -80,7 +96,7 @@ def get_instrument_data(self, instrument_ids: list[str]) -> list[dict]:
ins_ids_str: str = "|".join(instrument_ids)
url = (
"https://www.wikidata.org/w/api.php?action=wbgetentities&"
- f"ids={ins_ids_str}&format=json&props=labels|descriptions|"
+ f"ids={ins_ids_str}&format=json&props=labels|descriptions|aliases|"
"claims&languages=en|fr"
)
response = requests.get(url, timeout=10)
@@ -104,14 +120,28 @@ def create_database_objects(
thumbnail_img_path [str]: Path to the thumbnail of the instrument image
"""
ins_names = instrument_attrs.pop("ins_names")
+ ins_descs = instrument_attrs.pop("ins_descs")
+ ins_alias = instrument_attrs.pop("ins_alias")
instrument = Instrument.objects.create(**instrument_attrs)
for lang, name in ins_names.items():
+ description = ins_descs.get(lang, "")
+ # Create InstrumentName object for "name" in "lang" with "description"
InstrumentName.objects.create(
instrument=instrument,
language=self.language_map[lang],
+ description=description,
name=name,
source_name="Wikidata",
)
+ alias = ins_alias.get(lang, [])
+ # Create InstrumentName object for "alias" in language "lang"
+ for alias_name in alias:
+ InstrumentName.objects.create(
+ instrument=instrument,
+ language=self.language_map[lang],
+ name=alias_name,
+ source_name="Wikidata",
+ )
img_obj = AVResource.objects.create(
instrument=instrument,
type="image",
@@ -130,7 +160,7 @@ def create_database_objects(
def handle(self, *args, **options) -> None:
with open(
- "startup_data/all_instruments_16aug_2024.csv",
+ "startup_data/all_instruments_11oct_2024.csv",
encoding="utf-8-sig",
) as csvfile:
reader = csv.DictReader(csvfile)
diff --git a/web-app/django/VIM/apps/instruments/migrations/0006_instrumentname_description.py b/web-app/django/VIM/apps/instruments/migrations/0006_instrumentname_description.py
new file mode 100644
index 0000000..fb05de8
--- /dev/null
+++ b/web-app/django/VIM/apps/instruments/migrations/0006_instrumentname_description.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.5 on 2024-10-21 15:05
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("instruments", "0005_remove_language_wikidata_id"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="instrumentname",
+ name="description",
+ field=models.CharField(
+ blank=True, help_text="Description of the instrument name"
+ ),
+ ),
+ ]
diff --git a/web-app/django/VIM/apps/instruments/models/instrument_name.py b/web-app/django/VIM/apps/instruments/models/instrument_name.py
index b9d58c3..a8af53a 100644
--- a/web-app/django/VIM/apps/instruments/models/instrument_name.py
+++ b/web-app/django/VIM/apps/instruments/models/instrument_name.py
@@ -8,3 +8,6 @@ class InstrumentName(models.Model):
source_name = models.CharField(
max_length=50, blank=False, help_text="Who or what called the instrument this?"
) # Stand-in for source data; format TBD
+ description = models.CharField(
+ blank=True, help_text="Description of the instrument name"
+ ) # Stand-in for description
diff --git a/web-app/django/VIM/apps/instruments/static/instruments/css/detail.css b/web-app/django/VIM/apps/instruments/static/instruments/css/detail.css
deleted file mode 100644
index 7cb5e4f..0000000
--- a/web-app/django/VIM/apps/instruments/static/instruments/css/detail.css
+++ /dev/null
@@ -1,99 +0,0 @@
-.instrument-detail {
- display: flex;
- flex-wrap: nowrap;
- align-items: center;
- flex-direction: column;
- padding: 50px;
-}
-.detail-header {
- display: flex;
- flex-direction: row;
- justify-content: flex-start;
- flex-wrap: wrap;
- width: 100%;
-}
-.detail-header hr {
- width: 100%;
-}
-.detail-body {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- justify-content: space-evenly;
- align-items: flex-start;
- width: 100%;
-}
-.detail-image {
- display: flex;
- flex-direction: row;
- flex-wrap: nowrap;
- justify-content: flex-start;
- align-items: flex-start;
-}
-.instrument-image {
- max-width: 50%;
- margin-right: 10px;
-}
-.detail-image-caption {
- display: flex;
- flex-direction: row;
- flex-wrap: nowrap;
- justify-content: flex-start;
- align-items: flex-start;
-}
-.instrument-forms {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- justify-content: flex-start;
- width: 100%;
-}
-.name-form-item {
- display: flex;
- flex-direction: row;
- flex-wrap: nowrap;
- justify-content: space-between;
- align-items: center;
-}
-.name-field {
- width: 90%;
-}
-input.edit-field {
- width: inherit;
-}
-th[scope='col'] {
- width: 30%;
-}
-th[scope='row'] {
- width: 20%;
-}
-.button-group {
- display: flex;
- flex-direction: row;
-}
-.edit-field,
-.btn.cancel,
-.btn.publish {
- display: none;
-}
-.btn {
- display: inline-block;
- padding: 5px 10px;
- cursor: pointer;
- text-align: center;
- text-decoration: none;
- outline: none;
- color: #fff;
- border: none;
- border-radius: 3px;
- margin-right: 5px;
-}
-.btn.edit {
- background-color: #ffc107;
-}
-.btn.cancel {
- background-color: #dc3545;
-}
-.btn.publish {
- background-color: #28a745;
-}
diff --git a/web-app/django/VIM/apps/instruments/static/instruments/css/index.css b/web-app/django/VIM/apps/instruments/static/instruments/css/index.css
index cbd6a2f..155bee5 100644
--- a/web-app/django/VIM/apps/instruments/static/instruments/css/index.css
+++ b/web-app/django/VIM/apps/instruments/static/instruments/css/index.css
@@ -91,6 +91,83 @@ hr {
background-color: #faf1e4;
}
+.instrument-img-container:hover .instrument-img {
+ opacity: 0.3;
+}
+
+.instrument-img {
+ opacity: 1;
+ display: block;
+ width: 100%;
+ height: auto;
+ transition: 0.5s ease;
+ backface-visibility: hidden;
+}
+
+.instrument-img-container:hover .middle-button-group {
+ opacity: 1;
+}
+
+.middle-button-group {
+ transition: 0.5s ease;
+ opacity: 0;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ -ms-transform: translate(-50%, -50%);
+ text-align: center;
+ display: flex;
+ flex-direction: column;
+ flex-wrap: nowrap;
+ justify-content: space-around;
+ width: max-content;
+}
+
+.middle-button-group .btn {
+ background-color: #435334;
+ border: 1px solid #435334;
+ color: white;
+ font-size: 14px;
+ margin-bottom: 6px;
+}
+
+.middle-button-group .btn:hover {
+ background-color: #9eb384;
+ border: 1px solid #9eb384;
+ color: white;
+}
+
+.modal-btn {
+ background-color: #435334;
+ border: 1px solid #435334;
+}
+
+.modal-btn:hover {
+ background-color: #9eb384;
+ border: 1px solid #9eb384;
+}
+
+#instrumentNameInModal {
+ font-weight: bold;
+ color: #435334;
+}
+
+#previewImages {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+#previewImages .col-3 {
+ margin-bottom: 15px;
+}
+
+#previewImages img {
+ width: 100%;
+ height: auto;
+ border-radius: 5px;
+}
+
.card-title {
color: #435334;
}
diff --git a/web-app/django/VIM/apps/instruments/static/instruments/js/AddName.js b/web-app/django/VIM/apps/instruments/static/instruments/js/AddName.js
new file mode 100644
index 0000000..6593f35
--- /dev/null
+++ b/web-app/django/VIM/apps/instruments/static/instruments/js/AddName.js
@@ -0,0 +1,410 @@
+// Get the modal element
+var addNameModal = document.getElementById('addNameModal');
+
+addNameModal.addEventListener('show.bs.modal', function (event) {
+ var button = event.relatedTarget;
+ var instrumentName = button.getAttribute('data-instrument-name');
+ var instrumentWikidataId = button.getAttribute('data-instrument-wikidata-id');
+ var instrumentNameInModal = addNameModal.querySelector(
+ '#instrumentNameInModal',
+ );
+ instrumentNameInModal.textContent = instrumentName;
+
+ var instrumentWikidataIdInModal = addNameModal.querySelector(
+ '#instrumentWikidataIdInModal',
+ );
+ instrumentWikidataIdInModal.textContent = instrumentWikidataId;
+});
+
+// the number of rows in the modal
+let rowIndex = 1;
+
+// Function to validate that the user has selected a valid language from the datalist
+function isValidLanguage(inputElement) {
+ const datalistId = inputElement.getAttribute('list');
+ const datalist = document.getElementById(datalistId);
+ const options = datalist.querySelectorAll('option');
+
+ // Check if the input value matches any option value in the datalist
+ for (let option of options) {
+ if (option.value === inputElement.value) {
+ return true; // Valid language selected
+ }
+ }
+ return false; // Invalid language input
+}
+
+// Function to check if a name already exists in Wikidata for the given language
+async function checkNameInWikidata(wikidataId, languageCode, languageLabel) {
+ const sparqlQuery = `
+ SELECT ?nameLabel WHERE {
+ wd:${wikidataId} rdfs:label ?nameLabel .
+ FILTER(LANG(?nameLabel) = "${languageCode}")
+ } LIMIT 1
+ `;
+
+ const endpointUrl = 'https://query.wikidata.org/sparql';
+ const queryUrl = `${endpointUrl}?query=${encodeURIComponent(
+ sparqlQuery,
+ )}&format=json`;
+
+ try {
+ const response = await fetch(queryUrl);
+ const data = await response.json();
+
+ if (data.results.bindings.length > 0) {
+ return { exists: true, name: data.results.bindings[0].nameLabel.value };
+ } else {
+ return { exists: false };
+ }
+ } catch (error) {
+ console.error('Error querying Wikidata:', error);
+ throw new Error('Wikidata query failed');
+ }
+}
+
+// Reusable function to create a new row
+function createRow(index) {
+ const row = document.createElement('div');
+ row.classList.add('row', 'mb-1', 'name-row');
+
+ // Create datalist options dynamically using the global languages variable
+ let datalistOptions = languages
+ .map(
+ (language) => `
+
+ `,
+ )
+ .join('');
+
+ row.innerHTML = `
+
+
+
+
+
+
+
+
+ `;
+
+ // Add event listener for remove button
+ row.querySelector('.remove-row-btn').addEventListener('click', function () {
+ row.remove();
+ updateRemoveButtons(); // Ensure correct behavior when rows are removed
+ });
+
+ return row;
+}
+
+// Function to update remove button visibility based on the number of rows
+function updateRemoveButtons() {
+ const rows = document.querySelectorAll('.name-row');
+ rows.forEach((row, index) => {
+ const removeButton = row.querySelector('.remove-row-btn');
+ // Show the remove button only if there are more than one row
+ if (rows.length > 1) {
+ removeButton.style.display = 'inline-block';
+ } else {
+ removeButton.style.display = 'none'; // Hide the button if only one row remains
+ }
+ });
+}
+
+// Function to validate and check all rows on form submission
+document
+ .getElementById('addNameForm')
+ .addEventListener('submit', async function (event) {
+ event.preventDefault(); // Prevent form submission
+
+ const nameRows = document.querySelectorAll('.name-row');
+ let allValid = true;
+ let publishResults = ''; // Collect the results for confirmation
+
+ // Iterate over each row and check if the name already exists in Wikidata
+ for (let row of nameRows) {
+ const languageInput = row.querySelector('input[list]');
+ const nameInput = row.querySelector('.name-input input[type="text"]');
+ const sourceInput = row.querySelector('.source-input input[type="text"]');
+ const descriptionInput = row.querySelector(
+ '.description-input input[type="text"]',
+ );
+ const aliasInput = row.querySelector('.alias-input input[type="text"]');
+
+ const languageCode = languageInput.value;
+ const selectedOption = row.querySelector(
+ `option[value="${languageCode}"]`,
+ );
+ const languageLabel = selectedOption ? selectedOption.textContent : '';
+
+ // get feedback elements for valid and invalid inputs respectively for language and name
+ const languageFeedbackValid = row.querySelector(
+ '.language-input .valid-feedback',
+ );
+ const languageFeedbackInvalid = row.querySelector(
+ '.language-input .invalid-feedback',
+ );
+ const nameFeedbackInvalid = row.querySelector(
+ '.name-input .invalid-feedback',
+ );
+ const sourceFeedbackInvalid = row.querySelector(
+ '.source-input .invalid-feedback',
+ );
+
+ const wikidataId = document
+ .getElementById('instrumentWikidataIdInModal')
+ .textContent.trim();
+
+ if (!isValidLanguage(languageInput)) {
+ languageInput.classList.add('is-invalid');
+ languageFeedbackInvalid.textContent =
+ 'Please select a valid language from the list.';
+ allValid = false;
+ continue;
+ }
+
+ try {
+ const result = await checkNameInWikidata(
+ wikidataId,
+ languageCode,
+ languageLabel,
+ );
+ if (result.exists) {
+ languageInput.classList.add('is-invalid');
+ languageInput.classList.remove('is-valid');
+ languageFeedbackInvalid.textContent = `This instrument already has a name in ${languageLabel} (${languageCode}): ${result.name}`;
+ allValid = false;
+ } else {
+ languageInput.classList.add('is-valid');
+ languageInput.classList.remove('is-invalid');
+ languageFeedbackValid.textContent = `This instrument does not have a name in ${languageLabel} (${languageCode}) yet. You can add a new name.`;
+
+ // check if name is empty
+ if (nameInput.value.trim() === '') {
+ nameInput.classList.add('is-invalid');
+ nameInput.classList.remove('is-valid');
+ nameFeedbackInvalid.textContent =
+ 'Please enter a name for this instrument in the selected language.';
+ allValid = false;
+ } else {
+ nameInput.classList.add('is-valid');
+ nameInput.classList.remove('is-invalid');
+ }
+
+ // check if source is empty
+ if (sourceInput.value.trim() === '') {
+ sourceInput.classList.add('is-invalid');
+ sourceInput.classList.remove('is-valid');
+ sourceFeedbackInvalid.textContent =
+ 'Please enter the source of this name.';
+ allValid = false;
+ } else {
+ sourceInput.classList.add('is-valid');
+ sourceInput.classList.remove('is-invalid');
+ }
+
+ // Add the result to the confirmation message
+ publishResults += `
${languageLabel} (${languageCode}): ${nameInput.value}; Source: ${sourceInput.value}; Description: ${descriptionInput.value}; Alias: ${aliasInput.value}`;
+ }
+ } catch (error) {
+ displayMessage(
+ 'There was an error checking Wikidata. Please try again later.',
+ 'danger',
+ );
+ return; // Stop further processing
+ }
+ }
+
+ // If all rows are valid, show the confirmation modal
+ if (allValid) {
+ document.getElementById('publishResults').innerHTML =
+ `You will publish the following:
${publishResults}`;
+ const confirmationModal = new bootstrap.Modal(
+ document.getElementById('confirmationModal'),
+ );
+ confirmationModal.show();
+ }
+ });
+
+// Function to reset the modal and ensure only one row is present
+function resetModal() {
+ const nameRows = document.getElementById('nameRows');
+ nameRows.innerHTML = ''; // Clear all rows
+ nameRows.appendChild(createRow(1)); // Add initial row
+ updateRemoveButtons(); // Ensure remove buttons are updated on reset
+ rowIndex = 1; // Reset row index
+}
+
+// Fetch languages when the modal is loaded
+document.addEventListener('DOMContentLoaded', async () => {
+ resetModal();
+});
+
+// Add a new row when the 'Add another row' button is clicked
+document.getElementById('addRowBtn').addEventListener('click', function () {
+ rowIndex++;
+ const nameRows = document.getElementById('nameRows');
+ nameRows.appendChild(createRow(rowIndex));
+ updateRemoveButtons(); // Update remove buttons after adding a new row
+});
+
+document.addEventListener('DOMContentLoaded', function () {
+ const publishCheckbox = document.getElementById('publishToWikidataCheckbox');
+ const accountSelection = document.getElementById('accountSelection');
+ const accountOption = document.getElementById('accountOption');
+ const authorizeButtonRow = document.getElementById('authorizeButtonRow');
+ const authorizeBtn = document.getElementById('authorizeBtn');
+
+ // Show account selection when 'publish to Wikidata' checkbox is checked
+ publishCheckbox.addEventListener('change', function () {
+ accountSelection.style.display = publishCheckbox.checked ? 'block' : 'none';
+ if (!publishCheckbox.checked) {
+ authorizeButtonRow.style.display = 'none'; // Hide authorize button if unchecked
+ }
+ });
+
+ // Show authorize button if the user selects 'Your own Wikidata account'
+ accountOption.addEventListener('change', function () {
+ authorizeButtonRow.style.display =
+ accountOption.value === 'user_account' ? 'block' : 'none';
+ });
+
+ // Handle the OAuth authorization button click
+ authorizeBtn.addEventListener('click', function () {
+ // Redirect to your OAuth authorization endpoint
+ window.location.href = '/oauth/authorize';
+ });
+});
+
+// Reset the modal when hidden
+document
+ .getElementById('addNameModal')
+ .addEventListener('hide.bs.modal', resetModal);
+
+// Function to get CSRF token from cookies
+function getCookie(name) {
+ let cookieValue = null;
+ if (document.cookie && document.cookie !== '') {
+ const cookies = document.cookie.split(';');
+ for (let i = 0; i < cookies.length; i++) {
+ const cookie = cookies[i].trim();
+ // Check if this cookie string begins with the name
+ if (cookie.substring(0, name.length + 1) === name + '=') {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
+ }
+ }
+ }
+ return cookieValue;
+}
+
+// Function to handle confirm publish action
+document
+ .getElementById('confirmPublishBtn')
+ .addEventListener('click', function () {
+ const wikidataId = document
+ .getElementById('instrumentWikidataIdInModal')
+ .textContent.trim();
+ const entries = [];
+
+ // Collect the data to publish
+ const nameRows = document.querySelectorAll('.name-row');
+ nameRows.forEach((row) => {
+ const languageInput = row.querySelector('input[list]');
+ const nameInput = row.querySelector('.name-input input[type="text"]');
+ const sourceInput = row.querySelector('.source-input input[type="text"]');
+ const descriptionInput = row.querySelector(
+ '.description-input input[type="text"]',
+ );
+ const aliasInput = row.querySelector('.alias-input input[type="text"]');
+
+ const languageCode = languageInput.value;
+ const nameValue = nameInput.value;
+ const sourceValue = sourceInput.value;
+ const descriptionValue = descriptionInput.value || '';
+ const aliasValue = aliasInput.value || '';
+
+ entries.push({
+ language: languageCode,
+ name: nameValue,
+ source: sourceValue,
+ description: descriptionValue,
+ alias: aliasValue,
+ });
+ });
+
+ // Determine if publishing to Wikidata is enabled
+ const publishToWikidata = document.getElementById(
+ 'publishToWikidataCheckbox',
+ ).checked;
+ const accountOption = document.getElementById('accountOption').value;
+
+ // Send the request to publish
+ fetch('/publish_name/', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': getCookie('csrftoken'),
+ },
+ body: JSON.stringify({
+ wikidata_id: wikidataId,
+ entries: entries,
+ publish_to_wikidata: publishToWikidata,
+ account_option: accountOption,
+ }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.status === 'success') {
+ alert('Data published successfully!');
+ // Close both modals
+ const addNameModal = bootstrap.Modal.getInstance(
+ document.getElementById('addNameModal'),
+ );
+ const confirmationModal = bootstrap.Modal.getInstance(
+ document.getElementById('confirmationModal'),
+ );
+
+ if (addNameModal) {
+ addNameModal.hide(); // Close the 'Add Name' modal
+ }
+
+ if (confirmationModal) {
+ confirmationModal.hide(); // Close the 'Confirmation' modal
+ }
+ } else {
+ alert('Error: ' + data.message);
+ }
+ })
+ .catch((error) => {
+ alert('An error occurred while publishing the data: ' + error.message);
+ });
+ });
diff --git a/web-app/django/VIM/apps/instruments/static/instruments/js/ImageUpload.js b/web-app/django/VIM/apps/instruments/static/instruments/js/ImageUpload.js
new file mode 100644
index 0000000..6c9e8cb
--- /dev/null
+++ b/web-app/django/VIM/apps/instruments/static/instruments/js/ImageUpload.js
@@ -0,0 +1,69 @@
+function displaySelectedImage(event, elementId) {
+ const selectedImage = document.getElementById(elementId);
+ const fileInput = event.target;
+
+ if (fileInput.files && fileInput.files[0]) {
+ const reader = new FileReader();
+
+ reader.onload = function (e) {
+ selectedImage.src = e.target.result;
+ };
+
+ reader.readAsDataURL(fileInput.files[0]);
+ }
+}
+
+// Get the modal element
+var uploadImagesModal = document.getElementById('uploadImagesModal');
+
+uploadImagesModal.addEventListener('show.bs.modal', function (event) {
+ var button = event.relatedTarget;
+ var instrumentName = button.getAttribute('data-instrument-name');
+ var instrumentWikidataId = button.getAttribute('data-instrument-wikidata-id');
+ var instrumentNameInModal = uploadImagesModal.querySelector(
+ '#instrumentNameInModal'
+ );
+ instrumentNameInModal.textContent = instrumentName;
+
+ var instrumentWikidataIdInModal = uploadImagesModal.querySelector(
+ '#instrumentWikidataIdInModal'
+ );
+ instrumentWikidataIdInModal.textContent = instrumentWikidataId;
+});
+
+document
+ .getElementById('imageFiles')
+ .addEventListener('change', function (event) {
+ var previewContainer = document.getElementById('previewImages');
+ previewContainer.innerHTML = ''; // Clear existing previews
+
+ var files = event.target.files;
+
+ for (var i = 0; i < files.length; i++) {
+ var file = files[i];
+
+ // Ensure that the file is an image
+ if (file.type.startsWith('image/')) {
+ var reader = new FileReader();
+
+ reader.onload = (function (file) {
+ return function (e) {
+ var colDiv = document.createElement('div');
+ colDiv.className = 'col-3';
+
+ var img = document.createElement('img');
+ img.src = e.target.result;
+ img.className = 'img-thumbnail';
+ img.alt = file.name;
+ img.style.maxHeight = '150px';
+ img.style.objectFit = 'cover';
+
+ colDiv.appendChild(img);
+ previewContainer.appendChild(colDiv);
+ };
+ })(file);
+
+ reader.readAsDataURL(file);
+ }
+ }
+ });
diff --git a/web-app/django/VIM/apps/instruments/static/instruments/js/InstrumentDetail.js b/web-app/django/VIM/apps/instruments/static/instruments/js/InstrumentDetail.js
deleted file mode 100644
index c868191..0000000
--- a/web-app/django/VIM/apps/instruments/static/instruments/js/InstrumentDetail.js
+++ /dev/null
@@ -1,41 +0,0 @@
-document.addEventListener('DOMContentLoaded', function () {
- const editButtons = document.querySelectorAll('.btn.edit');
- const cancelButtons = document.querySelectorAll('.btn.cancel');
- const publishButtons = document.querySelectorAll('.btn.publish');
-
- editButtons.forEach((button) => {
- button.addEventListener('click', function () {
- const parentTd = this.closest('td');
- parentTd.querySelector('.view-field').style.display = 'none';
- parentTd.querySelector('.edit-field').style.display = 'inline-block';
- parentTd.querySelector('.btn.cancel').style.display = 'inline-block';
- parentTd.querySelector('.btn.publish').style.display = 'inline-block';
- this.style.display = 'none';
- });
- });
-
- cancelButtons.forEach((button) => {
- button.addEventListener('click', function () {
- const parentTd = this.closest('td');
- parentTd.querySelector('.view-field').style.display = 'inline';
- parentTd.querySelector('.edit-field').style.display = 'none';
- parentTd.querySelector('.btn.edit').style.display = 'inline-block';
- parentTd.querySelector('.btn.publish').style.display = 'none';
- this.style.display = 'none';
- });
- });
-
- publishButtons.forEach((button) => {
- button.addEventListener('click', function () {
- const parentTd = this.closest('td');
- const newValue = parentTd.querySelector('.edit-field').value;
- // TODO: request to update the value on the server
- parentTd.querySelector('.view-field').textContent = newValue;
- parentTd.querySelector('.view-field').style.display = 'inline';
- parentTd.querySelector('.edit-field').style.display = 'none';
- parentTd.querySelector('.btn.edit').style.display = 'inline-block';
- this.style.display = 'none';
- parentTd.querySelector('.btn.cancel').style.display = 'none';
- });
- });
-});
diff --git a/web-app/django/VIM/apps/instruments/templates/instruments/detail.html b/web-app/django/VIM/apps/instruments/templates/instruments/detail.html
deleted file mode 100644
index 2fe1dc7..0000000
--- a/web-app/django/VIM/apps/instruments/templates/instruments/detail.html
+++ /dev/null
@@ -1,157 +0,0 @@
-{% extends "base.html" %}
-
-{% load static %}
-
-{% block title %}
- Instrument Detail
-{% endblock title %}
-
-{% block css_files %}
-
-
-{% endblock css_files %}
-
-{% block content %}
-
-{% endblock content %}
-
-{% block scripts %}
-
-{% endblock scripts %}
diff --git a/web-app/django/VIM/apps/instruments/templates/instruments/includes/addNameModal.html b/web-app/django/VIM/apps/instruments/templates/instruments/includes/addNameModal.html
new file mode 100644
index 0000000..89674f1
--- /dev/null
+++ b/web-app/django/VIM/apps/instruments/templates/instruments/includes/addNameModal.html
@@ -0,0 +1,125 @@
+{% load static %}
+
+
+
+
+
+
+
+
+
+
+
Are you sure you want to publish these entries?
+
+
+
+
+
+
diff --git a/web-app/django/VIM/apps/instruments/templates/instruments/includes/instrumentContainer.html b/web-app/django/VIM/apps/instruments/templates/instruments/includes/instrumentContainer.html
new file mode 100644
index 0000000..4909f6d
--- /dev/null
+++ b/web-app/django/VIM/apps/instruments/templates/instruments/includes/instrumentContainer.html
@@ -0,0 +1,28 @@
+{% load static %}
+
+
+
+
+
diff --git a/web-app/django/VIM/apps/instruments/templates/instruments/includes/masonryView.html b/web-app/django/VIM/apps/instruments/templates/instruments/includes/masonryView.html
index d920518..f0e93c6 100644
--- a/web-app/django/VIM/apps/instruments/templates/instruments/includes/masonryView.html
+++ b/web-app/django/VIM/apps/instruments/templates/instruments/includes/masonryView.html
@@ -4,20 +4,17 @@
id="masonry-view">
{% for instrument in instruments %}
{% endfor %}
diff --git a/web-app/django/VIM/apps/instruments/templates/instruments/includes/stdView.html b/web-app/django/VIM/apps/instruments/templates/instruments/includes/stdView.html
index a645148..c9c4825 100644
--- a/web-app/django/VIM/apps/instruments/templates/instruments/includes/stdView.html
+++ b/web-app/django/VIM/apps/instruments/templates/instruments/includes/stdView.html
@@ -5,22 +5,19 @@
style="display:none">
{% for instrument in instruments %}
{% endfor %}
diff --git a/web-app/django/VIM/apps/instruments/templates/instruments/includes/uploadImgModal.html b/web-app/django/VIM/apps/instruments/templates/instruments/includes/uploadImgModal.html
new file mode 100644
index 0000000..ecac235
--- /dev/null
+++ b/web-app/django/VIM/apps/instruments/templates/instruments/includes/uploadImgModal.html
@@ -0,0 +1,44 @@
+{% load static %}
+
+
+
diff --git a/web-app/django/VIM/apps/instruments/templates/instruments/index.html b/web-app/django/VIM/apps/instruments/templates/instruments/index.html
index 42bc80a..159af10 100644
--- a/web-app/django/VIM/apps/instruments/templates/instruments/index.html
+++ b/web-app/django/VIM/apps/instruments/templates/instruments/index.html
@@ -17,7 +17,7 @@
+ {% include "instruments/includes/addNameModal.html" %}
+ {% include "instruments/includes/uploadImgModal.html" %}
+
diff --git a/web-app/django/VIM/settings.py b/web-app/django/VIM/settings.py
index ee99ceb..ad0c457 100644
--- a/web-app/django/VIM/settings.py
+++ b/web-app/django/VIM/settings.py
@@ -25,7 +25,13 @@
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = IS_DEVELOPMENT
-ALLOWED_HOSTS = [os.environ.get("HOST_NAME")]
+ALLOWED_HOSTS = [
+ os.environ.get("HOST_NAME"),
+ "localhost",
+ "127.0.0.1",
+ "vim-app",
+ "0.0.0.0",
+]
if DEBUG:
import socket
@@ -156,3 +162,7 @@
CSRF_COOKIE_SECURE = IS_PRODUCTION
CSRF_TRUSTED_ORIGINS = [f'https://{os.environ.get("HOST_NAME")}']
SESSION_COOKIE_SECURE = IS_PRODUCTION
+
+
+# OAuth settings
+WIKIDATA_ACCESS_TOKEN = os.environ.get("WIKIDATA_ACCESS_TOKEN")
diff --git a/web-app/django/VIM/urls.py b/web-app/django/VIM/urls.py
index e225526..bf8ea10 100644
--- a/web-app/django/VIM/urls.py
+++ b/web-app/django/VIM/urls.py
@@ -18,15 +18,18 @@
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
-from VIM.apps.instruments.views.instrument_list import InstrumentList
-from VIM.apps.instruments.views.instrument_detail import InstrumentDetail
from django.conf.urls.i18n import i18n_patterns
+from VIM.apps.instruments.views.instrument_list import InstrumentList
+from VIM.apps.instruments.views.publish_name import publish_name
+from VIM.apps.instruments.views.wiki_apis import wikidata_callback, wikidata_authorize
urlpatterns = i18n_patterns(
path("admin/", admin.site.urls),
path("", include("VIM.apps.main.urls", namespace="main")),
path("instruments/", InstrumentList.as_view(), name="instrument-list"),
- path("instrument/
/", InstrumentDetail.as_view(), name="instrument-detail"),
+ path("oauth/authorize/", wikidata_authorize, name="wikidata_authorize"),
+ path("oauth/callback/", wikidata_callback, name="wikidata_callback"),
+ path("publish_name/", publish_name, name="publish_name"),
prefix_default_language=False,
)
diff --git a/web-app/django/startup_data/all_instruments_16aug_2024.csv b/web-app/django/startup_data/all_instruments_11oct_2024.csv
similarity index 97%
rename from web-app/django/startup_data/all_instruments_16aug_2024.csv
rename to web-app/django/startup_data/all_instruments_11oct_2024.csv
index 0483fc3..fccf488 100644
--- a/web-app/django/startup_data/all_instruments_16aug_2024.csv
+++ b/web-app/django/startup_data/all_instruments_11oct_2024.csv
@@ -858,3 +858,25 @@ http://www.wikidata.org/entity/Q18449477,clog fiddle,http://commons.wikimedia.or
http://www.wikidata.org/entity/Q60689,banhu,http://commons.wikimedia.org/wiki/Special:FilePath/Banhu.jpg
http://www.wikidata.org/entity/Q98647213,octocontrabass clarinet,http://commons.wikimedia.org/wiki/Special:FilePath/Clarinette%20octo-contrebasse%20Leblanc.jpg
http://www.wikidata.org/entity/Q19650003,sets of free reeds,http://commons.wikimedia.org/wiki/Special:FilePath/Hohner%20Multimonica%20MIM.jpg
+http://www.wikidata.org/wiki/Q4837771,babendil,http://commons.wikimedia.org/wiki/Special:FilePath/Babendil_03.jpg
+http://www.wikidata.org/wiki/Q11859952,caxirola,http://commons.wikimedia.org/wiki/Special:FilePath/Carlinhos_Brown_com_Caxirolas.jpg
+http://www.wikidata.org/wiki/Q1095097,clapstick,http://commons.wikimedia.org/wiki/Special:FilePath/Clapsticks.JPG
+http://www.wikidata.org/wiki/Q1207707,dhol,http://commons.wikimedia.org/wiki/Special:FilePath/Armenian_Dhol.jpg
+http://www.wikidata.org/wiki/Q3773253,gong bass drum,http://commons.wikimedia.org/wiki/Special:FilePath/Gong_Drum_(from_Emil_Richards_Collection).jpg
+http://www.wikidata.org/wiki/Q1249186,idakka,http://commons.wikimedia.org/wiki/Special:FilePath/Idaykka.jpg
+http://www.wikidata.org/wiki/Q1062015,janggu,http://commons.wikimedia.org/wiki/Special:FilePath/Janggu.jpg
+http://www.wikidata.org/wiki/Q7997355,kanjira,http://commons.wikimedia.org/wiki/Special:FilePath/Kanjira.JPG
+http://www.wikidata.org/wiki/Q3194590,Kebero,http://commons.wikimedia.org/wiki/Special:FilePath/Äthiopien_Kirchentrommel_Linden-Museum_21084.jpg
+http://www.wikidata.org/wiki/Q1371129,krakebs,http://commons.wikimedia.org/wiki/Special:FilePath/Qaraqib.jpg
+http://www.wikidata.org/wiki/Q6754723,maram,http://commons.wikimedia.org/wiki/Special:FilePath/Maram.jpg
+http://www.wikidata.org/wiki/Q744831,mridangam,http://commons.wikimedia.org/wiki/Special:FilePath/Wiki-mridangam.jpg
+http://www.wikidata.org/wiki/Q1898734,naqareh,http://commons.wikimedia.org/wiki/Special:FilePath/COLLECTIE_TROPENMUSEUM_Keteltrom_van_koper_TMnr_3492-7a.jpg
+http://www.wikidata.org/wiki/Q892936,octaban,http://commons.wikimedia.org/wiki/Special:FilePath/Home_made_octoban.JPG
+http://www.wikidata.org/wiki/Q2046658,pakhavaj,http://commons.wikimedia.org/wiki/Special:FilePath/Gundecha_Brothers_02A.jpg
+http://www.wikidata.org/wiki/Q3196570,qilaut,"http://commons.wikimedia.org/wiki/Special:FilePath/Aniurunna,_Inuit_man,_singing_and_playing_drum.jpg"
+http://www.wikidata.org/wiki/Q747604,sabar,http://commons.wikimedia.org/wiki/Special:FilePath/M'bung_M'bung's.jpg
+http://www.wikidata.org/wiki/Q7410116,samphor,http://commons.wikimedia.org/wiki/Special:FilePath/Samphor.jpg
+http://www.wikidata.org/wiki/Q7681062,tamborita calentana,http://commons.wikimedia.org/wiki/Special:FilePath/Tambrita.jpg
+http://www.wikidata.org/wiki/Q1562149,taphon,http://commons.wikimedia.org/wiki/Special:FilePath/Thai_taphon.jpg
+http://www.wikidata.org/wiki/Q7690562,tbilat,"http://commons.wikimedia.org/wiki/Special:FilePath/Tbilat,Marokko2.jpg"
+http://www.wikidata.org/wiki/Q1355394,thavil,"http://commons.wikimedia.org/wiki/Special:FilePath/Tavil,_%E0%AE%A4%E0%AE%B5%E0%AE%BF%E0%AE%B2%E0%AF%8D,_%E0%B4%A4%E0%B4%B5%E0%B4%BF%E0%B5%BD.jpg"
\ No newline at end of file