diff --git a/applications/common/deploy/values-prod.yaml b/applications/common/deploy/values-prod.yaml index 38769bc9..2eeb259c 100644 --- a/applications/common/deploy/values-prod.yaml +++ b/applications/common/deploy/values-prod.yaml @@ -1,4 +1,5 @@ harness: env: - name: SENTRY_DSN - value: https://57b1ca8a70634782bd6c90dd119eeab5@sentry.metacell.us/9 + value: https://57b1ca8a70634782bd6c90dd119eeab5@o4506739169951744.ingest.sentry.io/4506758817382404 + diff --git a/applications/pgadmin/deploy/charts/values.yaml b/applications/pgadmin/deploy/charts/values.yaml index f9389e4d..c41b89ce 100644 --- a/applications/pgadmin/deploy/charts/values.yaml +++ b/applications/pgadmin/deploy/charts/values.yaml @@ -18,9 +18,11 @@ pgadmin: ## pgadmin config, Any custom environment variabls. Settings in config.py can be overriden with an environment variable using the prefix: PGADMIN_CONFIG_ ## eg turn off enhanced cookie protection for default AKS, loadbalancer installation - #config: - # - name: "PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION" - # value: "False" + config: + - name: "PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION" + value: "False" + - name: "PGADMIN_CONFIG_WTF_CSRF_CHECK_DEFAULT" + value: "False" ## Enable persistence using Persistent Volume Claims ## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ diff --git a/applications/portal/Dockerfile b/applications/portal/Dockerfile index a6f50cec..b1efde6f 100644 --- a/applications/portal/Dockerfile +++ b/applications/portal/Dockerfile @@ -1,7 +1,6 @@ -ARG CLOUDHARNESS_FRONTEND_BUILD ARG CLOUDHARNESS_DJANGO -FROM $CLOUDHARNESS_FRONTEND_BUILD as frontend +FROM node:20 as frontend ENV APP_DIR=/app @@ -17,6 +16,10 @@ RUN yarn build FROM $CLOUDHARNESS_DJANGO +RUN apt-get update && apt-get install -y --no-install-recommends \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + WORKDIR ${APP_DIR} RUN mkdir -p ${APP_DIR}/static/www diff --git a/applications/portal/README.md b/applications/portal/README.md index 6fffafcf..a2c808d7 100644 --- a/applications/portal/README.md +++ b/applications/portal/README.md @@ -31,7 +31,7 @@ See [here](backend/README.md#Develop) ### Frontend -Backend code is inside the *frontend* directory. +Frontend code is inside the *frontend* directory. Frontend is by default generated as a React web application, but no constraint about this specific technology. @@ -111,7 +111,7 @@ The `KUBERNETES_SERVICE_HOST` switch will activate the creation of the keycloak The base command to run tests is `python manage.py test`. -To run tests locally eed to add configure environmental variables to get the +To run tests locally you need to add configure environmental variables to get the correct configuration and a working configure instance of the postgres database running. If you already have a deployment with the database, first forward it: diff --git a/applications/portal/api/openapi.yaml b/applications/portal/api/openapi.yaml index b45bf33e..21e9ef0f 100644 --- a/applications/portal/api/openapi.yaml +++ b/applications/portal/api/openapi.yaml @@ -55,6 +55,16 @@ paths: format: date-time type: string in: query + - + examples: + query parameter added as - status=rejected: + value: /api/antibodies?page=1&size=100&status=rejected + name: status + description: 'Add a status to filter the query - CURATED, REJECTED, QUEUE. ' + schema: + type: string + in: query + required: false responses: '200': content: @@ -369,6 +379,16 @@ paths: type: integer in: path required: true + /antibodies/export: + summary: Exports all antobodies in csv format + get: + tags: + - antibody + responses: + '200': + content: + text/csv: {} + description: All antibodies in csv format components: schemas: FilterRequest: @@ -739,3 +759,19 @@ components: bearerFormat: JWT type: http x-bearerInfoFunc: cloudharness.auth.decode_token +tags: + - + name: antibody + description: '' + - + name: search + description: '' + - + name: api + description: '' + - + name: test + description: '' + - + name: ingest + description: '' diff --git a/applications/portal/backend/README.md b/applications/portal/backend/README.md index bfa7869c..66ef0ba4 100644 --- a/applications/portal/backend/README.md +++ b/applications/portal/backend/README.md @@ -10,6 +10,17 @@ This example uses the [Django](https://www.djangoproject.com/) library on top of ## Requirements Python >= 3 +Cloudharness libraries (check also dockerfile chain) + +``` +pip install -e cloud-harness/libraries/models +pip install -e cloud-harness/libraries/cloudharness-common +pip install -r cloud-harness/infrastructure/common-images/cloudharness-fastapi/libraries/fastapi/requirements.txt +pip install -e cloud-harness/infrastructure/common-images/cloudharness-django/libraries/cloudharness-django +pip install -r applications/portal/backend/requirements.txt +pip install -e applications/portal/backend +``` + ## Local backend development ``` # store the accounts api admin password on the local disk diff --git a/applications/portal/backend/api/admin.py b/applications/portal/backend/api/admin.py index 7df09fe6..d5107354 100644 --- a/applications/portal/backend/api/admin.py +++ b/applications/portal/backend/api/admin.py @@ -12,7 +12,7 @@ from django.utils.encoding import smart_str from django.utils.html import escape, format_html, format_html_join, mark_safe from django.utils.text import format_lazy -from import_export.admin import ImportMixin +from import_export.admin import ImportExportModelAdmin from keycloak.exceptions import KeycloakGetError from cloudharness_django.models import Member @@ -100,7 +100,7 @@ class AntibodyFilesAdmin(admin.TabularInline): extra = 1 @admin.register(Antibody) -class AntibodyAdmin(ImportMixin, admin.ModelAdmin): +class AntibodyAdmin(ImportExportModelAdmin): change_form_template = "admin/antibody_change_form.html" diff --git a/applications/portal/backend/api/controllers/antibody_controller.py b/applications/portal/backend/api/controllers/antibody_controller.py index 32134e78..457c02b1 100644 --- a/applications/portal/backend/api/controllers/antibody_controller.py +++ b/applications/portal/backend/api/controllers/antibody_controller.py @@ -8,7 +8,7 @@ from fastapi import HTTPException from fastapi.encoders import jsonable_encoder -from fastapi.responses import JSONResponse +from fastapi.responses import JSONResponse, RedirectResponse from api.services import antibody_service from api.utilities.exceptions import AntibodyDataException @@ -17,8 +17,7 @@ from openapi.models import UpdateAntibody as UpdateAntibodyDTO from openapi.models import Antibody as AntibodyDTO - -def get_antibodies(page: int, size: int, updated_from: datetime, updated_to: datetime) -> PaginatedAntibodies: +def get_antibodies(page: int, size: int, updated_from: datetime, updated_to: datetime, status=str) -> PaginatedAntibodies: if page is None: page = 1 if size is None: @@ -28,7 +27,7 @@ def get_antibodies(page: int, size: int, updated_from: datetime, updated_to: dat if size < 1: raise HTTPException(status_code=400, detail="Size must be greater than 0") try: - return antibody_service.get_antibodies(int(page), int(size), updated_from, updated_to) + return antibody_service.get_antibodies(int(page), int(size), updated_from, updated_to, status) except ValueError: raise HTTPException(status_code=400, detail="Page and size must be integers") @@ -80,3 +79,15 @@ def get_by_accession(accession_number: int) -> AntibodyDTO: return antibody_service.get_antibody_by_accession(accession_number) except Antibody.DoesNotExist as e: raise HTTPException(status_code=404, detail=e.message) + + +def get_antibodies_export(): + from api.services.export_service import generate_antibodies_csv_file + fname = "static/www/antibodies_export.csv" + + # check if file exists and it is created within 24 hours + # if not, generate a new file + if not os.path.exists(fname) or (datetime.now() - datetime.fromtimestamp(os.path.getmtime(fname))).days > 1: + generate_antibodies_csv_file(fname) + return RedirectResponse("/" + fname) + # return FileResponse(fname, filename="antibodies_export.csv") \ No newline at end of file diff --git a/applications/portal/backend/api/migrations/0008_auto_20240215_0459.py b/applications/portal/backend/api/migrations/0008_auto_20240215_0459.py new file mode 100644 index 00000000..0be1168c --- /dev/null +++ b/applications/portal/backend/api/migrations/0008_auto_20240215_0459.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.9 on 2024-02-15 12:59 + +from django.db import migrations, connection +from cloudharness import log +from api.repositories.maintainance import rechunk_catalog_number + +def chunk_catalog_number(apps, schema_editor): + Antibody = apps.get_model('api', 'Antibody') + rechunk_catalog_number(Antibody) + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0007_alter_antibody_comments"), + ] + operations = [ + migrations.RunPython(chunk_catalog_number) + ] diff --git a/applications/portal/backend/api/migrations/0009_auto_20240220_0451.py b/applications/portal/backend/api/migrations/0009_auto_20240220_0451.py new file mode 100644 index 00000000..aafb6169 --- /dev/null +++ b/applications/portal/backend/api/migrations/0009_auto_20240220_0451.py @@ -0,0 +1,72 @@ +# Generated by Django 4.2.10 on 2024-02-20 12:51 + +import api.models +import django.contrib.postgres.indexes +import django.contrib.postgres.search +from django.db import migrations, models +import django.db.models.deletion + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0008_auto_20240215_0459"), + ] + + operations = [ + migrations.RunSQL( + sql=""" + DROP MATERIALIZED VIEW IF EXISTS antibody_search; + CREATE MATERIALIZED VIEW antibody_search AS + SELECT ix, + ( + setweight(to_tsvector('english'::regconfig, ((( + COALESCE(ab_name, ''::text) || ' '::text) || + COALESCE(clone_id, ''::text) || ' '::text)) + ), 'A'::"char") || + + setweight(to_tsvector('english'::regconfig, ((((((((((((((((((((((((( + COALESCE(api_vendor.vendor, ''::text) || ' '::text) || + COALESCE(api_specie.name, ''::text) || ' '::text) || + COALESCE(target_subregion, ''::text) || ' '::text) || + COALESCE(clonality, ''::text)) || ' '::text) || + COALESCE(target_modification, ''::text)) || ' '::text) || + COALESCE(epitope, ''::character varying)::text) || ' '::text) || + COALESCE(product_isotype, ''::character varying)::text) || ' '::text) || + COALESCE(ab_target, ''::character varying)::text) || ' '::text) || + COALESCE(ab_target_entrez_gid, ''::character varying)::text) || ' '::text) || + COALESCE(uniprot_id, ''::character varying)::text) || ' '::text) || + COALESCE(product_isotype, ''::character varying)::text) || ' '::text) || + COALESCE(product_conjugate, ''::text) || + COALESCE(product_form, ''::character varying)::text) || ' '::text) || + COALESCE(target_species_raw, ''::character varying)::text) || ' '::text) || + COALESCE(kit_contents, ''::character varying)::text) || ' '::text)), 'C'::"char") || + setweight(to_tsvector('english'::regconfig, ((( + COALESCE(comments, ''::text) || ' '::text) || + COALESCE(curator_comment, ''::text) || ' '::text)) + ), 'D'::"char") + + ) AS search_vector, + defining_citation, + disc_date, + status + FROM api_antibody + LEFT JOIN api_vendor ON api_vendor.id = api_antibody.vendor_id + LEFT JOIN api_specie ON api_specie.id = api_antibody.source_organism_id + """, + reverse_sql = ''' + DROP MATERIALIZED VIEW antibody_search; + ''' + ), + migrations.RunSQL( + sql="""CREATE UNIQUE INDEX IF NOT EXISTS antibody_search_idx + ON antibody_search + (ix); + """, + ), + migrations.AddIndex( + model_name="antibodysearch", + index=django.contrib.postgres.indexes.GinIndex( + fields=["search_vector"], name="antibody_search_fts_idx" + ), + ) + ] diff --git a/applications/portal/backend/api/models.py b/applications/portal/backend/api/models.py index 6b5d1bd0..6dc91f49 100644 --- a/applications/portal/backend/api/models.py +++ b/applications/portal/backend/api/models.py @@ -277,8 +277,10 @@ def import_save(self): if first_save: super().save() self._handle_duplicates() + if self.catalog_num: - self.catalog_num_search = catalog_number_chunked(self.catalog_num) + self.catalog_num_search = catalog_number_chunked(self.catalog_num, self.cat_alt) + if not self.accession or not self.ab_id: self._generate_automatic_attributes() super().save() @@ -296,7 +298,7 @@ def save(self, *args, update_search=True, **kwargs): super(Antibody, self).save(*args, **kwargs) if self.catalog_num: - self.catalog_num_search = catalog_number_chunked(self.catalog_num) + self.catalog_num_search = catalog_number_chunked(self.catalog_num, self.cat_alt) self._handle_duplicates(*args, **kwargs) self._generate_related_fields(*args, **kwargs) self._synchronize_target_species(old_species) diff --git a/applications/portal/backend/api/repositories/maintainance.py b/applications/portal/backend/api/repositories/maintainance.py index 888d182c..c74bb9c0 100644 --- a/applications/portal/backend/api/repositories/maintainance.py +++ b/applications/portal/backend/api/repositories/maintainance.py @@ -1,5 +1,7 @@ from django.db import connection from cloudharness import log +from api.utilities.functions import catalog_number_chunked + def refresh_search_view(): import threading @@ -8,4 +10,21 @@ def refresh_search_view_thread(): with connection.cursor() as cursor: cursor.execute("REFRESH MATERIALIZED VIEW CONCURRENTLY antibody_search;") connection.commit() - threading.Thread(target=refresh_search_view_thread).start() \ No newline at end of file + threading.Thread(target=refresh_search_view_thread).start() + + +def rechunk_catalog_number(Antibody_model): + i = 0 + from api.models import STATUS + with connection.cursor() as cursor: + for antibody in Antibody_model.objects.filter(status=STATUS.CURATED).values('ix', 'catalog_num_search', 'catalog_num', 'cat_alt'): + i = i + 1 + if i % 10000 == 0: + print("Migrated", i) + new_catalog_number_chunked = catalog_number_chunked(antibody['catalog_num'], antibody['cat_alt']) + if new_catalog_number_chunked != antibody['catalog_num_search']: + + try: + cursor.execute(f"UPDATE api_antibody SET catalog_num_search = '{new_catalog_number_chunked}' WHERE ix={antibody['ix']};") + except Exception as e: + log.exception("`%s` `%s` %s`", antibody['catalog_num_search'], antibody['catalog_num'], antibody['cat_alt']) \ No newline at end of file diff --git a/applications/portal/backend/api/repositories/search_repository.py b/applications/portal/backend/api/repositories/search_repository.py index 3b0860c1..49819433 100644 --- a/applications/portal/backend/api/repositories/search_repository.py +++ b/applications/portal/backend/api/repositories/search_repository.py @@ -64,7 +64,8 @@ def fts_antibodies(page: int = 0, size: int = settings.LIMIT_NUM_RESULTS, search # search only allows alphanumeric characters and spaces if might_be_catalog_number(search): - cat_search = fts_by_catalog_number(re.sub(r'[^\w\s]', '', search), page, size) + cat_search = fts_by_catalog_number(search, page, size) + if cat_search: return cat_search diff --git a/applications/portal/backend/api/services/antibody_service.py b/applications/portal/backend/api/services/antibody_service.py index f73b0111..5db037bf 100644 --- a/applications/portal/backend/api/services/antibody_service.py +++ b/applications/portal/backend/api/services/antibody_service.py @@ -10,9 +10,9 @@ from api.utilities.exceptions import DuplicatedAntibody, AntibodyDataException from cloudharness import log from openapi.models import AddAntibody as AddAntibodyDTO -from openapi.models import UpdateAntibody as UpdateAntibodyDTO +from openapi.models import UpdateAntibody as UpdateAntibodyDTO, Status from openapi.models import Antibody as AntibodyDTO, PaginatedAntibodies - +from api.utilities.functions import check_if_status_exists_or_curated antibody_mapper = AntibodyMapper() @@ -25,9 +25,9 @@ def search_antibodies_by_catalog(search: str, page: int = 1, size: int = 50, return PaginatedAntibodies(page=int(page), totalElements=p.count, items=items) -def get_antibodies(page: int = 1, size: int = 50, date_from: datetime = None, date_to: datetime = None) -> PaginatedAntibodies: +def get_antibodies(page: int = 1, size: int = 50, date_from: datetime = None, date_to: datetime = None, status:str=None) -> PaginatedAntibodies: try: - query = Antibody.objects.filter(status=STATUS.CURATED) + query = Antibody.objects.filter(status=check_if_status_exists_or_curated(status)) if date_from: query = query.filter(lastedit_time__gte=date_from) if date_to: @@ -53,9 +53,6 @@ def create_antibody(body: AddAntibodyDTO, userid: str) -> AntibodyDTO: antibody.uid = userid antibody.save() - if antibody.get_duplicate(): - raise DuplicatedAntibody(antibody_mapper.to_dto(antibody)) - return antibody_mapper.to_dto(antibody) diff --git a/applications/portal/backend/api/services/export_service.py b/applications/portal/backend/api/services/export_service.py new file mode 100644 index 00000000..e832e007 --- /dev/null +++ b/applications/portal/backend/api/services/export_service.py @@ -0,0 +1,38 @@ +import os +import subprocess +import sys + +from cloudharness.applications import get_current_configuration +from cloudharness.utils.config import CloudharnessConfig + + +def generate_antibodies_csv_file(fname, status="CURATED"): + app = get_current_configuration() + my_env = os.environ + os.environ["PGPASSWORD"] = app.harness.database['pass'] + # execute shell command + proc = subprocess.run([ + "psql", "-h", + f"{app.db_name}.{CloudharnessConfig.get_namespace()}", + "-U", app.harness.database.user, + "-d", app.harness.database.postgres['initialdb'], + "-c", + f"\\copy ({app['export_query']} AND status='{status}') TO '{fname}' DELIMITER ',' CSV HEADER"], + env=my_env, + stderr=subprocess.STDOUT, + text=True + ) + if proc.returncode != 0: + raise Exception("Error during csv export: %s", proc.stdout) + + +if __name__ == '__main__': + def test_export(self): + from api.services import export_service + fname = "/tmp/f.csv" + export_service.generate_antibodies_csv_file(fname) + + with open(fname) as f: + l = f.readlines() + assert l + test_export() \ No newline at end of file diff --git a/applications/portal/backend/api/tests.py b/applications/portal/backend/api/tests.py index e0e10ed8..371c092e 100644 --- a/applications/portal/backend/api/tests.py +++ b/applications/portal/backend/api/tests.py @@ -14,6 +14,7 @@ from .admin import VendorAdmin from .models import Vendor, Antibody, VendorDomain, VendorSynonym, Specie +from api.utilities.functions import catalog_number_chunked example_ab = { "clonality": "cocktail", @@ -136,22 +137,18 @@ def test_create(self): a.status = STATUS.CURATED a.save(update_search=False) - try: - duplicated = AddAntibodyDTO(**example_ab) - duplicated.vendorName = "My vendor synonym" # should keep the same vendor and add a synonym - create_antibody(duplicated, "bbb") - self.fail("Should detect duplicate antibody") - except DuplicatedAntibody as e: - da = e.antibody - assert da.accession != da.abId - assert da.abId == ab.abId - assert da.status == Status.QUEUE - assert da.vendorId == ab.vendorId - assert da.vendorName == "My vendorname" + + duplicated = AddAntibodyDTO(**example_ab) + # duplicated.vendorName = "My vendor synonym" # should keep the same vendor and add a synonym + da = create_antibody(duplicated, "bbb") + + assert da.accession != da.abId + assert da.abId == ab.abId + assert da.status == Status.QUEUE + assert da.vendorId == ab.vendorId + assert da.vendorName == "My vendorname" - synonyms = VendorSynonym.objects.filter(vendor__id=da.vendorId) - assert len(synonyms) == 1 - + assert VendorDomain.objects.all().count() == 2 assert Vendor.objects.all().count() == 1 @@ -173,16 +170,36 @@ def test_create(self): assert ab.ix assert ab.showLink is not None + ab_by_accession = get_antibody_by_accession(ab.accession) + assert ab_by_accession.abId == ab.abId + assert ab_by_accession.accession == ab.accession + assert ab_by_accession.vendorName == ab.vendorName + # Search by catalog number self.assertEquals(len(fts_antibodies(search="N176A").items), 2) assert len(fts_antibodies(search="N176A 35").items) == 1 assert len(fts_antibodies(search="N176A|35").items) == 1 + assert len(fts_antibodies(search="N176A|35").items) == 1 assert len(fts_antibodies(search="N17").items) == 0 assert len(fts_antibodies(search="N17").items) == 0 + + a = Antibody.objects.get(ab_id=ab.abId) + a.catalog_num='N0304-AB635P-L' + a.cat_alt='N0304-AB635P-S' + a.save() + + assert len(fts_antibodies(search="N0304-AB635P-L").items) == 1 + assert len(fts_antibodies(search="N0304AB635PL").items) == 1 + assert len(fts_antibodies(search="N0304-AB635P-S").items) == 1 + assert len(fts_antibodies(search="635P-L").items) == 1 + assert len(fts_antibodies(search="N0304-AB635P-X").items) == 0 + # Search in plain fields + # FIXME full-text search is not working in the tests as cannot account for materialized view update + # refresh_search_view() # import time # time.sleep(60) # give time to the materialized view to be updated @@ -208,10 +225,7 @@ def test_create(self): # assert len(fts_antibodies(search="Rabbit").items) == 1 # assert len(fts_antibodies(search="Andrew Dingwall").items) == 1 - # ab_by_accession = get_antibody_by_accession(ab.accession) - # assert ab_by_accession.abId == ab.abId - # assert ab_by_accession.accession == ab.accession - # assert ab_by_accession.vendorName == ab.vendorName + def test_update(self): user_id = "aaaa" @@ -246,6 +260,10 @@ def test_update(self): dao.save() assert dao.species.count() == 3 + + + + class VendorAdminTests(TestCase): def setUp(self): self.site = AdminSite() @@ -309,3 +327,5 @@ def test_swap_ownership_antibodies(self): # self.assertEquals(len(Antibody.objects.all()), 2) # self.assertEquals(len(Vendor.objects.all()), 2) # self.assertEquals(len(VendorDomain.objects.all()), 1) + + diff --git a/applications/portal/backend/api/utilities/functions.py b/applications/portal/backend/api/utilities/functions.py index dd00df49..0687c177 100644 --- a/applications/portal/backend/api/utilities/functions.py +++ b/applications/portal/backend/api/utilities/functions.py @@ -5,7 +5,7 @@ from django.db.models import AutoField from django.forms import URLField -from portal.settings import ANTIBODY_PERSISTENCE +from portal.constants import ANTIBODY_PERSISTENCE magic = 64544 prefix = "AB_" @@ -34,10 +34,34 @@ def extract_base_url(url: Union[str, URLField]) -> str: def get_antibody_persistence_directory(ab_id: str, filename: str) -> str: return f'{ANTIBODY_PERSISTENCE}/{ab_id}/{filename}' -def catalog_number_chunked(catalog_number: str, fill=' ') -> List[str]: - if not catalog_number: +def get_catalog_number_digits(catalog_number: str) -> List[str]: + return [c.lower() for c in re.split(r'(\d+)',re.sub(r'[^\w]', '', catalog_number)) if c] + + +def get_catalog_non_alphanumeric(catalog_number: str) -> List[str]: + split_by_non_alphanumeric_array = [c.lower() for c in re.split(r'\W+', catalog_number) if c] + alphanumeric_catalogs = [] + for c in split_by_non_alphanumeric_array: + alphanumeric_catalogs.extend(get_catalog_number_digits(c)) + return alphanumeric_catalogs + +def catalog_number_chunked(catalog_number: str, catalog_alt_number: str=None, fill=' ') -> List[str]: + if not catalog_number and not catalog_alt_number: return "" try: - return fill.join(c for c in re.split(r'(\d+)',re.sub(r'[^\w]', '', catalog_number)) if c).strip().lower() + cat_split_by_digits = get_catalog_number_digits(catalog_number) + cat_split_by_non_alphanumeric = get_catalog_non_alphanumeric(catalog_number) + + cat_alt_split_by_digits = get_catalog_number_digits(catalog_alt_number) if catalog_alt_number else [] + cat_alt_split_by_non_alphanumeric = get_catalog_non_alphanumeric(catalog_alt_number) if catalog_alt_number else [] + + + joined_catalog_chunk = fill.join(set(cat_split_by_digits + cat_split_by_non_alphanumeric + cat_alt_split_by_digits + cat_alt_split_by_non_alphanumeric)) + + return joined_catalog_chunk.strip().lower() except Exception as e: - return "" \ No newline at end of file + return "" + +def check_if_status_exists_or_curated(status: str) -> bool: + from api.models import STATUS + return status.upper() if (status and (status.upper() in STATUS.__members__)) else STATUS.CURATED diff --git a/applications/portal/backend/main.py b/applications/portal/backend/main.py index 6edbb9b7..082811c8 100644 --- a/applications/portal/backend/main.py +++ b/applications/portal/backend/main.py @@ -1,6 +1,6 @@ # generated by fastapi-codegen: # filename: openapi.yaml -# timestamp: 2023-12-13T16:50:55+00:00 +# timestamp: 2024-02-13T20:03:17+00:00 from __future__ import annotations @@ -87,6 +87,7 @@ def start_auth_service(): start_auth_service() threading.Thread(target=start_auth_service).start() + # start the kafka event listener when running in/for k8s def start_event_listener(): try: @@ -133,6 +134,7 @@ def get_antibodies( size: Optional[int] = None, updated_from: Optional[datetime] = None, updated_to: Optional[datetime] = None, + status: Optional[str] = None, ) -> PaginatedAntibodies: """ List Antibodies @@ -142,6 +144,7 @@ def get_antibodies( size=size, updated_from=updated_from, updated_to=updated_to, + status=status, ) @@ -157,6 +160,11 @@ def create_antibody(body: AddAntibody) -> None: ) +@prefix_router.get('/antibodies/export', response_model=None, tags=['antibody']) +def get_antibodies_export() -> None: + return antibody_controller.get_antibodies_export() + + @prefix_router.get( '/antibodies/search', response_model=PaginatedAntibodies, tags=['search'] ) diff --git a/applications/portal/backend/openapi/models.py b/applications/portal/backend/openapi/models.py index f26b5c04..a4e438ad 100644 --- a/applications/portal/backend/openapi/models.py +++ b/applications/portal/backend/openapi/models.py @@ -1,6 +1,6 @@ # generated by fastapi-codegen: # filename: openapi.yaml -# timestamp: 2023-09-21T14:25:42+00:00 +# timestamp: 2024-02-13T20:03:17+00:00 from __future__ import annotations diff --git a/applications/portal/backend/portal/constants.py b/applications/portal/backend/portal/constants.py new file mode 100644 index 00000000..23ea3179 --- /dev/null +++ b/applications/portal/backend/portal/constants.py @@ -0,0 +1,121 @@ +# Default KC roles +KC_ADMIN_ROLE = f"portal:administrator" # admin user +KC_MANAGER_ROLE = f"portal:manager" # manager user +KC_USER_ROLE = f"user" # customer user +KC_ALL_ROLES = [ + KC_ADMIN_ROLE, + KC_MANAGER_ROLE, + KC_USER_ROLE, +] +KC_PRIVILEGED_ROLES = [ + KC_ADMIN_ROLE, + KC_MANAGER_ROLE, +] + +KC_DEFAULT_USER_ROLE = None # don't add the user role to the realm default role + +# Database models settings +IMPORT_EXPORT_USE_TRANSACTIONS = True + +# comment refers to max length of column at ingestion time (12/09/2022) +ANTIBODY_NAME_MAX_LEN = 512 # 352 +ANTIBODY_TARGET_MAX_LEN = 1024 # 783 +ANTIBODY_TARGET_SPECIES_MAX_LEN = 4096 # 2047 +VENDOR_MAX_LEN = 512 # 200 +ANTIBODY_CATALOG_NUMBER_MAX_LEN = 256 # 165 +ANTIBODY_CLONALITY_MAX_LEN = 32 +ANTIBODY_CLONE_ID_MAX_LEN = 256 # 150 +URL_MAX_LEN = 2048 # 938 +ANTIGEN_ENTREZ_ID_MAX_LEN = 2048 # 1383 +ANTIBODY_PRODUCT_ISOTYPE_MAX_LEN = 256 # 150 +ANTIBODY_PRODUCT_CONJUGATE_MAX_LEN = 512 # 285 +ANTIBODY_PRODUCT_FORM_MAX_LEN = 1024 # 802 +ANTIBODY_TARGET_SUBREGION_MAX_LEN = 256 # 221 +ANTIBODY_TARGET_MODIFICATION_MAX_LEN = 128 # 67 +ANTIBODY_DEFINING_CITATION_MAX_LEN = 16384 # 9206 +ANTIBODY_DISC_DATE_MAX_LEN = 128 # 85 +ANTIBODY_ID_MAX_LEN = 32 # 8 +ANTIBODY_UID_MAX_LEN = 256 +STATUS_MAX_LEN = 8 +ANTIBODY_CAT_ALT_MAX_LEN = 512 # 334 +VENDOR_COMMERCIAL_TYPE_MAX_LEN = 32 # 10 +ANTIGEN_UNIPROT_ID_MAX_LEN = 255 # 32 +ANTIBODY_TARGET_EPITOPE_MAX_LEN = 1024 # 897 +VENDOR_NIF_MAX_LEN = 32 # 14 +VENDOR_EU_ID_MAX_LEN = 255 +APPLICATION_MAX_LEN = 255 +ANTIBODY_FILE_DISPLAY_NAME_MAX_LEN = 256 +ANTIBODY_FILES_HASH_MAX_LEN = 32 +ANTIBODY_FILE_TYPE_MAX_LEN = 32 + + +ANTIBODY_ANTIBODY_START_SEQ = 3000000 # 2858735 +ANTIBODY_VENDOR_DOMAIN_START_SEQ = 2000 # 813 +ANTIBODY_VENDOR_START_SEQ = 20000 # 12233 +ANTIBODY_FILE_START_SEQ = 10000 # 7001 + +RAW_ANTIBODY_DATA_BASENAME = 'antibody_table' +RAW_VENDOR_DATA_BASENAME = 'antibody_vendors' +RAW_VENDOR_DOMAIN_DATA_BASENAME = 'antibody_vendors_domain' +RAW_USERS_DATA_BASENAME = 'users_antibody' +RAW_ANTIBODY_FILES_BASENAME = 'antibody_files' + +CHUNK_SIZE = 10 ** 4 + +UID_KEY = 'uid' +GUID_KEY = 'guid' + +# Used for data import/export processing +ANTIBODY_HEADER = {'ab_name': "text", 'ab_target': "text", 'target_species': "text", 'vendor': "text", + 'vendor_id': "int", 'catalog_num': "text", 'clonality': "text", + 'source_organism': "text", 'clone_id': "text", 'url': "text", 'link': "text", + 'ab_target_entrez_gid': "text", 'product_isotype': "text", + 'product_conjugate': "text", 'product_form': "text", 'target_subregion': "text", + 'target_modification': "text", 'comments': "text", + 'feedback': "text", 'defining_citation': "text", 'disc_date': "text", 'curator_comment': "text", + 'id': "text", 'ab_id': "text", 'ab_id_old': "text", + 'of_record': "text", 'ix': "int", UID_KEY: "text", 'status': "text", + 'insert_time': "text", 'curate_time': "text", 'cat_alt': "text", 'commercial_type': "text", + 'uniprot_id': "text", 'epitope': "text", 'uid_legacy': "text", 'catalog_num_search': "text"} + +D_TYPES = ANTIBODY_HEADER.copy() +for dt in D_TYPES: + if dt == 'int': + D_TYPES[dt] = 'int64' + else: + D_TYPES[dt] = 'unicode' + +MAX_TRIES = 10 + +ORCID_URL = "https://orcid.org/" +PROVIDER_ID = 'orcid' + +USERS_RELEVANT_HEADER = ['id', GUID_KEY, 'email', 'level', 'firstName', 'middleInitial', 'lastName', 'organization', + 'created', 'orcid_id'] + +UID_INDEX = list(ANTIBODY_HEADER.keys()).index(UID_KEY) +GUID_INDEX = USERS_RELEVANT_HEADER.index(GUID_KEY) +EMAIL_INDEX = USERS_RELEVANT_HEADER.index('email') + +DEFAULT_UID = '43' + +# Search limit for Antibodies for returning without ranking +LIMIT_NUM_RESULTS = 250 + +FOR_NEW_KEY = 'for_new' +FOR_EXTANT_KEY = 'for_extant' +METHOD_KEY = 'method' +IGNORE_KEY = 'ignore' +INSERT_KEY = 'insert' +UPDATE_KEY = 'update' +DUPLICATE_KEY = 'duplicate' +OVERRIDE_KEY = 'override' +FILL_KEY = 'fill' +SKIP_KEY = 'skip' +ID_KEY = 'id' +IX_KEY = 'ix' +KC_USER_ID_KEY = 'keycloak_user' +USER_KEY = 'user' +REMOVE_KEYWORD = 'remove' + +ANTIBODY_PERSISTENCE = 'antibodies' \ No newline at end of file diff --git a/applications/portal/backend/portal/settings.py b/applications/portal/backend/portal/settings.py index 03c7884e..e760149e 100644 --- a/applications/portal/backend/portal/settings.py +++ b/applications/portal/backend/portal/settings.py @@ -153,124 +153,4 @@ # portal specific roles -# Default KC roles -KC_ADMIN_ROLE = f"portal:administrator" # admin user -KC_MANAGER_ROLE = f"portal:manager" # manager user -KC_USER_ROLE = f"user" # customer user -KC_ALL_ROLES = [ - KC_ADMIN_ROLE, - KC_MANAGER_ROLE, - KC_USER_ROLE, -] -KC_PRIVILEGED_ROLES = [ - KC_ADMIN_ROLE, - KC_MANAGER_ROLE, -] - -KC_DEFAULT_USER_ROLE = None # don't add the user role to the realm default role - -# Database models settings -IMPORT_EXPORT_USE_TRANSACTIONS = True - -# comment refers to max length of column at ingestion time (12/09/2022) -ANTIBODY_NAME_MAX_LEN = 512 # 352 -ANTIBODY_TARGET_MAX_LEN = 1024 # 783 -ANTIBODY_TARGET_SPECIES_MAX_LEN = 4096 # 2047 -VENDOR_MAX_LEN = 512 # 200 -ANTIBODY_CATALOG_NUMBER_MAX_LEN = 256 # 165 -ANTIBODY_CLONALITY_MAX_LEN = 32 -ANTIBODY_CLONE_ID_MAX_LEN = 256 # 150 -URL_MAX_LEN = 2048 # 938 -ANTIGEN_ENTREZ_ID_MAX_LEN = 2048 # 1383 -ANTIBODY_PRODUCT_ISOTYPE_MAX_LEN = 256 # 150 -ANTIBODY_PRODUCT_CONJUGATE_MAX_LEN = 512 # 285 -ANTIBODY_PRODUCT_FORM_MAX_LEN = 1024 # 802 -ANTIBODY_TARGET_SUBREGION_MAX_LEN = 256 # 221 -ANTIBODY_TARGET_MODIFICATION_MAX_LEN = 128 # 67 -ANTIBODY_DEFINING_CITATION_MAX_LEN = 16384 # 9206 -ANTIBODY_DISC_DATE_MAX_LEN = 128 # 85 -ANTIBODY_ID_MAX_LEN = 32 # 8 -ANTIBODY_UID_MAX_LEN = 256 -STATUS_MAX_LEN = 8 -ANTIBODY_CAT_ALT_MAX_LEN = 512 # 334 -VENDOR_COMMERCIAL_TYPE_MAX_LEN = 32 # 10 -ANTIGEN_UNIPROT_ID_MAX_LEN = 255 # 32 -ANTIBODY_TARGET_EPITOPE_MAX_LEN = 1024 # 897 -VENDOR_NIF_MAX_LEN = 32 # 14 -VENDOR_EU_ID_MAX_LEN = 255 -APPLICATION_MAX_LEN = 255 -ANTIBODY_FILE_DISPLAY_NAME_MAX_LEN = 256 -ANTIBODY_FILES_HASH_MAX_LEN = 32 -ANTIBODY_FILE_TYPE_MAX_LEN = 32 - - -ANTIBODY_ANTIBODY_START_SEQ = 3000000 # 2858735 -ANTIBODY_VENDOR_DOMAIN_START_SEQ = 2000 # 813 -ANTIBODY_VENDOR_START_SEQ = 20000 # 12233 -ANTIBODY_FILE_START_SEQ = 10000 # 7001 - -RAW_ANTIBODY_DATA_BASENAME = 'antibody_table' -RAW_VENDOR_DATA_BASENAME = 'antibody_vendors' -RAW_VENDOR_DOMAIN_DATA_BASENAME = 'antibody_vendors_domain' -RAW_USERS_DATA_BASENAME = 'users_antibody' -RAW_ANTIBODY_FILES_BASENAME = 'antibody_files' - -CHUNK_SIZE = 10 ** 4 - -UID_KEY = 'uid' -GUID_KEY = 'guid' - -# Used for data import/export processing -ANTIBODY_HEADER = {'ab_name': "text", 'ab_target': "text", 'target_species': "text", 'vendor': "text", - 'vendor_id': "int", 'catalog_num': "text", 'clonality': "text", - 'source_organism': "text", 'clone_id': "text", 'url': "text", 'link': "text", - 'ab_target_entrez_gid': "text", 'product_isotype': "text", - 'product_conjugate': "text", 'product_form': "text", 'target_subregion': "text", - 'target_modification': "text", 'comments': "text", - 'feedback': "text", 'defining_citation': "text", 'disc_date': "text", 'curator_comment': "text", - 'id': "text", 'ab_id': "text", 'ab_id_old': "text", - 'of_record': "text", 'ix': "int", UID_KEY: "text", 'status': "text", - 'insert_time': "text", 'curate_time': "text", 'cat_alt': "text", 'commercial_type': "text", - 'uniprot_id': "text", 'epitope': "text", 'uid_legacy': "text", 'catalog_num_search': "text"} - -D_TYPES = ANTIBODY_HEADER.copy() -for dt in D_TYPES: - if dt == 'int': - D_TYPES[dt] = 'int64' - else: - D_TYPES[dt] = 'unicode' - -MAX_TRIES = 10 - -ORCID_URL = "https://orcid.org/" -PROVIDER_ID = 'orcid' - -USERS_RELEVANT_HEADER = ['id', GUID_KEY, 'email', 'level', 'firstName', 'middleInitial', 'lastName', 'organization', - 'created', 'orcid_id'] - -UID_INDEX = list(ANTIBODY_HEADER.keys()).index(UID_KEY) -GUID_INDEX = USERS_RELEVANT_HEADER.index(GUID_KEY) -EMAIL_INDEX = USERS_RELEVANT_HEADER.index('email') - -DEFAULT_UID = '43' - -# Search limit for Antibodies for returning without ranking -LIMIT_NUM_RESULTS = 250 - -FOR_NEW_KEY = 'for_new' -FOR_EXTANT_KEY = 'for_extant' -METHOD_KEY = 'method' -IGNORE_KEY = 'ignore' -INSERT_KEY = 'insert' -UPDATE_KEY = 'update' -DUPLICATE_KEY = 'duplicate' -OVERRIDE_KEY = 'override' -FILL_KEY = 'fill' -SKIP_KEY = 'skip' -ID_KEY = 'id' -IX_KEY = 'ix' -KC_USER_ID_KEY = 'keycloak_user' -USER_KEY = 'user' -REMOVE_KEYWORD = 'remove' - -ANTIBODY_PERSISTENCE = 'antibodies' \ No newline at end of file +from .constants import * \ No newline at end of file diff --git a/applications/portal/backend/requirements-freeze.txt b/applications/portal/backend/requirements-freeze.txt new file mode 100644 index 00000000..4d8e4ec8 --- /dev/null +++ b/applications/portal/backend/requirements-freeze.txt @@ -0,0 +1,136 @@ +anyio==4.2.0 +argcomplete==2.1.2 +argo-workflows==5.0.0 +asgiref==3.7.2 +attrs==23.2.0 +beautifulsoup4==4.12.3 +black==24.1.1 +blinker==1.7.0 +Brotli @ file:///home/conda/feedstock_root/build_artifacts/brotli-split_1648883617327/work +CacheControl @ file:///home/conda/feedstock_root/build_artifacts/cachecontrol-split_1706895135817/work +cachetools==5.3.2 +certifi @ file:///home/conda/feedstock_root/build_artifacts/certifi_1707022139797/work/certifi +cffi==1.16.0 +chardet==5.2.0 +charset-normalizer @ file:///home/conda/feedstock_root/build_artifacts/charset-normalizer_1698833585322/work +click==8.1.7 +cloudharness @ file:///home/user/ucsd-antibody-registry/cloud-harness/libraries/cloudharness-common +-e git+https://github.com/MetaCell/cloud-harness.git@c958a9e17dd094c1b4990c177ed68536baabda0a#egg=cloudharness_django&subdirectory=infrastructure/common-images/cloudharness-django/libraries/cloudharness-django +-e git+https://github.com/MetaCell/cloud-harness.git@c958a9e17dd094c1b4990c177ed68536baabda0a#egg=cloudharness_model&subdirectory=libraries/models +colorama==0.4.6 +cryptography==42.0.2 +cyclonedx-python-lib @ file:///home/conda/feedstock_root/build_artifacts/cyclonedx-python-lib_1659363848869/work/dist +datamodel-code-generator==0.11.19 +defusedxml==0.7.1 +deprecation==2.1.0 +diff-match-patch==20230430 +Django==4.2.10 +django-admin-extra-buttons==1.5.7 +django-import-export==3.0.1 +dnspython==2.5.0 +ecdsa==0.18.0 +email-validator==2.1.0.post1 +et-xmlfile==1.1.0 +exceptiongroup==1.2.0 +fastapi==0.109.2 +fastapi-code-generator==0.3.5 +filelock==3.13.1 +Flask==3.0.2 +fonttools==4.38.0 +gdown==5.1.0 +genson==1.2.2 +google-auth==2.27.0 +h11==0.14.0 +html5lib @ file:///home/conda/feedstock_root/build_artifacts/html5lib_1592930327044/work +httpcore==1.0.2 +httpx==0.26.0 +idna @ file:///home/conda/feedstock_root/build_artifacts/idna_1701026962277/work +importlib-metadata @ file:///home/conda/feedstock_root/build_artifacts/importlib-metadata_1703269254275/work +inflect==5.6.2 +isodate==0.6.1 +isort==5.13.2 +itsdangerous==2.1.2 +Jinja2==3.1.3 +jsonschema==3.2.0 +kafka-python==2.0.2 +kubernetes==29.0.0 +markdown-it-py @ file:///home/conda/feedstock_root/build_artifacts/markdown-it-py_1686175045316/work +MarkupPy==1.14 +MarkupSafe==2.1.5 +mdurl @ file:///home/conda/feedstock_root/build_artifacts/mdurl_1704317613764/work +msgpack @ file:///opt/conda/conda-bld/msgpack-python_1652362659880/work +mypy-extensions==1.0.0 +nlopt==2.7.1 +numpy==1.26.4 +oauthlib==3.2.2 +odfpy==1.4.1 +openapi-schema-validator==0.1.6 +openapi-spec-validator==0.3.3 +openpyxl==3.1.2 +oyaml==1.0 +packageurl-python @ file:///home/conda/feedstock_root/build_artifacts/packageurl-python_1704811825047/work +packaging @ file:///home/conda/feedstock_root/build_artifacts/packaging_1696202382185/work +pandas==1.4.4 +pathspec==0.12.1 +pillow==10.2.0 +pip-api @ file:///home/conda/feedstock_root/build_artifacts/pip-api_1658274893957/work +pip-requirements-parser @ file:///home/conda/feedstock_root/build_artifacts/pip-requirements-parser_1672265598496/work +pip_audit @ file:///home/conda/feedstock_root/build_artifacts/pip-audit_1683232118275/work +platformdirs==4.2.0 +-e git+ssh://git@github.com/MetaCell/scicrunch-antibody-registry.git@1f4c2ac43ac61b7ddb2cc5b754f9c4f01e5e8120#egg=portal&subdirectory=applications/portal/backend +prance==0.22.2.22.0 +psutil==5.9.4 +psycopg2-binary==2.9.6 +pyaml==23.12.0 +pyasn1==0.5.1 +pyasn1-modules==0.3.0 +pycparser==2.21 +pydantic==1.10.14 +Pygments @ file:///home/conda/feedstock_root/build_artifacts/pygments_1700607939962/work +pyhumps==3.8.0 +PyJWT==2.8.0 +PyOpenGL==3.1.6 +pyparsing @ file:///home/conda/feedstock_root/build_artifacts/pyparsing_1690737849915/work +PyQt5==5.15.7 +PyQt5-Qt5==5.15.2 +PyQt5-sip==12.11.0 +pyrsistent==0.16.1 +PySnooper==0.5.0 +PySocks @ file:///home/conda/feedstock_root/build_artifacts/pysocks_1648857263093/work +python-dateutil==2.8.2 +python-jose==3.3.0 +python-keycloak==3.7.0 +pytz==2024.1 +PyYAML==6.0.1 +requests @ file:///home/conda/feedstock_root/build_artifacts/requests_1684774241324/work +requests-oauthlib==1.3.1 +requests-toolbelt==1.0.0 +rich @ file:///home/conda/feedstock_root/build_artifacts/rich-split_1700160075651/work/dist +rsa==4.9 +ruamel.yaml==0.18.6 +ruamel.yaml.clib==0.2.8 +sentry-sdk==1.40.3 +shellingham==1.5.4 +six @ file:///home/conda/feedstock_root/build_artifacts/six_1620240208055/work +sniffio==1.3.0 +sortedcontainers @ file:///home/conda/feedstock_root/build_artifacts/sortedcontainers_1621217038088/work +soupsieve==2.5 +sqlparse==0.4.4 +starlette==0.36.3 +stringcase==1.2.0 +swagger_ui_bundle==1.1.0 +tablib==3.5.0 +toml @ file:///home/conda/feedstock_root/build_artifacts/toml_1604308577558/work +tomli==2.0.1 +tqdm==4.66.1 +typed-ast==1.5.5 +typer==0.4.2 +typing_extensions @ file:///home/conda/feedstock_root/build_artifacts/typing_extensions_1702176139754/work +urllib3 @ file:///home/conda/feedstock_root/build_artifacts/urllib3_1706638047111/work +uvicorn==0.17.6 +webencodings @ file:///home/conda/feedstock_root/build_artifacts/webencodings_1694681268211/work +websocket-client==1.7.0 +Werkzeug==3.0.1 +xlrd==2.0.1 +xlwt==1.3.0 +zipp @ file:///home/conda/feedstock_root/build_artifacts/zipp_1695255097490/work diff --git a/applications/portal/backend/requirements.txt b/applications/portal/backend/requirements.txt index 51dd69c5..c757e3b7 100644 --- a/applications/portal/backend/requirements.txt +++ b/applications/portal/backend/requirements.txt @@ -1,4 +1,9 @@ gdown>=4.6.0 pandas==1.4.4 django-import-export==3.0.1 -psycopg2-binary==2.9.6 \ No newline at end of file +psycopg2-binary==2.9.6 + +# Security +starlette>=0.36.2 +pillow >= 10.2.0 +fastapi >= 0.109.1 \ No newline at end of file diff --git a/applications/portal/backend/static/www/.gitignore b/applications/portal/backend/static/www/.gitignore new file mode 100644 index 00000000..16f2dc5f --- /dev/null +++ b/applications/portal/backend/static/www/.gitignore @@ -0,0 +1 @@ +*.csv \ No newline at end of file diff --git a/applications/portal/deploy/values-dev.yaml b/applications/portal/deploy/values-dev.yaml index c9d548ad..f9fb14f4 100644 --- a/applications/portal/deploy/values-dev.yaml +++ b/applications/portal/deploy/values-dev.yaml @@ -3,3 +3,4 @@ harness: replicas: 1 database: size: 20Gi + diff --git a/applications/portal/deploy/values.yaml b/applications/portal/deploy/values.yaml index c20fae1d..577d5afa 100644 --- a/applications/portal/deploy/values.yaml +++ b/applications/portal/deploy/values.yaml @@ -6,7 +6,7 @@ harness: auto: true deployment: auto: true - replicas: 3 + replicas: 2 port: 8080 livenessProbe: path: /api/live @@ -14,7 +14,6 @@ harness: path: /api/ready dependencies: build: - - cloudharness-frontend-build - cloudharness-django hard: - common @@ -60,7 +59,6 @@ harness: cpu: 200m limits: memory: 4Gi - cpu: 8 accounts: roles: - administrator @@ -69,3 +67,5 @@ harness: test: e2e: enabled: true + +export_query: "SELECT 'AB_' || ab_id as rrid, ab_name, catalog_num, cat_alt, api_vendor.vendor as vendor_name, api_vendor.vendor || ' Cat# ' || catalog_num || ', RRID:AB_' || ab_id AS proper_citation from api_antibody LEFT JOIN api_vendor on api_vendor.id = vendor_id WHERE true" \ No newline at end of file diff --git a/applications/portal/frontend/package.json b/applications/portal/frontend/package.json index 1e362115..cb1718cc 100644 --- a/applications/portal/frontend/package.json +++ b/applications/portal/frontend/package.json @@ -22,8 +22,8 @@ "@mui/material": "^5.10.6", "@mui/styles": "^5.10.6", "@mui/x-data-grid": "^5.14.0", - "axios": "^0.21.4", - "formik": "^2.2.9", + "axios": "^1.6.7", + "formik": "^2.4.5", "keycloak-js": "^19.0.1", "lodash.debounce": "^4.0.8", "pluralize": "^8.0.0", @@ -33,9 +33,9 @@ "react-gtm-module": "^2.0.11", "react-router": "^5.0.0", "react-router-dom": "^5.0.0", - "react-slick": "^0.29.0", + "react-slick": "^0.30.0", "slick-carousel": "^1.8.1", - "use-clipboard-copy": "^0.1.2", + "use-clipboard-copy": "^0.2.0", "yup": "^0.32.11" }, "devDependencies": { @@ -45,12 +45,11 @@ "@babel/preset-react": "^7.8.3", "babel-eslint": "^10.1.0", "babel-loader": "^8.0.6", - "babel-plugin-module-resolver": "^4.0.0", + "babel-plugin-module-resolver": "^5.0.0", "babel-preset-minify": "^0.5.1", "clean-webpack-plugin": "^3.0.0", "compression-webpack-plugin": "^7.1.2", "copy-webpack-plugin": "^6.2.1", - "css-loader": "^5.2.4", "dotenv": "^16.0.2", "eslint": "5.16.0", "eslint-plugin-jest": "^23.8.2", @@ -59,17 +58,14 @@ "file-loader": "^5.0.2", "html-loader": "^0.5.5", "html-webpack-plugin": "^5.5.0", - "image-webpack-loader": "^8.1.0", - "less": "^3.10.3", - "less-loader": "^6.1.2", - "less-vars-to-js": "^1.3.0", - "raw-loader": "^4.0.2", - "style-loader": "^1.1.3", "ts-loader": "^9.0.0", "typescript": "^4.8.3", "webpack": "^5.61.0", "webpack-cli": "^4.6.0", "webpack-dev-server": "^4.5.0", "webpack-merge": "^5.7.0" + }, + "resolutions": { + "graceful-fs": "^4.2.11" } } diff --git a/applications/portal/frontend/src/App.tsx b/applications/portal/frontend/src/App.tsx index 065ade44..2900e319 100644 --- a/applications/portal/frontend/src/App.tsx +++ b/applications/portal/frontend/src/App.tsx @@ -1,5 +1,4 @@ import React, { useState } from "react"; -import "./styles/style.less"; import { CssBaseline } from "@mui/material"; import { ThemeProvider } from "@mui/material/styles"; import theme from "./theme/Theme"; diff --git a/applications/portal/frontend/src/assets/partners/Boster_Biological-logo.png b/applications/portal/frontend/src/assets/partners/Boster_Biological-logo.png new file mode 100644 index 00000000..3697308b Binary files /dev/null and b/applications/portal/frontend/src/assets/partners/Boster_Biological-logo.png differ diff --git a/applications/portal/frontend/src/assets/partners/Fujirebio.jpeg b/applications/portal/frontend/src/assets/partners/Fujirebio.jpeg new file mode 100644 index 00000000..1d820e4c Binary files /dev/null and b/applications/portal/frontend/src/assets/partners/Fujirebio.jpeg differ diff --git a/applications/portal/frontend/src/assets/partners/HUABIO.png b/applications/portal/frontend/src/assets/partners/HUABIO.png new file mode 100644 index 00000000..86ec75d9 Binary files /dev/null and b/applications/portal/frontend/src/assets/partners/HUABIO.png differ diff --git a/applications/portal/frontend/src/assets/partners/Institute_for_Protein_Innovation.png b/applications/portal/frontend/src/assets/partners/Institute_for_Protein_Innovation.png new file mode 100644 index 00000000..769af555 Binary files /dev/null and b/applications/portal/frontend/src/assets/partners/Institute_for_Protein_Innovation.png differ diff --git a/applications/portal/frontend/src/assets/partners/NanoTag.jpg b/applications/portal/frontend/src/assets/partners/NanoTag.jpg new file mode 100644 index 00000000..fa22284f Binary files /dev/null and b/applications/portal/frontend/src/assets/partners/NanoTag.jpg differ diff --git a/applications/portal/frontend/src/assets/partners/antibodies_incorporated.jpg b/applications/portal/frontend/src/assets/partners/antibodies_incorporated.jpg new file mode 100644 index 00000000..29378b24 Binary files /dev/null and b/applications/portal/frontend/src/assets/partners/antibodies_incorporated.jpg differ diff --git a/applications/portal/frontend/src/assets/partners/fujifilm-wako-chemicals-usa-corporation-logo-vector.png b/applications/portal/frontend/src/assets/partners/fujifilm-wako-chemicals-usa-corporation-logo-vector.png new file mode 100644 index 00000000..f815cb0b Binary files /dev/null and b/applications/portal/frontend/src/assets/partners/fujifilm-wako-chemicals-usa-corporation-logo-vector.png differ diff --git a/applications/portal/frontend/src/assets/partners/partners logos/Boster_Biological-logo.png b/applications/portal/frontend/src/assets/partners/partners logos/Boster_Biological-logo.png new file mode 100644 index 00000000..3697308b Binary files /dev/null and b/applications/portal/frontend/src/assets/partners/partners logos/Boster_Biological-logo.png differ diff --git a/applications/portal/frontend/src/assets/partners/partners logos/Fujirebio.jpeg b/applications/portal/frontend/src/assets/partners/partners logos/Fujirebio.jpeg new file mode 100644 index 00000000..1d820e4c Binary files /dev/null and b/applications/portal/frontend/src/assets/partners/partners logos/Fujirebio.jpeg differ diff --git a/applications/portal/frontend/src/assets/partners/partners logos/HUABIO.png b/applications/portal/frontend/src/assets/partners/partners logos/HUABIO.png new file mode 100644 index 00000000..86ec75d9 Binary files /dev/null and b/applications/portal/frontend/src/assets/partners/partners logos/HUABIO.png differ diff --git a/applications/portal/frontend/src/assets/partners/partners logos/Institute for Protein Innovation.png b/applications/portal/frontend/src/assets/partners/partners logos/Institute for Protein Innovation.png new file mode 100644 index 00000000..769af555 Binary files /dev/null and b/applications/portal/frontend/src/assets/partners/partners logos/Institute for Protein Innovation.png differ diff --git a/applications/portal/frontend/src/assets/partners/partners logos/antibodies_incorporated.jpg b/applications/portal/frontend/src/assets/partners/partners logos/antibodies_incorporated.jpg new file mode 100644 index 00000000..29378b24 Binary files /dev/null and b/applications/portal/frontend/src/assets/partners/partners logos/antibodies_incorporated.jpg differ diff --git a/applications/portal/frontend/src/assets/partners/partners logos/fujifilm-wako-chemicals-usa-corporation-logo-vector.png b/applications/portal/frontend/src/assets/partners/partners logos/fujifilm-wako-chemicals-usa-corporation-logo-vector.png new file mode 100644 index 00000000..f815cb0b Binary files /dev/null and b/applications/portal/frontend/src/assets/partners/partners logos/fujifilm-wako-chemicals-usa-corporation-logo-vector.png differ diff --git a/applications/portal/frontend/src/assets/partners/partners logos/nanotag_biotechnologies_logo.jpeg b/applications/portal/frontend/src/assets/partners/partners logos/nanotag_biotechnologies_logo.jpeg new file mode 100644 index 00000000..04b373b8 Binary files /dev/null and b/applications/portal/frontend/src/assets/partners/partners logos/nanotag_biotechnologies_logo.jpeg differ diff --git a/applications/portal/frontend/src/components/About.tsx b/applications/portal/frontend/src/components/About.tsx index e515298e..f8e8bb02 100644 --- a/applications/portal/frontend/src/components/About.tsx +++ b/applications/portal/frontend/src/components/About.tsx @@ -4,8 +4,7 @@ import { Box, Button, Container, Divider, Grid, Link, Typography } from '@mui/ma import Slider from "react-slick"; import { vars } from "../theme/variables"; import { useHistory } from 'react-router-dom'; -import "slick-carousel/slick/slick.css"; -import "slick-carousel/slick/slick-theme.css"; + const { footerBg, whiteColor, sepratorColor, primaryColor, contentBg, contentBorderColor, primaryTextColor, bannerHeadingColor } = vars; const styles = { @@ -191,6 +190,13 @@ const About = () => { { name: 'Niels Danbolt University of Oslo', url: 'https://www.uio.no/', img: './assets/partners/University_of_Oslo.png' }, { name: 'NIH', url: 'https://www.nhpreagents.org/', img: './assets/partners/NIH.png' }, { name: 'ichorbio', url: 'https://ichor.bio/', img: './assets/partners/ichorbio.png' }, + { name: 'Antibodies Incorporated', url: 'https://www.antibodiesinc.com/', img: './assets/partners/antibodies_incorporated.jpg' }, + { name: 'HUABIO', url: 'https://www.huabio.com/', img: './assets/partners/HUABIO.png' }, + { name: 'NanoTag', url: 'https://nano-tag.com/', img: './assets/partners/NanoTag.jpg' }, + { name: 'Institute for Protein Innovation', url: 'https://proteininnovation.org/', img: './assets/partners/Institute_for_Protein_Innovation.png' }, + { name: 'Fujirebio', url: 'https://www.fujirebio.com/en', img: './assets/partners/Fujirebio.jpeg' }, + { name: 'Boster Biological Technology', url: 'https://www.bosterbio.com/', img: './assets/partners/Boster_Biological-logo.png' }, + { name: 'Fujifilm Wako USA', url: 'https://wakousa.com/', img: './assets/partners/fujifilm-wako-chemicals-usa-corporation-logo-vector.png' } ] const history = useHistory(); const navigate = () => history.push('/'); @@ -212,7 +218,7 @@ const About = () => { - We would like to thank our partners, who submit data to us regularly making author's jobs easier. Would you like to become a partner? Inquire here. + We would like to thank our partners, who submit data to us regularly making author's jobs easier. Would you like to become a partner? Inquire here. @@ -287,7 +293,7 @@ const About = () => { - We never delete records, so even when an antibody disappears from a vendor's catalog, or is sold to another vendor, we can trace the provenance of that antibody. (Bandrowski et al). + We never delete records, so even when an antibody disappears from a vendor's catalog, or is sold to another vendor, we can trace the provenance of that antibody. (Bandrowski et al). diff --git a/applications/portal/frontend/src/components/NavBar/Navbar.tsx b/applications/portal/frontend/src/components/NavBar/Navbar.tsx index d62c0a58..56a0d192 100644 --- a/applications/portal/frontend/src/components/NavBar/Navbar.tsx +++ b/applications/portal/frontend/src/components/NavBar/Navbar.tsx @@ -15,7 +15,7 @@ import NavLinks from "./NavLinks"; import HelpMenu from "./HelpMenu"; import { UserContext, User } from "../../services/UserService"; import UserAccountMenu from "./UserAccountMenu"; -import logo from "../../assets/logo.svg"; + const Navbar = () => { const user: User = React.useContext(UserContext)[0]; @@ -45,7 +45,7 @@ const Navbar = () => { > - + + + +