diff --git a/alembic/versions/318dcd16a600_dns.py b/alembic/versions/318dcd16a600_dns.py new file mode 100644 index 0000000..1179e38 --- /dev/null +++ b/alembic/versions/318dcd16a600_dns.py @@ -0,0 +1,35 @@ +"""dns + +Revision ID: 318dcd16a600 +Revises: 260e33819b5e +Create Date: 2024-06-17 18:13:43.120105 + +""" +from alembic import op +import sqlalchemy as sa + +import geoalchemy2 + +# revision identifiers, used by Alembic. +revision = '318dcd16a600' +down_revision = '260e33819b5e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('unit', sa.Column('basis_of_record', sa.String(length=50), nullable=True)) + op.add_column('unit', sa.Column('material_entity_id', sa.String(length=500), nullable=True)) + op.add_column('unit', sa.Column('dna_sequence', sa.Text(), nullable=True)) + op.add_column('unit', sa.Column('sequencer', sa.String(length=500), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('unit', 'sequencer') + op.drop_column('unit', 'dna_sequence') + op.drop_column('unit', 'material_entity_id') + op.drop_column('unit', 'basis_of_record') + # ### end Alembic commands ### diff --git a/alembic/versions/3785ccd6eb37_site_rm_code.py b/alembic/versions/3785ccd6eb37_site_rm_code.py new file mode 100644 index 0000000..4801e31 --- /dev/null +++ b/alembic/versions/3785ccd6eb37_site_rm_code.py @@ -0,0 +1,29 @@ +"""site-rm-code + +Revision ID: 3785ccd6eb37 +Revises: 3da76abfc432 +Create Date: 2024-06-18 05:19:48.455171 + +""" +from alembic import op +import sqlalchemy as sa + +import geoalchemy2 + +# revision identifiers, used by Alembic. +revision = '3785ccd6eb37' +down_revision = '3da76abfc432' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('site', 'code') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('site', sa.Column('code', sa.VARCHAR(length=500), autoincrement=False, nullable=True)) + # ### end Alembic commands ### diff --git a/alembic/versions/3da76abfc432_site_schema.py b/alembic/versions/3da76abfc432_site_schema.py new file mode 100644 index 0000000..6c1b905 --- /dev/null +++ b/alembic/versions/3da76abfc432_site_schema.py @@ -0,0 +1,61 @@ +"""site-schema + +Revision ID: 3da76abfc432 +Revises: 4c228755cffb +Create Date: 2024-06-18 05:09:31.292914 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql +import geoalchemy2 + +# revision identifiers, used by Alembic. +revision = '3da76abfc432' +down_revision = '4c228755cffb' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('organization', 'domain') + op.drop_column('organization', 'is_site') + op.drop_column('organization', 'settings') + op.drop_column('organization', 'subdomain') + op.drop_column('organization', 'description') + op.drop_column('organization', 'ark_nma') + op.drop_column('organization', 'logo_url') + op.add_column('site', sa.Column('title', sa.String(length=500), nullable=True)) + op.add_column('site', sa.Column('title_en', sa.String(length=500), nullable=True)) + op.add_column('site', sa.Column('logo_url', sa.String(length=500), nullable=True)) + op.add_column('site', sa.Column('description', sa.Text(), nullable=True)) + op.alter_column('site', 'name', + existing_type=sa.VARCHAR(length=500), + type_=sa.String(length=50), + existing_nullable=True) + op.drop_column('site', 'name_en') + op.drop_column('site', 'short_name') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('site', sa.Column('short_name', sa.VARCHAR(length=500), autoincrement=False, nullable=True)) + op.add_column('site', sa.Column('name_en', sa.VARCHAR(length=500), autoincrement=False, nullable=True)) + op.alter_column('site', 'name', + existing_type=sa.String(length=50), + type_=sa.VARCHAR(length=500), + existing_nullable=True) + op.drop_column('site', 'description') + op.drop_column('site', 'logo_url') + op.drop_column('site', 'title_en') + op.drop_column('site', 'title') + op.add_column('organization', sa.Column('logo_url', sa.VARCHAR(length=500), autoincrement=False, nullable=True)) + op.add_column('organization', sa.Column('ark_nma', sa.VARCHAR(length=500), autoincrement=False, nullable=True)) + op.add_column('organization', sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('organization', sa.Column('subdomain', sa.VARCHAR(length=100), autoincrement=False, nullable=True)) + op.add_column('organization', sa.Column('settings', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=True)) + op.add_column('organization', sa.Column('is_site', sa.BOOLEAN(), autoincrement=False, nullable=True)) + op.add_column('organization', sa.Column('domain', sa.VARCHAR(length=500), autoincrement=False, nullable=True)) + # ### end Alembic commands ### diff --git a/alembic/versions/4c228755cffb_host.py b/alembic/versions/4c228755cffb_host.py new file mode 100644 index 0000000..124a753 --- /dev/null +++ b/alembic/versions/4c228755cffb_host.py @@ -0,0 +1,31 @@ +"""host + +Revision ID: 4c228755cffb +Revises: aef30cda76fc +Create Date: 2024-06-18 02:51:58.696708 + +""" +from alembic import op +import sqlalchemy as sa + +import geoalchemy2 + +# revision identifiers, used by Alembic. +revision = '4c228755cffb' +down_revision = 'aef30cda76fc' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('site', sa.Column('host', sa.String(length=500), nullable=True)) + op.drop_column('site', 'domain') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('site', sa.Column('domain', sa.VARCHAR(length=500), autoincrement=False, nullable=True)) + op.drop_column('site', 'host') + # ### end Alembic commands ### diff --git a/alembic/versions/54844ef10da8_collection_site_id.py b/alembic/versions/54844ef10da8_collection_site_id.py new file mode 100644 index 0000000..97f12ef --- /dev/null +++ b/alembic/versions/54844ef10da8_collection_site_id.py @@ -0,0 +1,31 @@ +"""collection-site-id + +Revision ID: 54844ef10da8 +Revises: c80f91a5fba6 +Create Date: 2024-06-19 10:11:44.863294 + +""" +from alembic import op +import sqlalchemy as sa + +import geoalchemy2 + +# revision identifiers, used by Alembic. +revision = '54844ef10da8' +down_revision = 'c80f91a5fba6' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('collection', sa.Column('site_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'collection', 'site', ['site_id'], ['id'], ondelete='SET NULL') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'collection', type_='foreignkey') + op.drop_column('collection', 'site_id') + # ### end Alembic commands ### diff --git a/alembic/versions/aef30cda76fc_site_id.py b/alembic/versions/aef30cda76fc_site_id.py new file mode 100644 index 0000000..5eb89fe --- /dev/null +++ b/alembic/versions/aef30cda76fc_site_id.py @@ -0,0 +1,67 @@ +"""site_id + +Revision ID: aef30cda76fc +Revises: b5a42e695eb4 +Create Date: 2024-06-18 02:48:05.528842 + +""" +from alembic import op +import sqlalchemy as sa + +import geoalchemy2 + +# revision identifiers, used by Alembic. +revision = 'aef30cda76fc' +down_revision = 'b5a42e695eb4' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('article', sa.Column('site_id', sa.Integer(), nullable=True)) + op.drop_constraint('article_organization_id_fkey', 'article', type_='foreignkey') + op.create_foreign_key(None, 'article', 'site', ['site_id'], ['id'], ondelete='SET NULL') + op.drop_column('article', 'organization_id') + op.add_column('article_category', sa.Column('site_id', sa.Integer(), nullable=True)) + op.drop_constraint('article_category_organization_id_fkey', 'article_category', type_='foreignkey') + op.create_foreign_key(None, 'article_category', 'site', ['site_id'], ['id'], ondelete='SET NULL') + op.drop_column('article_category', 'organization_id') + op.add_column('related_link', sa.Column('site_id', sa.Integer(), nullable=True)) + op.drop_constraint('related_link_organization_id_fkey', 'related_link', type_='foreignkey') + op.create_foreign_key(None, 'related_link', 'site', ['site_id'], ['id'], ondelete='SET NULL') + op.drop_column('related_link', 'organization_id') + op.add_column('related_link_category', sa.Column('site_id', sa.Integer(), nullable=True)) + op.drop_constraint('related_link_category_organization_id_fkey', 'related_link_category', type_='foreignkey') + op.create_foreign_key(None, 'related_link_category', 'site', ['site_id'], ['id'], ondelete='SET NULL') + op.drop_column('related_link_category', 'organization_id') + op.add_column('user', sa.Column('site_id', sa.Integer(), nullable=True)) + op.drop_constraint('user_organization_id_fkey', 'user', type_='foreignkey') + op.create_foreign_key(None, 'user', 'site', ['site_id'], ['id'], ondelete='SET NULL') + op.drop_column('user', 'organization_id') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('organization_id', sa.INTEGER(), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'user', type_='foreignkey') + op.create_foreign_key('user_organization_id_fkey', 'user', 'organization', ['organization_id'], ['id'], ondelete='SET NULL') + op.drop_column('user', 'site_id') + op.add_column('related_link_category', sa.Column('organization_id', sa.INTEGER(), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'related_link_category', type_='foreignkey') + op.create_foreign_key('related_link_category_organization_id_fkey', 'related_link_category', 'organization', ['organization_id'], ['id'], ondelete='SET NULL') + op.drop_column('related_link_category', 'site_id') + op.add_column('related_link', sa.Column('organization_id', sa.INTEGER(), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'related_link', type_='foreignkey') + op.create_foreign_key('related_link_organization_id_fkey', 'related_link', 'organization', ['organization_id'], ['id'], ondelete='SET NULL') + op.drop_column('related_link', 'site_id') + op.add_column('article_category', sa.Column('organization_id', sa.INTEGER(), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'article_category', type_='foreignkey') + op.create_foreign_key('article_category_organization_id_fkey', 'article_category', 'organization', ['organization_id'], ['id'], ondelete='SET NULL') + op.drop_column('article_category', 'site_id') + op.add_column('article', sa.Column('organization_id', sa.INTEGER(), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'article', type_='foreignkey') + op.create_foreign_key('article_organization_id_fkey', 'article', 'organization', ['organization_id'], ['id'], ondelete='SET NULL') + op.drop_column('article', 'site_id') + # ### end Alembic commands ### diff --git a/alembic/versions/b5a42e695eb4_site_dep.py b/alembic/versions/b5a42e695eb4_site_dep.py new file mode 100644 index 0000000..e8fc34c --- /dev/null +++ b/alembic/versions/b5a42e695eb4_site_dep.py @@ -0,0 +1,41 @@ +"""site-dep + +Revision ID: b5a42e695eb4 +Revises: 318dcd16a600 +Create Date: 2024-06-18 02:38:08.714498 + +""" +from alembic import op +import sqlalchemy as sa + +import geoalchemy2 + +# revision identifiers, used by Alembic. +revision = 'b5a42e695eb4' +down_revision = '318dcd16a600' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('site', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=500), nullable=True), + sa.Column('name_en', sa.String(length=500), nullable=True), + sa.Column('short_name', sa.String(length=500), nullable=True), + sa.Column('code', sa.String(length=500), nullable=True), + sa.Column('domain', sa.String(length=500), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.add_column('organization', sa.Column('site_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'organization', 'site', ['site_id'], ['id'], ondelete='SET NULL') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'organization', type_='foreignkey') + op.drop_column('organization', 'site_id') + op.drop_table('site') + # ### end Alembic commands ### diff --git a/alembic/versions/c80f91a5fba6_site_data.py b/alembic/versions/c80f91a5fba6_site_data.py new file mode 100644 index 0000000..dc316b8 --- /dev/null +++ b/alembic/versions/c80f91a5fba6_site_data.py @@ -0,0 +1,29 @@ +"""site.data + +Revision ID: c80f91a5fba6 +Revises: 3785ccd6eb37 +Create Date: 2024-06-18 08:42:35.128037 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql +import geoalchemy2 + +# revision identifiers, used by Alembic. +revision = 'c80f91a5fba6' +down_revision = '3785ccd6eb37' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('site', sa.Column('data', postgresql.JSONB(astext_type=sa.Text()), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('site', 'data') + # ### end Alembic commands ### diff --git a/app/application.py b/app/application.py index fe65204..2488c78 100644 --- a/app/application.py +++ b/app/application.py @@ -31,7 +31,7 @@ from app.models.site import ( User, - Organization, + Site, ) from app.utils import find_date @@ -57,20 +57,14 @@ ''' def apply_blueprints(app): from app.blueprints.base import base as base_bp - from app.blueprints.frontend import frontend as frontend_bp - #from app.blueprints.data import data as data_bp - #from app.blueprints.main import main as main_bp - #from app.blueprints.page import page as page_bp + from app.blueprints.frontpage import frontpage as frontpage_bp from app.blueprints.admin import admin as admin_bp; from app.blueprints.api import api as api_bp; app.register_blueprint(base_bp) - app.register_blueprint(frontend_bp) - #app.register_blueprint(main_bp) - #app.register_blueprint(page_bp) + app.register_blueprint(frontpage_bp) app.register_blueprint(admin_bp, url_prefix='/admin') app.register_blueprint(api_bp, url_prefix='/api/v1') - #app.register_blueprint(data_bp) def get_locale(): #print(request.cookies.get('language'), flush=True) @@ -152,16 +146,17 @@ def create_app(): def load_user(id): return User.query.get(id) + @flask_app.route('/') def cover(): - domain = request.headers.get('Host', '') - if domain == os.getenv('PORTAL_SITE'): + host = request.headers.get('Host', '') + if host == os.getenv('PORTAL_SITE'): return render_template('cover.html') - if site := Organization.get_site(domain): - return redirect(url_for('frontend.news', lang_code='zh')) + if site := Site.find_by_host(host): + return redirect(url_for('frontpage.index', lang_code='zh')) - return abort(404) + return 'naturedb: no site' #abort(404) @flask_app.route('/import') def import_data(): diff --git a/app/blueprints/api.py b/app/blueprints/api.py index c5de1c7..01977ae 100644 --- a/app/blueprints/api.py +++ b/app/blueprints/api.py @@ -13,6 +13,7 @@ jsonify, redirect, url_for, + current_app, ) from flask.views import MethodView from sqlalchemy import ( @@ -40,6 +41,7 @@ from app.database import session from app.models.site import ( User, + Site, ) from app.models.collection import ( Record, @@ -149,13 +151,45 @@ def get_search(): 'range': json.loads(request.args.get('range')) if request.args.get('range') else [0, 20], } + useSourceData = False + stmt = make_specimen_query(payload['filter']) - res = session.query(Collection.id).where(Collection.organization_id==1).all() - collection_ids = [x[0] for x in res] - stmt = stmt.where(Unit.collection_id.in_(collection_ids)) # only get HAST - #print(payload['filter'], '====', flush=True) - #print('[SEARCH]', stmt, flush=True) + # strict collection + available_collection_ids = [] + site_collection_ids = [] + if host := request.headers.get('Host'): + site = Site.find_by_host(host) + site_collection_ids = [x.id for x in site.collections] + + if filter_collection_id := payload['filter'].get('collection_id'): + if isinstance(filter_collection_id, list): + available_collection_ids = set(site_collection_ids) & set(filter_collection_id) + elif int(filter_collection_ids) in site_collection_ids: + available_collection_ids = set(site_collection_ids) & set(filter_collection_id) + + stmt = stmt.where(Unit.collection_id.in_(available_collection_ids)) + + #current_app.logger.debug(stmt) + + if sd := payload['filter'].get('sourceData'): + useSourceData = True + if sd.get('annotate'): + if count_fields := sd['annotate'].get('values'): + fields = [Record.source_data[x] for x in count_fields] + stmt = stmt.group_by(*fields) + + if sd.get('count'): + subquery = stmt.subquery() + stmt = select(func.count()).select_from(subquery) + data = session.execute(stmt).scalar() + else: + result= session.execute(stmt) + data = [list(x) for x in result] + + if sd.get('annotate') or sd.get('count'): + return jsonify({'data': data, 'debug_stmt': str(stmt)}) + base_stmt = stmt ## sort @@ -208,6 +242,10 @@ def get_search(): subquery = base_stmt.subquery() count_stmt = select(func.count()).select_from(subquery) total = session.execute(count_stmt).scalar() + + if payload['filter'].get('count'): + return jsonify({'count': total}) + elapsed_count = time.time() - begin_time # -------------- @@ -238,9 +276,9 @@ def get_search(): if record.proxy_taxon_common_name: taxon_text = f'{record.proxy_taxon_scientific_name} ({record.proxy_taxon_common_name})' if not view or view == 'table': - data.append({ + d = { 'unit_id': unit.id if unit else '', - 'collection_id': record.id, + 'record_id': record.id, 'record_key': f'u{unit.id}' if unit else f'c{record.id}', # 'accession_number': unit.accession_number if unit else '', 'accession_number': unit.accession_number if unit else '', @@ -257,7 +295,13 @@ def get_search(): 'longitude_decimal': record.longitude_decimal, 'latitude_decimal': record.latitude_decimal, 'type_status': unit.type_status if unit and (unit.type_status and unit.pub_status=='P' and unit.type_is_published is True) else '', - }) + } + + if useSourceData: + d['source_data'] = record.source_data + + data.append(d) + elif view == 'map': if record.longitude_decimal and record.latitude_decimal: data.append({ @@ -810,3 +854,23 @@ def api_create_admin_record(collection_id): }) else: return jsonify({'message': 'ok'}) + +@api.route('/collections//raw') +def get_collection_raw_list(collection_id): + #print(collection_id, flush=True) + if c := session.get(Collection, collection_id): + rows = Record.query.filter(Record.collection_id==collection_id).limit(10).all() + print(rows, flush=True) + return jsonify({'result': 'ok'}) + return jsonify({'foo', 'bar'}) + + +@api.route('/collections//records//raw') +def get_collection_raw_detail(collection_id, record_id): + #print(collection_id, flush=True) + if c := session.get(Collection, collection_id): + #rows = Record.query.filter(Record.collection_id==collection_id).limit(10).all() + if r := session.get(Record, record_id): + return jsonify({'raw': r.source_data}) + + return jsonify({'foo', 'bar'}) diff --git a/app/blueprints/frontend.py b/app/blueprints/frontpage.py similarity index 54% rename from app/blueprints/frontend.py rename to app/blueprints/frontpage.py index 0492f3f..5b91efa 100644 --- a/app/blueprints/frontend.py +++ b/app/blueprints/frontpage.py @@ -11,6 +11,7 @@ url_for, current_app, ) +from jinja2.exceptions import TemplateNotFound from sqlalchemy import ( desc, func, @@ -22,7 +23,7 @@ from app.models.site import ( Article, - Organization, + Site, ) from app.models.collection import ( Unit, @@ -40,7 +41,6 @@ ) from app.helpers import ( get_current_site, - get_or_set_type_specimens, ) from app.helpers_query import ( make_specimen_query, @@ -48,11 +48,11 @@ from app.config import Config #frontend = Blueprint('frontend', __name__, url_prefix='/') -frontend = Blueprint('frontend', __name__) +frontpage = Blueprint('frontpage', __name__) DEFAULT_LANG_CODE = Config.DEFAULT_LANG_CODE -@frontend.url_defaults +@frontpage.url_defaults def add_language_code(endpoint, values): #print('add code', endpoint, values, flush=True) if 'lang_code' in values or not 'lang_code' in g: @@ -65,7 +65,7 @@ def add_language_code(endpoint, values): #print('expect', current_app.url_map.is_endpoint_expecting(endpoint, 'lang_code'), flush=True) -@frontend.url_value_preprocessor +@frontpage.url_value_preprocessor def pull_lang_code(endpoint, values): #print('pull code', endpoint, values, request.path, flush=True) lang_code = values.get('lang_code') @@ -83,59 +83,64 @@ def pull_lang_code(endpoint, values): g.site = site -@frontend.route('/news', defaults={'lang_code': DEFAULT_LANG_CODE}) -@frontend.route('//news') -def news(lang_code): - site = g.site - if site.id == 1: - articles = [x.to_dict() for x in Article.query.filter(Article.organization_id==site.id).order_by(Article.publish_date.desc()).limit(10).all()] - #units = Unit.query.filter(Unit.accession_number!='').order_by(func.random()).limit(4).all() - units = [] - stmt = select(Unit.id).where(Unit.accession_number!='', Collection.organization_id==site.id).join(Record).join(Collection).order_by(func.random()).limit(4) - - results = session.execute(stmt) - for i in results.all(): - u = session.get(Unit, int(i[0])) - units.append(u) - return render_template('index.html', articles=articles, units=units) - else: - return render_template('index-other.html') +@frontpage.route('/', defaults={'lang_code': DEFAULT_LANG_CODE}) +@frontpage.route('/') +def index(lang_code): + #current_app.logger.debug(f'{g.site.name}, {lang_code}') + try: + return render_template(f'sites/{g.site.name}/index.html') + except TemplateNotFound: + return render_template('index.html') -@frontend.route('/page/', defaults={'lang_code': DEFAULT_LANG_CODE}) -@frontend.route('//page/') -def page(lang_code, name=''): - if name in ['making-specimen', 'visiting', 'people', 'about-us', 'herbarium']: # TODO page, tempalet mapping - return render_template(f'page-{name}.html') +@frontpage.route('/news', defaults={'lang_code': DEFAULT_LANG_CODE}) +@frontpage.route('//news') +def news(lang_code): + #articles = [x.to_dict() for x in Article.query.filter(Article.site_id==g.site.id).order_by(Article.publish_date.desc()).limit(10).all()] + articles = [x.to_dict() for x in Article.query.order_by(Article.publish_date.desc()).limit(10).all()] + + try: + return render_template(f'sites/{g.site.name}/news.html', articles=articles) + except TemplateNotFound: + return render_template('news.html', articles=articles) - elif name == 'type-specimens': - unit_stats = get_or_set_type_specimens() - return render_template('page-type-specimens.html', unit_stats=unit_stats) - elif name == 'related-links': - return render_template('related_links.html') +@frontpage.route('/pages/', defaults={'lang_code': DEFAULT_LANG_CODE}) +@frontpage.route('//pages/') +def page(lang_code, name=''): + if g.site.data: + if name in g.site.data.get('pages'): + page_name = name.replace('/', '_') + try: + return render_template(f'sites/{g.site.name}/page-{page_name}.html') + except TemplateNotFound: + return 'template not found' return 'page' -@frontend.route('/articles/', defaults={'lang_code': DEFAULT_LANG_CODE}) +@frontpage.route('/articles/', defaults={'lang_code': DEFAULT_LANG_CODE}) def article_detail(lang_code, article_id): article = Article.query.get(article_id) article.content_html = markdown.markdown(article.content) - return render_template('article-detail.html', article=article) + + try: + return render_template(f'sites/{g.site.name}/article-detail.html', article=article) + except TemplateNotFound: + return render_template('article-detail.html', article=article) -@frontend.route('/specimens/SpecimenDetailC.aspx', defaults={'lang_code': DEFAULT_LANG_CODE}) +@frontpage.route('/specimens/SpecimenDetailC.aspx', defaults={'lang_code': DEFAULT_LANG_CODE}) def specimen_detail_legacy(lang_code): if key := request.args.get('specimenOrderNum'): entity = Unit.get_specimen(f'HAST:{int(key)}') return render_template('specimen-detail.html', entity=entity) return abort(404) - -@frontend.route('/collections/', defaults={'lang_code': DEFAULT_LANG_CODE}) -@frontend.route('//collections/') -@frontend.route('/specimens/', defaults={'lang_code': DEFAULT_LANG_CODE}) -@frontend.route('//specimens/') -#@frontend.route('/specimens/') + +@frontpage.route('/collections/', defaults={'lang_code': DEFAULT_LANG_CODE}) +@frontpage.route('//collections/') +@frontpage.route('/specimens/', defaults={'lang_code': DEFAULT_LANG_CODE}) +@frontpage.route('//specimens/') +#@frontpage.route('/specimens/') def specimen_detail(record_key, lang_code): entity = None @@ -155,12 +160,15 @@ def specimen_detail(record_key, lang_code): pass if entity: - return render_template('specimen-detail.html', entity=entity) + try: + return render_template(f'sites/{g.site.name}/specimen-detail.html', entity=entity) + except TemplateNotFound: + return render_template('specimen-detail.html', entity=entity) return abort(404) -@frontend.route('/species/', defaults={'lang_code': DEFAULT_LANG_CODE}) -@frontend.route('//species/') +@frontpage.route('/species/', defaults={'lang_code': DEFAULT_LANG_CODE}) +@frontpage.route('//species/') def species_detail(taxon_id, lang_code): if species := session.get(Taxon, taxon_id): stmt = make_specimen_query({'taxon_id': taxon_id}) @@ -168,17 +176,25 @@ def species_detail(taxon_id, lang_code): items = [] for row in result.all(): items.append(row) - return render_template('species-detail.html', species=species, items=items) + + try: + return render_template(f'sites/{g.site.name}/species-detail.html', species=species, items=items) + except TemplateNotFound: + return render_template('species-detail.html', species=species, items=items) else: return abort(404) -@frontend.route('/taxa', defaults={'lang_code': DEFAULT_LANG_CODE}) -@frontend.route('//taxa') +@frontpage.route('/taxa', defaults={'lang_code': DEFAULT_LANG_CODE}) +@frontpage.route('//taxa') def taxa_index(lang_code): taxa = Taxon.query.filter(Taxon.rank=='family').order_by(Taxon.full_scientific_name).all() - return render_template('taxa-index.html', taxa=taxa) -@frontend.route('/specimen-image/') + try: + return render_template(f'sites/{g.site.name}/taxa-index.html', taxa=taxa) + except TemplateNotFound: + return render_template('taxa-index.html', taxa=taxa) + +@frontpage.route('/specimen-image/') def specimen_image(locale, entity_key): keys = entity_key.split(':') cat_num = keys[1] @@ -194,8 +210,12 @@ def specimen_image(locale, entity_key): img_url = f'http://brmas-pub.s3-ap-northeast-1.amazonaws.com/hast/{first_3}/S_{cat_num}_s.jpg' return render_template('specimen-image.html', image_url=img_url) -@frontend.route('/data', defaults={'lang_code': DEFAULT_LANG_CODE}) -@frontend.route('//data') +@frontpage.route('/data', defaults={'lang_code': DEFAULT_LANG_CODE}) +@frontpage.route('//data') def data_search(lang_code): #print(mimetypes.knownfiles, flush=True) - return render_template('data-search.html') + + try: + return render_template(f'sites/{g.site.name}/data-search.html') + except TemplateNotFound: + return render_template('data-search.html') diff --git a/app/blueprints/page.py b/app/blueprints/page.py deleted file mode 100644 index 98b9861..0000000 --- a/app/blueprints/page.py +++ /dev/null @@ -1,124 +0,0 @@ - -from flask import ( - Blueprint, - render_template, - g, - request, - abort, -) -from sqlalchemy import ( - select, -) -import markdown - -from app.database import session -from app.models.site import ( - Organization, - Article, -) -from app.models.collection import ( - Record, - Unit, - Taxon, -) -from app.utils import( - get_cache, - set_cache, -) -from flask_babel import get_translations - -page = Blueprint('page', __name__) - -@page.before_request -def set_locale(): - if 'en/' in request.path: - setattr(g, 'LOCALE', 'en') - elif 'zh/' in request.path: - setattr(g, 'LOCALE', 'zh') - -''' -@page.route('//people', subdomain='') -@page.route('/people', subdomain='') -def people(lang=''): - return render_template('page-people.html') - -@page.route('//visiting', subdomain='') -@page.route('/visiting', subdomain='') -def visiting(lang=''): - return render_template('page-visiting.html') - -@page.route('//make-specimen', subdomain='') -@page.route('/making-specimen', subdomain='') -def making_specimen(lang=''): - return render_template('page-making-specimen.html') - - -@page.route('//about', subdomain='') -@page.route('/about', subdomain='') -def about_page(lang='', subdomain=''): - return render_template('page-about.html') -''' - -@page.route('/', subdomain='') -@page.route('//', subdomain='') -def static_page(page='', lang='', subdomain=''): - #print(request.headers['Host'], flush=True) - #print(page, lang, subdomain, flush=True) - if site := Organization.get_site(subdomain): - print(site, flush=True) - if page in ['make-specimen', 'visiting', 'people', 'about']: - return render_template(f'page-{page}.html', site=site) - - return abort(404) - -@page.route('//type_specimens', subdomain='') -@page.route('/type_specimens', subdomain='') -def type_specimens(lang_code='', subdomain=''): - g.lang_code = lang_code - - CACHE_KEY = 'type-stat' - CACHE_EXPIRE = 86400 # 1 day: 60 * 60 * 24 - unit_stats = None - - if site := Organization.query.filter(Organization.is_site==True, Organization.subdomain==subdomain).first(): - - if x := get_cache(CACHE_KEY): - unit_stats = x - else: - rows = Unit.query.filter(Unit.type_status != '', Unit.pub_status=='P', Unit.type_is_published==True).all() - stats = { x[0]: 0 for x in Unit.TYPE_STATUS_OPTIONS } - units = [] - for u in rows: - if u.type_status and u.type_status in stats: - stats[u.type_status] += 1 - - # prevent lazy loading - units.append({ - 'family': u.record.taxon_family.full_scientific_name if u.record.taxon_family else '', - 'scientific_name': u.record.proxy_taxon_scientific_name, - 'common_name': u.record.proxy_taxon_common_name, - 'type_reference_link': u.type_reference_link, - 'type_reference': u.type_reference, - 'specimen_url': u.get_specimen_url(), - 'accession_number': u.accession_number, - 'type_status': u.type_status - }) - units = sorted(units, key=lambda x: (x['family'], x['scientific_name'])) - unit_stats = {'units': units, 'stats': stats} - set_cache(CACHE_KEY, unit_stats, CACHE_EXPIRE) - - return render_template('page-type-specimens.html', unit_stats=unit_stats, site=site) - else: - return abort(404) - -@page.route('//related_links') -@page.route('/related_links') -def related_links(lang=''): - org = session.get(Organization, 1) - return render_template('related_links.html', organization=org) - -@page.route('/articles/') -def article_detail(article_id): - article = Article.query.get(article_id) - article.content_html = markdown.markdown(article.content) - return render_template('article-detail.html', article=article) diff --git a/app/helpers.py b/app/helpers.py index 3c304b5..bd6b95b 100644 --- a/app/helpers.py +++ b/app/helpers.py @@ -13,7 +13,7 @@ session, ModelHistory, ) -from app.models.site import Organization +from app.models.site import Site from app.models.collection import ( Collection, Unit, @@ -25,14 +25,14 @@ set_cache, ) -def get_or_set_type_specimens(): +def get_or_set_type_specimens(collection_ids): CACHE_KEY = 'type-stat' CACHE_EXPIRE = 86400 # 1 day: 60 * 60 * 24 unit_stats = None if x := get_cache(CACHE_KEY): unit_stats = x else: - rows = Unit.query.filter(Unit.type_status != '', Unit.pub_status=='P', Unit.type_is_published==True).all() + rows = Unit.query.filter(Unit.type_status != '', Unit.pub_status=='P', Unit.type_is_published==True, Unit.collection_id.in_(collection_ids)).all() stats = { x[0]: 0 for x in Unit.TYPE_STATUS_OPTIONS } units = [] for u in rows: @@ -58,8 +58,8 @@ def get_or_set_type_specimens(): def get_current_site(request): if request and request.headers: - if domain := request.headers.get('Host'): - if site := Organization.get_site(domain): + if host := request.headers.get('Host'): + if site := Site.find_by_host(host): return site return None diff --git a/app/helpers_data.py b/app/helpers_data.py index 09c16d4..95b98d6 100644 --- a/app/helpers_data.py +++ b/app/helpers_data.py @@ -129,3 +129,12 @@ def export_specimen_dwc_csv(): print(t3, t2, t1, flush=True) print(t3-t2, t2-t1, flush=True) + +def import_phase0(data, collection_id): + r = Record(source_data=data, collection_id=collection_id) + session.add(r) + session.commit() + + u = Unit(collection_id=collection_id, record_id=r.id) + session.add(u) + session.commit() diff --git a/app/helpers_query.py b/app/helpers_query.py index 349b379..719d489 100644 --- a/app/helpers_query.py +++ b/app/helpers_query.py @@ -35,10 +35,46 @@ ) def make_specimen_query(filtr): - + ''' + filter data + ''' # TODO: # 處理異體字: 台/臺 # basic stmt + + if sd := filtr.get('sourceData'): + if sd.get('annotate'): + fields = [] + if count_fields := sd['annotate'].get('values'): + fields = [Record.source_data[x] for x in count_fields] + + if sd['annotate'].get('aggregate'): + stmt = select( + func.count("*"), + *fields, + ) + else: + stmt = select( + *fields, + ) + + else: + stmt = select(Unit, Record) + + stmt = stmt.join(Unit, Unit.record_id==Record.id) # prevent cartesian product + + if q := sd.get('q', ''): + many_or = or_() + for field in sd['qFields']: + many_or = or_(many_or, or_(Record.source_data[field].astext.ilike(f'%{q}%'))) + stmt = stmt.where(many_or) + + if ft := sd.get('filters'): + for k, v in ft.items(): + stmt = stmt.where(Record.source_data[k].astext == v) + + return stmt + stmt = select(Unit, Record) \ .join(Unit, Unit.record_id==Record.id) \ .join(Person, Record.collector_id==Person.id, isouter=True) diff --git a/app/models/collection.py b/app/models/collection.py index db0c853..ff8081e 100644 --- a/app/models/collection.py +++ b/app/models/collection.py @@ -102,6 +102,7 @@ class Collection(Base, TimestampMixin): label = Column(String(500)) organization_id = Column(Integer, ForeignKey('organization.id', ondelete='SET NULL'), nullable=True) + site_id = Column(Integer, ForeignKey('site.id', ondelete='SET NULL'), nullable=True) sort = Column(Integer, default=0) people = relationship('Person', secondary=collection_person_map, back_populates='collections') @@ -671,6 +672,23 @@ class Unit(Base, TimestampMixin, UpdateMixin): '''mixed abcd: SpecimenUnit/ObservationUnit (phycal state-specific subtypes of the unit reocrd) BotanicalGardenUnit/HerbariumUnit/ZoologicalUnit/PaleontologicalUnit ''' + BASIS_OF_RECORD_OPTIONS = [ + 'MaterialEntity', + 'PreservedSpecimen', + 'FossilSpecimen', + 'LivingSpecimen', + 'MaterialSample', + 'Event', + 'HumanObservation', + 'MachineObservation', + 'Taxon', + 'Occurrence', + 'MaterialCitation', + 'DrawingOrPhotograph', + 'MultimediaObject', + 'AbsenceObservation', + ] + KIND_OF_UNIT_MAP = { 'HS': 'Herbarium Sheet', 'whole organism': 'whole organizm', @@ -720,7 +738,12 @@ class Unit(Base, TimestampMixin, UpdateMixin): #owner #identifications = relationship('Identification', back_populates='unit') kind_of_unit = Column(String(500)) # herbarium sheet (HS), leaf, muscle, leg, blood, ..., ref: https://arctos.database.museum/info/ctDocumentation.cfm?table=ctspecimen_part_name#whole_organism - + basis_of_record = Column(String(50)) # abcd:RecordBasis + material_entity_id = Column(String(500)) + #material_entity_verbatim_label + #material_entity_associated_seequences + dna_sequence = Column(Text) + sequencer = Column(String(500)) # assemblages # associations # sequences @@ -787,6 +810,15 @@ def ark(self): return x.key return None + @staticmethod + def get_public_stmt(collection_ids): + stmt = select(Unit).where( + Unit.pub_status=='P', + Unit.accession_number != '', + Unit.collection_id.in_(collection_ids) + ) + return stmt + @staticmethod def get_specimen(entity_key): org_code, accession_number = entity_key.split(':') diff --git a/app/models/gazetter.py b/app/models/gazetter.py index 516ae74..00ca5ba 100644 --- a/app/models/gazetter.py +++ b/app/models/gazetter.py @@ -109,6 +109,12 @@ class NamedArea(Base, TimestampMixin): def __str__(self): return f'' + def get_country(self): + if self.area_class_id == 7: # TODO: other class + return Country.query.filter(Country.iso3==self.code).first() + + return None + @property def display_name(self): return '{}{}'.format( diff --git a/app/models/site.py b/app/models/site.py index 2a7e171..2a377b7 100644 --- a/app/models/site.py +++ b/app/models/site.py @@ -1,4 +1,5 @@ from sqlalchemy import ( + select, Table, Column, Integer, @@ -10,6 +11,7 @@ Boolean, ForeignKey, desc, + func, ) from sqlalchemy.orm import ( relationship, @@ -46,9 +48,9 @@ class User(Base, UserMixin, TimestampMixin): username = Column(String(500)) passwd = Column(String(500)) status = Column(String(1), default='P') - organization_id = Column(Integer, ForeignKey('organization.id', ondelete='SET NULL'), nullable=True) + site_id = Column(Integer, ForeignKey('site.id', ondelete='SET NULL'), nullable=True) #default_collection_id = Column(Integer, ForeignKey('collection.id', on - organization = relationship('Organization') + site = relationship('Site') user_list_categories = relationship('UserListCategory') user_lists = relationship('UserList') @@ -94,9 +96,64 @@ class UserList(Base): category = relationship('UserListCategory') +class Site(Base): + ''' + for register admin, organization, collection + ''' + __tablename__ = 'site' + + id = Column(Integer, primary_key=True) + title = Column(String(500)) + title_en = Column(String(500)) + logo_url = Column(String(500)) + name = Column(String(50)) + description = Column(Text) + host = Column(String(500)) + data = Column(JSONB) + related_link_categories = relationship('RelatedLinkCategory') + organizations = relationship('Organization', back_populates='site') + collections = relationship('Collection') + + @staticmethod + def find_by_host(host='foo'): + if site := Site.query.filter(Site.host.ilike(f'{host}')).first(): + return site + + return None + + @staticmethod + def find_collection_ids(host): + if site := Site.find_by_host(host): + print(site.organizations,'aaa',site.id, flush=True) + return [x.collections for x in site.organizations] + #return site.get_collection_ids() + + def get_units(self, num): + from app.models.collection import Unit, Collection, Record + #units = Unit.query.filter(Unit.accession_number!='').order_by(func.random()).limit(4).all() + org_ids = [x.id for x in self.organizations] + units = [] + stmt = select(Unit.id).where(Unit.accession_number!='', Collection.organization_id.in_(org_ids)).join(Record).join(Collection).order_by(func.random()).limit(num) + results = session.execute(stmt) + for i in results.all(): + u = session.get(Unit, int(i[0])) + units.append(u) + + return units + + def get_type_specimens(self): + from app.helpers import get_or_set_type_specimens + + cids = [] + for x in self.organizations: + cids += x.collection_ids + + return get_or_set_type_specimens(cids) + + class Organization(Base, TimestampMixin): ''' - for registered admin user or collection + for collection ''' __tablename__ = 'organization' @@ -105,23 +162,25 @@ class Organization(Base, TimestampMixin): other_name = Column(String(500)) short_name = Column(String(500)) code = Column(String(500)) - related_link_categories = relationship('RelatedLinkCategory') + #related_link_categories = relationship('RelatedLinkCategory') website_url = Column(String(500)) - logo_url = Column(String(500)) + #logo_url = Column(String(500)) taxonomic_scope = Column(String(1000)) geographic_scope = Column(String(1000)) - description = Column(Text) + #description = Column(Text) #collections = relationship('Collection', secondary=organization_collection) collections = relationship('Collection') data = Column(JSONB) # country #default_collection_id = Column(Integer, ForeignKey('collection.id', ondelete='SET NULL'), nullable=True) #default_collection = relationship('Collection', primaryjoin='Organization.default_collection_id == Collection.id') - is_site = Column(Boolean, default=False) - subdomain = Column(String(100)) - domain = Column(String(500)) - ark_nma = Column(String(500)) # Name Mapping Authority (NMA) - settings = Column(JSONB) - + #is_site = Column(Boolean, default=False) + #subdomain = Column(String(100)) + #domain = Column(String(500)) + #ark_nma = Column(String(500)) # Name Mapping Authority (NMA) + #settings = Column(JSONB) + site_id = Column(Integer, ForeignKey('site.id', ondelete='SET NULL')) + + site = relationship('Site', back_populates='organizations') pids = relationship('PersistentIdentifierOrganization') def __repr__(self): @@ -134,13 +193,6 @@ def to_dict(self): 'abbreviation': self.abbreviation, } - @staticmethod - def get_site(domain=''): - if site := Organization.query.filter(Organization.is_site==True, Organization.domain.ilike(f'%{domain}%')).first(): - return site - return None - - @property def collection_ids(self): return [x.id for x in self.collections] @@ -157,7 +209,7 @@ class ArticleCategory(Base): id = Column(Integer, primary_key=True) name = Column(String(500)) label = Column(String(500)) - organization_id = Column(Integer, ForeignKey('organization.id', ondelete='SET NULL'), nullable=True) + site_id = Column(Integer, ForeignKey('site.id', ondelete='SET NULL'), nullable=True) def to_dict(self): return { @@ -166,13 +218,14 @@ def to_dict(self): 'label': self.label, } + class Article(Base, TimestampMixin): __tablename__ = 'article' id = Column(Integer, primary_key=True) subject = Column(String(500)) content = Column(Text) - organization_id = Column(Integer, ForeignKey('organization.id', ondelete='SET NULL'), nullable=True) + site_id = Column(Integer, ForeignKey('site.id', ondelete='SET NULL'), nullable=True) category_id = Column(Integer, ForeignKey('article_category.id', ondelete='SET NULL'), nullable=True) category = relationship('ArticleCategory') publish_date = Column(Date) @@ -203,7 +256,7 @@ class RelatedLinkCategory(Base): label = Column(String(500)) name = Column(String(500)) sort = Column(Integer, nullable=True) - organization_id = Column(ForeignKey('organization.id', ondelete='SET NULL')) + site_id = Column(ForeignKey('site.id', ondelete='SET NULL')) related_links = relationship('RelatedLink') class RelatedLink(Base, TimestampMixin): @@ -215,6 +268,6 @@ class RelatedLink(Base, TimestampMixin): url = Column(String(1000)) note = Column(String(1000)) status = Column(String(4), default='P') - organization_id = Column(Integer, ForeignKey('organization.id', ondelete='SET NULL'), nullable=True) + site_id = Column(Integer, ForeignKey('site.id', ondelete='SET NULL'), nullable=True) category = relationship('RelatedLinkCategory', back_populates='related_links') diff --git a/app/static/css/data-search.css b/app/static/css/data-search.css deleted file mode 100644 index 5d78b33..0000000 --- a/app/static/css/data-search.css +++ /dev/null @@ -1,58 +0,0 @@ -.add-filter:hover{ - background-color: #0f7ae5; - color: #fff; - padding: 4px 8px; - border-radius: 4px; -} -#data-search__head { - background-color: #e0e6b7; - color: #7c7c7c; - border-radius: 10px; -} -#data-search-searchbar-dropdown-list { - height: 200px; - overflow-y: scroll; -} -#data-search-result-map { height: 100vh } -.data-search-token {border: 1px solid #bfbfbf; padding: 10px 20px;} -#data-search-searchbar-dropdown { - padding: 25px; - box-shadow: 0 5px 12px rgba(0,0,0,.15); - z-index: 1020; -} -#data-search-searchbar-dropdown li:hover { - background-color: #f0f2b4; -} -#data-adv-form-container { - margin-top: 30px; - width: 680px; -} - -@media screen and (min-width: 640px) { - #data-adv-form-container .uk-form-controls { - margin-left: 100px; - } - #data-adv-form-container .uk-form-label { - width: 100px; - } -} - - -#form-collector__dropdown { - padding: 25px; - box-shadow: 0 5px 12px rgba(0,0,0,.15); - z-index: 1020; -} -#form-collector__dropdown li:hover{ - background-color: #f4f79b; -} - -.en-dash { - float: left; - margin-left: -19px; - line-height: 2.3; - content: "\2013"; -} -.mg-inline-to-block { - display: block; -} diff --git a/app/static/css/mvp.css b/app/static/css/mvp.css new file mode 100644 index 0000000..a44c50b --- /dev/null +++ b/app/static/css/mvp.css @@ -0,0 +1,538 @@ +/* MVP.css v1.15 - https://github.com/andybrewer/mvp */ + +:root { + --active-brightness: 0.85; + --border-radius: 5px; + --box-shadow: 2px 2px 10px; + --color-accent: #118bee15; + --color-bg: #fff; + --color-bg-secondary: #e9e9e9; + --color-link: #118bee; + --color-secondary: #920de9; + --color-secondary-accent: #920de90b; + --color-shadow: #f4f4f4; + --color-table: #118bee; + --color-text: #000; + --color-text-secondary: #999; + --color-scrollbar: #cacae8; + --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + --hover-brightness: 1.2; + --justify-important: center; + --justify-normal: left; + --line-height: 1.5; + --width-card: 285px; + --width-card-medium: 460px; + --width-card-wide: 800px; + --width-content: 1080px; +} + +@media (prefers-color-scheme: dark) { + :root[color-mode="user"] { + --color-accent: #0097fc4f; + --color-bg: #333; + --color-bg-secondary: #555; + --color-link: #0097fc; + --color-secondary: #e20de9; + --color-secondary-accent: #e20de94f; + --color-shadow: #bbbbbb20; + --color-table: #0097fc; + --color-text: #f7f7f7; + --color-text-secondary: #aaa; + } +} + +html { + scroll-behavior: smooth; +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } +} + +/* Layout */ +article aside { + background: var(--color-secondary-accent); + border-left: 4px solid var(--color-secondary); + padding: 0.01rem 0.8rem; +} + +body { + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-family); + line-height: var(--line-height); + margin: 0; + overflow-x: hidden; + padding: 0; +} + +footer, +header, +main { + margin: 0 auto; + max-width: var(--width-content); + padding: 3rem 1rem; +} + +hr { + background-color: var(--color-bg-secondary); + border: none; + height: 1px; + margin: 4rem 0; + width: 100%; +} + +section { + display: flex; + flex-wrap: wrap; + justify-content: var(--justify-important); +} + +section img, +article img { + max-width: 100%; +} + +section pre { + overflow: auto; +} + +section aside { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow) var(--color-shadow); + margin: 1rem; + padding: 1.25rem; + width: var(--width-card); +} + +section aside:hover { + box-shadow: var(--box-shadow) var(--color-bg-secondary); +} + +[hidden] { + display: none; +} + +/* Headers */ +article header, +div header, +main header { + padding-top: 0; +} + +header { + text-align: var(--justify-important); +} + +header a b, +header a em, +header a i, +header a strong { + margin-left: 0.5rem; + margin-right: 0.5rem; +} + +header nav img { + margin: 1rem 0; +} + +section header { + padding-top: 0; + width: 100%; +} + +/* Nav */ +nav { + align-items: center; + display: flex; + font-weight: bold; + justify-content: space-between; + margin-bottom: 7rem; +} + +nav ul { + list-style: none; + padding: 0; +} + +nav ul li { + display: inline-block; + margin: 0 0.5rem; + position: relative; + text-align: left; +} + +/* Nav Dropdown */ +nav ul li:hover ul { + display: block; +} + +nav ul li ul { + background: var(--color-bg); + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow) var(--color-shadow); + display: none; + height: auto; + left: -2px; + padding: .5rem 1rem; + position: absolute; + top: 1.7rem; + white-space: nowrap; + width: auto; + z-index: 1; +} + +nav ul li ul::before { + /* fill gap above to make mousing over them easier */ + content: ""; + position: absolute; + left: 0; + right: 0; + top: -0.5rem; + height: 0.5rem; +} + +nav ul li ul li, +nav ul li ul li a { + display: block; +} + +/* Typography */ +code, +samp { + background-color: var(--color-accent); + border-radius: var(--border-radius); + color: var(--color-text); + display: inline-block; + margin: 0 0.1rem; + padding: 0 0.5rem; +} + +details { + margin: 1.3rem 0; +} + +details summary { + font-weight: bold; + cursor: pointer; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + line-height: var(--line-height); + text-wrap: balance; +} + +mark { + padding: 0.1rem; +} + +ol li, +ul li { + padding: 0.2rem 0; +} + +p { + margin: 0.75rem 0; + padding: 0; + width: 100%; +} + +pre { + margin: 1rem 0; + max-width: var(--width-card-wide); + padding: 1rem 0; +} + +pre code, +pre samp { + display: block; + max-width: var(--width-card-wide); + padding: 0.5rem 2rem; + white-space: pre-wrap; +} + +small { + color: var(--color-text-secondary); +} + +sup { + background-color: var(--color-secondary); + border-radius: var(--border-radius); + color: var(--color-bg); + font-size: xx-small; + font-weight: bold; + margin: 0.2rem; + padding: 0.2rem 0.3rem; + position: relative; + top: -2px; +} + +/* Links */ +a { + color: var(--color-link); + display: inline-block; + font-weight: bold; + text-decoration: underline; +} + +a:hover { + filter: brightness(var(--hover-brightness)); +} + +a:active { + filter: brightness(var(--active-brightness)); +} + +a b, +a em, +a i, +a strong, +button, +input[type="submit"] { + border-radius: var(--border-radius); + display: inline-block; + font-size: medium; + font-weight: bold; + line-height: var(--line-height); + margin: 0.5rem 0; + padding: 1rem 2rem; +} + +button, +input[type="submit"] { + font-family: var(--font-family); +} + +button:hover, +input[type="submit"]:hover { + cursor: pointer; + filter: brightness(var(--hover-brightness)); +} + +button:active, +input[type="submit"]:active { + filter: brightness(var(--active-brightness)); +} + +a b, +a strong, +button, +input[type="submit"] { + background-color: var(--color-link); + border: 2px solid var(--color-link); + color: var(--color-bg); +} + +a em, +a i { + border: 2px solid var(--color-link); + border-radius: var(--border-radius); + color: var(--color-link); + display: inline-block; + padding: 1rem 2rem; +} + +article aside a { + color: var(--color-secondary); +} + +/* Images */ +figure { + margin: 0; + padding: 0; +} + +figure img { + max-width: 100%; +} + +figure figcaption { + color: var(--color-text-secondary); +} + +/* Forms */ +button:disabled, +input:disabled { + background: var(--color-bg-secondary); + border-color: var(--color-bg-secondary); + color: var(--color-text-secondary); + cursor: not-allowed; +} + +button[disabled]:hover, +input[type="submit"][disabled]:hover { + filter: none; +} + +form { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow) var(--color-shadow); + display: block; + max-width: var(--width-card-wide); + min-width: var(--width-card); + padding: 1.5rem; + text-align: var(--justify-normal); +} + +form header { + margin: 1.5rem 0; + padding: 1.5rem 0; +} + +input, +label, +select, +textarea { + display: block; + font-size: inherit; + max-width: var(--width-card-wide); +} + +input[type="checkbox"], +input[type="radio"] { + display: inline-block; +} + +input[type="checkbox"]+label, +input[type="radio"]+label { + display: inline-block; + font-weight: normal; + position: relative; + top: 1px; +} + +input[type="range"] { + padding: 0.4rem 0; +} + +input, +select, +textarea { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + margin-bottom: 1rem; + padding: 0.4rem 0.8rem; +} + +input[type="text"], +input[type="password"] +textarea { + width: calc(100% - 1.6rem); +} + +input[readonly], +textarea[readonly] { + background-color: var(--color-bg-secondary); +} + +label { + font-weight: bold; + margin-bottom: 0.2rem; +} + +/* Popups */ +dialog { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow) var(--color-shadow); + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 50%; + z-index: 999; +} + +/* Tables */ +table { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + border-spacing: 0; + display: inline-block; + max-width: 100%; + overflow-x: auto; + padding: 0; + white-space: nowrap; +} + +table td, +table th, +table tr { + padding: 0.4rem 0.8rem; + text-align: var(--justify-important); +} + +table thead { + background-color: var(--color-table); + border-collapse: collapse; + border-radius: var(--border-radius); + color: var(--color-bg); + margin: 0; + padding: 0; +} + +table thead tr:first-child th:first-child { + border-top-left-radius: var(--border-radius); +} + +table thead tr:first-child th:last-child { + border-top-right-radius: var(--border-radius); +} + +table thead th:first-child, +table tr td:first-child { + text-align: var(--justify-normal); +} + +table tr:nth-child(even) { + background-color: var(--color-accent); +} + +/* Quotes */ +blockquote { + display: block; + font-size: x-large; + line-height: var(--line-height); + margin: 1rem auto; + max-width: var(--width-card-medium); + padding: 1.5rem 1rem; + text-align: var(--justify-important); +} + +blockquote footer { + color: var(--color-text-secondary); + display: block; + font-size: small; + line-height: var(--line-height); + padding: 1.5rem 0; +} + +/* Scrollbars */ +* { + scrollbar-width: thin; + scrollbar-color: var(--color-scrollbar) transparent; +} + +*::-webkit-scrollbar { + width: 5px; + height: 5px; +} + +*::-webkit-scrollbar-track { + background: transparent; +} + +*::-webkit-scrollbar-thumb { + background-color: var(--color-scrollbar); + border-radius: 10px; +} diff --git a/app/static/css/taxon-tree.css b/app/static/css/taxon-tree.css deleted file mode 100644 index 1410e72..0000000 --- a/app/static/css/taxon-tree.css +++ /dev/null @@ -1,21 +0,0 @@ -.taxonTree__root { - list-style: none; -} -.taxonTree__title { - cursor: pointer; -} -.taxonTree__items { - list-style: none; - border-left: 1px solid #dcdcdc; - white-space: nowrap; - margin-right: 4px; - padding-left: 20px; -} -div.taxonTree__title.closed:before { - content: "+"; - margin-right: 4px; -} -div.taxonTree__title.opened:before { - content: "-"; - margin-right: 4px; -} diff --git a/app/static/js/taxon-tree.js b/app/static/js/taxon-tree.js deleted file mode 100644 index 6a29192..0000000 --- a/app/static/js/taxon-tree.js +++ /dev/null @@ -1,74 +0,0 @@ -(function () { - 'use strict'; - - const TAXON_RANKS = ['family', 'genus', 'species']; - - const fetchChildren = (rootElem, taxaFilter) => { - const spinner = document.createElement('div'); - spinner.setAttribute('uk-spinner', ''); - rootElem.appendChild(spinner); - fetch(`/api/v1/taxa?filter=${JSON.stringify(taxaFilter)}`) - .then(resp => resp.json()) - .then(result => { - spinner.remove(); - - let ul = document.createElement('ul'); - if (taxaFilter.parent_id === null) { - ul.classList.add('taxonTree__root'); - } else { - ul.classList.add('taxonTree__items'); - } - - result.data.forEach((item) => { - const li = document.createElement('li'); - const title = document.createElement('div'); - - title.classList.add('taxonTree__title'); - title.classList.add('closed'); - title.dataset.taxonid = item.id; - title.dataset.taxonrank = item.rank; - let scname = item.full_scientific_name; - if(item.common_name) { - scname = `${scname} (${item.common_name})`; - } - if (item.rank === 'species') { - const taxonLink = document.createElement('a'); - taxonLink.textContent = scname; - taxonLink.href = `/species/${item.id}`; - taxonLink.target = '_blank'; - title.classList.remove('closed'); - title.appendChild(taxonLink); - } else { - const taxonToggle = document.createElement('span'); - taxonToggle.textContent = scname; - title.appendChild(taxonToggle); - taxonToggle.onclick = (e) => { - e.preventDefault(); - const children = title.nextElementSibling; - //console.log(children, title.dataset); - if (children === null) { - const rankIndex = TAXON_RANKS.indexOf(taxaFilter.rank) + 1; - fetchChildren(title, {parent_id: item.id, rank: TAXON_RANKS[rankIndex]}); - } else { - if (children.style.display === 'none') { - children.style.display = 'block'; - title.classList.remove('closed'); - title.classList.add('opened'); - } else { - children.style.display = 'none'; - title.classList.remove('opened'); - title.classList.add('closed'); - } - } - } - } - - li.appendChild(title); - ul.appendChild(li); - rootElem.insertAdjacentElement('afterend', ul); - }); - }); - }; - const taxonTree = document.getElementById('taxonTree'); - fetchChildren(taxonTree, {parent_id: null, rank: 'family'}) -})(); diff --git a/app/templates/_inc_alert.html b/app/templates/_inc_alert.html deleted file mode 100644 index 9c53245..0000000 --- a/app/templates/_inc_alert.html +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/app/templates/_inc_data-explore_adv-filter.html b/app/templates/_inc_data-explore_adv-filter.html deleted file mode 100644 index 1e61f77..0000000 --- a/app/templates/_inc_data-explore_adv-filter.html +++ /dev/null @@ -1,76 +0,0 @@ -
  • -
    -
    - -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    diff --git a/app/templates/_inc_data_filter_form.html b/app/templates/_inc_data_filter_form.html deleted file mode 100644 index b3dec5c..0000000 --- a/app/templates/_inc_data_filter_form.html +++ /dev/null @@ -1,189 +0,0 @@ -
    -
    - -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    -
    - -
    -
    - - -
    - -
    -
      - {# example -
    • -
      example1
      -
      name2
      -
    • - #} -
    -
    -
    -
    -
    - -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    - -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    - -
    - -
    -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    - -
    -
    -
    -
    - -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    -
    - -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    - -
    - -
    -
    -
    - -
    - - -
    diff --git a/app/templates/_inc_data_result_checklist.html b/app/templates/_inc_data_result_checklist.html deleted file mode 100644 index 538704c..0000000 --- a/app/templates/_inc_data_result_checklist.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/templates/_inc_data_result_gallery.html b/app/templates/_inc_data_result_gallery.html deleted file mode 100644 index 2d78885..0000000 --- a/app/templates/_inc_data_result_gallery.html +++ /dev/null @@ -1,15 +0,0 @@ -
    - {% for i in range(8) %} -
    -
    -
    - Image -
    -
    - 館號: ${x.accession_number} -
    -
    -
    - {% endfor %} - -
    diff --git a/app/templates/_inc_data_result_list.html b/app/templates/_inc_data_result_list.html deleted file mode 100644 index a7598b3..0000000 --- a/app/templates/_inc_data_result_list.html +++ /dev/null @@ -1,21 +0,0 @@ - - -{# - {% for i in range(8) %} -
    -
    -
    HAST:123467
    -

    Lorem ipsum dolor

    -

    - 採集者/採集號: C.I. Peng 10000 2022-10-13
    - 採集日期: 202-2. - 採集地: aoeu -

    -
    -
    - Image -
    -
    - {% endfor %} -#} diff --git a/app/templates/_inc_data_result_map.html b/app/templates/_inc_data_result_map.html deleted file mode 100644 index 6749855..0000000 --- a/app/templates/_inc_data_result_map.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/templates/_inc_data_result_table.html b/app/templates/_inc_data_result_table.html deleted file mode 100644 index e7cf480..0000000 --- a/app/templates/_inc_data_result_table.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - {# - - #} diff --git a/app/templates/_inc_data_result_table2.html b/app/templates/_inc_data_result_table2.html deleted file mode 100644 index bb46130..0000000 --- a/app/templates/_inc_data_result_table2.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - -
    {{ _('標本照') }}{{ _('館號') }}{{ _('模式標本') }}{{ _('物種') }}{{ _('採集號') }}{{ _('採集日期') }}{{ _('採集地點') }}
    diff --git a/app/templates/_inc_loading.html b/app/templates/_inc_loading.html deleted file mode 100644 index 558b318..0000000 --- a/app/templates/_inc_loading.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/app/templates/article-detail.html b/app/templates/article-detail.html index 0eb9a08..ad5f910 100644 --- a/app/templates/article-detail.html +++ b/app/templates/article-detail.html @@ -1,13 +1,11 @@ - {% extends "base.html" %} {% block main %} -
    -
    -

    {{ article.subject }}

    - - - {{ article.content_html | safe }} -
    -
    +
    +

    {{ article.subject }}

    + +

    {{ article.content_html | safe }}

    +
    {% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html index 57adb32..1586a66 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,163 +1,57 @@ +{##} - - - - - - {{ g.site.name }}::{{ g.site.other_name}} - {##} - - - {% block style %}{% endblock %} + + + + {% block style %}{% endblock %} + {{ g.site.title }} - -
    - +
    + +

    {{ g.site.title_en }}

    +

    {{ g.site.title }}

    - -
    - {% block main %} - {% endblock %} -
    - -
    -{% endblock %} diff --git a/app/templates/data-search.html b/app/templates/data-search.html index 4968c9e..76083d6 100644 --- a/app/templates/data-search.html +++ b/app/templates/data-search.html @@ -1,34 +1,38 @@ {% extends "base.html" %} -{% block script %} - -{% if config.WEB_ENV == "dev" %} -{##} -{% elif config.WEB_ENV == "prod" %} -{##} -{% endif %} - - -{% endblock %} - -{% block style %} - - - - -{% endblock %} - {% block main %} -{# -
    -
    -

    {{ gettext('標本資料庫查詢') }}

    -
    {{ ngettext("總共: %(num)d 筆。", "總共: %(num)d 筆。", 135879) }}
    -
    -
    -
    -#} -
    -
    -
    +
    +
    +
    + + + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    Col ACol BCol C
    Row 1Cell A1Cell B1Cell C1
    Row 2Cell A2Cell B2Cell C2
    +
    + {% endblock %} diff --git a/app/templates/index-other.html b/app/templates/index-other.html deleted file mode 100644 index 9f4a05d..0000000 --- a/app/templates/index-other.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - {{ site.name }}::NatureDB - - - - - - - -
    -
    - -
    -
    - - - -
    -
    -
    -

    {{ site.other_name }}, {{ site.code }}

    -

    - {{ site.name }} -

    -
    - -
    - {#Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod - tempor incididunt ut labore et dolore magna aliqua.#} - Search Data -
    -
    -
    - - - - - - - -
    -
    - - -

    Title

    -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

    -
    -
    - - - - - - - - diff --git a/app/templates/index.html b/app/templates/index.html index bf5e02d..faa1f67 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,121 +1,5 @@ {% extends "base.html" %} {% block main %} - -
    -
    -
    - {#Alt img#} - Alt img -
    -
    - {{ g.site.other_name }} -

    {{ g.site.name }}

    -

    {{ g.site.other_name }}

    - {#Search Speicmen#} -
    -
    - -
    -
    -
    - - - -
    -

    Featured Specimen

    -
    -
    -
    -
      - {% for u in units %} -
    • -
      -
      -
      - {##} - -
      -
      - Herbarium Sheet -

      - HAST: {{ u.accession_number }} -

      - -

      {{ u.record.proxy_taxon_scientific_name }}
      {{ u.record.proxy_taxon_common_name }}

      -
      -
      -
      -
    • - {% endfor %} -
    -
    -
    - - -
    -
    - - -
    -
    -
      -
    • -
    -
    -
    - - -
    -
    -
    -
    -

    Latest News / 最新消息

    - {% for i in articles %} - - {% endfor %} -
    -
    -
    - {# -

    Archive

    - - #} - {# -

    About Us

    -
    - 本館主要蒐藏台灣(含附屬島嶼)及東亞維管束植物。特殊的蒐藏有菊科、秋海棠科、鴨跖草科、珍珠菜屬(報春花科)、細辛屬(馬兜鈴科)、魔芋屬(天南星科)、茜草科、地錦屬(大戟科)及茶科。蕨類方面有王弼昭先生所採集的10,000多張標本。2003年私立高雄醫學大學贈送本館標本一批。其主要包含日據時代重要採集者島田彌市先生(Yaiti Simada)之標本;亦不乏前台大醫學院長、高醫創辦人杜聰明博士之標本。截至2007年4月為止,已鑑定且編號的蒐藏標本有115,000餘號。平均每年增加約5,000號的標本。本館與世界約30所學術機構交換複份標本,以換取東亞(特別是大陸及日本)植物為重點。目前交換對象除國內各大學及植物研究機構外,遍及亞洲各國、美國、英國及荷蘭之主要大學、植物標本館、博物館與植物園等。 -
    - #} -
    -
    -
    -
    -
    - +index {% endblock %} diff --git a/app/templates/news.html b/app/templates/news.html new file mode 100644 index 0000000..217461e --- /dev/null +++ b/app/templates/news.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block main %} +
    +
    + {% for i in articles %} +
    + {{ i.category.label }}{{ i.subject }} {{ i.publish_date.replace('-', '.') }} +

    {{ i.content|striptags|truncate(300) }}

    + more → +
    + {% endfor %} +
    +
    +{% endblock %} diff --git a/app/templates/page-about-us.html b/app/templates/page-about-us.html deleted file mode 100644 index 27a412a..0000000 --- a/app/templates/page-about-us.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "base.html" %} - -{% block main %} -
    -
    -

    關於HAST

    -

    本館主要蒐藏台灣(含附屬島嶼)及東亞維管束植物。特殊的蒐藏有菊科、秋海棠科、鴨跖草科、珍珠菜屬(報春花科)、細辛屬(馬兜鈴科)、魔芋屬(天南星科)、茜草科、地錦屬(大戟科)及茶科。

    -

    蕨類方面有王弼昭先生所採集的10,000多張標本。2003年私立高雄醫學大學贈送本館標本一批。其主要包含日據時代重要採集者島田彌市先生(Yaiti Simada)之標本;亦不乏前台大醫學院長、高醫創辦人杜聰明博士之標本。截至2007年4月為止,已鑑定且編號的蒐藏標本有115,000餘號。平均每年增加約5,000號的標本。本館與世界約30所學術機構交換複份標本,以換取東亞(特別是大陸及日本)植物為重點。目前交換對象除國內各大學及植物研究機構外,遍及亞洲各國、美國、英國及荷蘭之主要大學、植物標本館、博物館與植物園等。

    -
    -
    -{% endblock%} diff --git a/app/templates/page-herbarium.html b/app/templates/page-herbarium.html deleted file mode 100644 index 11913a0..0000000 --- a/app/templates/page-herbarium.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends "base.html" %} - -{% block main %} -
    -
    -

    簡介

    -

    植物標本館做些什麼?

    -

    植物標本館是蒐藏及研究植物標本的地方。這裡有許多植物採集者至野外調查,或採集所製作而保存下來的證據標本。持續長期有系統的蒐藏、交換、研究及管理,標本館就可以提供植物學相關領域的研究者和植物愛好者,作為研究某一特定類群﹙例如:種,屬,科﹚的地方。到標本館可以比對物種的學名,觀察物種較完整的外表形態或性狀﹙如枝條、葉、花、果實和種子﹚的變異;並歸納出某一物種的開花結果時期、地理分佈範圍和生育地環境等相關資訊。

    -

    為什麼需要標本館?

    -

    不論是植物分類研究、植物生態或資源調查、藥學及生物技術等方面的研究,都必須有證據標本,提供將來查證比對之用。根據國際植物命名法規:一新的物種首次被命名發表時,植物學家必須指定一份標本為正模式(holotype),且必須指明蒐藏的標本館,以供其他學者及後人比對研究。若將標本上所記錄的資料輸入電腦,建立資料庫,將可以提供大家迅速的查詢。

    -

    現今分子生物技術日益精進,已可自古老標本中萃取DNA,因此標本館蒐藏豐富與否,在未來生物科學研究上益形重要。

    -

    標本交換為本館的重要業務之一。透過標本之交換除可以互補各機構間蒐藏、研究專長的不足及提昇學名鑑定的正確性外,亦有分散收藏、降低天災或人為導致的損失風險,以確保研究成果;並可相互檢驗及訂正鑑定的結果,增加資料的可信度,提高使用效率,達到資源互惠共享。

    -

    為什麼必須持續前往各地採集?

    -
      -
    • 瞭解某種植物的生育環境;生長及生殖情形;及在不同分佈範圍的形態變異。
    • -
    • 比較研究某一植物類群內的所有物種之分類性狀和生育地特性。
    • -
    • 瞭解特定地區的植物資源種類、地理分佈、和豐富度。
    • -
    • 追蹤並歸納隨環境及時間而變遷的植物組成變化。
    • -
    - -

    標本館歷史

    -
    - - -

    本館由已故的莊燦煬教授創於1961年(時任本所助理研究員)。次年莊先生赴美深造,所有相關業務旋告停頓。1982年底,彭鏡毅博士恢復運作,並以HAST代號(Herbarium, Institute of Botany, Academia Sinica, Taipei)申請登錄於國際標本館索引(Index Herbariorum)。生物多樣性研究中心於2004年正式成立後,動物所及植物所即分別將此二館典藏之所有標本、設備、空間及人員移交給多樣中心,並於2007年3月15日院務會議討論通過,合併改制為『中央研究院生物多樣性研究博物館』(Biodiversity Research Museum, Academia Sinica)。

    -

    彭博士於1995至1999年兼任植物標本館主任;1999年8月 至2003年6月由趙淑妙研究員兼任;2003年6月6日起至2007年3月15日由彭鏡毅研究員兼主任;之後動、植物標本館合併為中央研究院生物多樣性研究博物館,由彭鏡毅研究員兼任主任至2015年7月31日退休;續由邵廣昭研究員兼任至2016年7月31日;目前由鍾國芳副研究員兼任博物館主任。

    -

    本館位置始於植物所研究大樓北樓402研究室,因標本量漸增,於1990年遷至生物館(黃樓)東南側增建部分(原為植物所圖書室,已於2014年拆除)二樓,約30坪使用空間;1991年擴大使用增建部分一樓;1993年續整理黃樓一樓東側約30坪使用;1998年溫室主控大樓(白樓)地下室約60坪規劃為新鮮標本處理室及複分標本室。2013年為配合院方規劃,擬搬遷至白樓地下室約89坪空間,裝修施工期間標本裝箱暫存跨領域大樓,並暫停對外開放及標本業務;2015年2月搬遷完畢正式重新對外開放。

    -
    - -

    標本館現況

    -

    本館主要蒐藏台灣(含附屬島嶼)及東亞維管束植物。特殊的蒐藏有菊科、秋海棠科、鴨跖草科、珍珠菜屬(報春花科)、細辛屬(馬兜鈴科)、魔芋屬(天南星科)、茜草科、地錦屬(大戟科)及茶科。蕨類則有王弼昭先生畢生採集遺贈的珍貴標本約9,000份。此外,尚有日據時代重要採集者島田彌市(Yaiti Simada)先生之標本2,000餘份、台灣蘭科專家蘇鴻傑教授贈送本館包含85屬246種台灣原生蘭科標本,以及與美國加州科學院及哈佛大學交換獲得的大量中國西南地區植物標本。2000年初朱宇敏博士加入植物所,本館即開始真菌類標本之蒐藏。迄2016年9月,典藏之植物標本已達140,000餘號,其中53件標本已列為本院珍貴動產。

    -

    本館與世界約30所學術機構交換複份標本,以換取東亞(特別是大陸及日本)植物為重點。目前交換對象除國內各大學及植物研究機構外,遍及亞洲各國、美國、英國及荷蘭之主要大學、植物標本館、博物館與植物園等。

    -

    自1992年起,本館得到中央研究院植物研究所及行政院農業委員會支持,由彭鏡毅博士研究室及標本館同仁執行「台灣植物資源調查及資料庫建立」計劃。目的是建立台灣植物普查資料,提昇本土植物種原保護,蒐集環境影響評估之資料,並建立資料庫於網際網路。

    -

    自1995年起在中央研究院計算中心的協助下,本館研究蒐藏之成果已公佈於全球網際網路,為台灣第一個開放於網路上的資料庫。彭鏡毅研究員於1996年編輯出版之「台灣維管束植物編碼索引」(農委會贊助)也置於該網站。

    -

    2002年起執行「數位典藏國家型科技計畫」,將各類植物之標本、文獻、分布及生態影像等珍貴典藏品數位化,建置資料庫,並公開於網路提供各界查詢利用(2022年9月已建檔標本標籤資料145,730筆、標本影像100,260筆、植物生態影像12,731筆)。博物館之典藏品,經過數位化過程獲得重生;經由網際網路之流通平台,提供學術研究、科普教育等全方位之利用,並呈現了台灣傲人的生物多樣性。

    - - -

    社會服務

    -

    近年來本館持續開放給學校團體參觀研習,增進國人對植物標本製作,標本館功能、運作與蒐藏的認識。若有參觀或研習的需求, 請在一個月前預約及發公文以安排參觀事宜。對於一般遊客,僅提供在每年院區開放日限額預約導覽參觀,不便之處敬請見諒。

    - -
    -
    -{% endblock%} diff --git a/app/templates/page-making-specimen.html b/app/templates/page-making-specimen.html deleted file mode 100644 index 8f6f369..0000000 --- a/app/templates/page-making-specimen.html +++ /dev/null @@ -1,53 +0,0 @@ - -{% extends "base.html" %} - -{% block main %} -
    -
    -

    植物標本製作

    -

    曾經為野外盛開的花朵或造形奇特的葉片而驚豔嗎?拾起地上的落花,小心翼翼地夾入日記本裡,希望能保存這剎那間的美麗 …… 啊哈您已經踏出植物標本製作的第一步了!然而製作標本不僅只是為了個人的收藏喜好,同時也可作為鑑識及保存物種的具體材料;對專業的研究人員而言,一份好的標本更是自己及他人進行研究的重要依據。

    -

    何謂植物標本?

    -

    廣意而言,標本為可以保存植物任何一部特徵以做為記錄或提供研究之用的樣本。因此,凡舉陽台上的盆栽、書本夾著的楓葉書籤、顯微鏡下的組織切片…甚至仿製得唯妙唯肖的人造花,只要有具體的辨識特徵,皆可列得上標本之席。然而一般我們所說的「標本」,多指如臘葉標本及浸液標本等具研究目的的標本,此類標本只要處理得當,便可長期保存植物的形態特徵,因此國內外標本館也以此類標本為主要收藏內容。

    -

    為什麼要採集標本?

    -

    採集標本絕對不是頂著「採集」的藉口任意攀折花木!標本館裡的每一份標本都是經過工作人員們謹慎判斷後,具有研究價值者才動手採集。花草樹木也是有生命的,一旦採下了它,就得負責到底。所有採下的植物都得經過許多步驟小心處理,製成標本保存,沒有一草一葉是可以隨意丟棄浪費的。

    -

    既然一份標本的來源如此繁瑣,究竟如此辛苦製作標本的目的何在呢?科學研究講求具體的證據,在描述一種植物時再詳盡的文字說明也不若一份好的標本那麼簡潔有力;而且有具體的標本,研究人員在鑑定植物的種類時也才有其實際的比對依據。換句話說,標本館就像一本大型的植物圖鑑,提供與各種植物相關的珍貴資料。

    -

    標本的種類

    -

    自古植物學家及花卉愛好者們便為了如何長期保存枝葉花果而大傷腦筋,有人潛心於研究乾燥花的製作,發展出使花朵常保色彩鮮艷的技術;也有人以金箔或有機玻璃鑲嵌於植物體,製成精美的標本維持其栩栩如生的姿態。但是這些方法的成本太高且製作過程複雜,並不適用於研究用標本的處理。以下為一般標本館典藏標本的三種類型:

    - -
    1. 原色標本
    -

    原色標本是利用急速脫水以保持花朵和葉片的顏色,或以化學藥品處理使植物色素的性質變而不易褪色然而其製作過程繁複且不易長久保存,再加上很難達到不失真的原色,故標本館中此類標本很少有人製作。

    -
    2. 浸液標本
    -

    浸液標本可以長期保存植物脫水後難以保持原形的花、果及肉質組織。其製作方法為將欲保存的部分浸漬於酒精和福馬林配製的防腐溶液中,以玻璃瓶保存。浸液標本的成本較高且所佔空間龐大,通常只使用於無法製作成臘葉標本的植物。

    -
    3. 臘葉標本
    -

    臘葉標本為將植物體壓平脫水後固定於台紙上保存。其製作過程簡易,在整理和查看上也很方便,因此一般標本館的標本多以此種方式收藏。也因這是最常見的標本製作方法,本單元將更進一步介紹臘葉標本的製作流程。

    - -

    植物標本的採集與製作

    -

    一份好的標本應具備以下件條件:

    -
      -
    • 莖、葉、花、果實、種子俱全,能提供詳盡的鑑定特徵。
    • -
    • 植物體各部位的安置可清楚地顯示各項特徵,方便使用者觀察。
    • -
    • 盡量使各器官維持原狀,且保存良好,沒有蟲蛀發霉的情況。
    • -
    • 簡明但準確地記載植物的生長環境,讓使用者容易了解其現地狀況。
    • -
    -

    採集

    -

    要製作植物標本,採集的工作當然是不能少的。親身至野外採集,除了體會一下大自然悠然自在,深吸一口清新的空氣,更要仔細觀植物的生活形態和生長環境,並且詳加記錄,這些資料對將來標本的鑑定整理及研究工作都是非常重要的。準備好要一睹植物在山野中的迷人風采了嗎?走吧,打包行囊,準備出發囉!

    - -
    1. 採集工具
    -

    野外採集可不是帶著魷魚絲和汽水的賞花郊遊哦!攜帶足夠的水和食物,穿著輕便吸汗的服裝的帽子,同時配備了適宜的採集用具,才能使採集工作事半功倍。此外常用的急救藥品也是不可少的,更別忘了出門在外最重要的一件事-安全第一!

    -
    -
    2. 採集要點
    -

    標本主要為提供鑑定、保存及研究之用,故採集植物時需注意選取其容易辨識的特徵;同時要儘量採集健全植物上的標本,避免幼小、發育不良的植株及腐爛或蟲害的組織,剪取適當的長度,能使之後標本的處理工作較為輕鬆。為了製作更完善的標本,採集植物時有幾個小要領:

    -
      -
    • 植物在日照充足的地區生長情況良好,較容易採得健全的標本。
    • -
    • 選取帶有花和果實的枝條,無法兩者俱全時則至少有花或果;蕨類植物則需同時採集營養葉及帶孢子囊的葉片。
    • -
    • 採集的長度約30~50公分(約一張台紙大小),草本植物需連根採集。
    • -
    • 大型葉片的種類可採一部分的葉片顯示其葉脈、葉尖、葉基的特徵,再配以花果,必要時可分置於多張台紙上。
    • -
    • 寄生植物需連寄主的組織一起採集,並註明寄主的種類。
    • -
    -
    3. 採集記錄
    -

    植物經乾燥製成標本之後,其顏色及形態或多或少都會受到改變。因此採集時做詳細的記錄對往後的鑑定工作是很重要的,對植物的各項性狀、顏色、氣味等,都要仔細地記錄下來,而對植物分佈位置和生長、繁殖季節的描述也是鑑定和研究時的重要依據。再者採集者、採集時間及採集編號也要列入採集記錄中,每一份標本都有一個唯一的編號,以便將來歸檔、查詢。

    - -

    寫採集記錄時應儘量避免過於主觀的描述,且野外採集時標本數量多,要有效率地做採集記錄,最好的方法還是製作一張簡單的表格,再以定義明確的詞句扼要地把所觀察到的項目填寫上去。

    - -
    -{% endblock %} diff --git a/app/templates/page-people.html b/app/templates/page-people.html deleted file mode 100644 index cb8feeb..0000000 --- a/app/templates/page-people.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "base.html" %} - -{% block main %} -
    -
    -

    研究人員

    - -

    鍾國芳 Dr. Kuo-Fang Chung (館主任)

    -

    Plant systematics, Biogeography, Conservation Genetics, Ethnobotany.
    - 植物系統分類、生物地理、保育遺傳學、民族植物

    - -

    趙淑妙 Dr. Shu-Miaw Chaw

    -

    Molecular phylogeny of gymnosperms; Chamaesyce, Euphorbiaceae; Antirhea, Rubiaceae; introduced plants.
    - 茜草科、種子植物之分子親緣

    - -

    黃仁磐 Dr. Jen-Pan Huang

    -

    Speciation and species delimitation, biogeography and phylogeography, and conservation.
    - 種化及物種界定,生物地理及親緣地理學,保育生物學

    - -

    陳可萱 Dr. Ko-Hsuan Chen

    -

    Mycology, Plant-fungal interaction, Plant & Soil Microbiome, Fungal Systematics, Microbial Ecology
    -真菌學,植物真菌互動,植物及土壤微生物相,真菌系統分類,微生物生態學

    - -

    朱宇敏 Dr. Yu-Ming Ju

    -

    Pyrenomycetes
    -炭角菌科真菌-世界性專誌研究及地域性菌類相調查;分子系統發生學

    - -

    彭鏡毅 Dr. Ching-I Peng (退休)

    -

    Flora of Taiwan; Phylogeography; Biosystematics and taxonomy of Asteraceae, Begoniaceae, Commelinaceae, Lysimachia (Primulaceae),Tricyrtis(Liliaceae); Digitalization of herbarium collections.
    -菊科、秋海棠科、鴨跖草科、珍珠菜屬、細辛屬及魔芋屬之分類 

    - -

    鄒稚華 Dr. Chih-Hua Tsou (退休)

    -

    Systematics of Lecythidaceae and Theaceae; embryology.
    -茶科及玉蕊科的分類及親緣關係

    - -

    研究助理

    -
    -
    劉翠雅 Ms. Tsui-Ya Liu
    -
    標本蒐藏管理、野外植物調查採集和資料庫建立
    Specimen management; field collection and database construction .
    -
    李思賢 Mr. Szu-Hsien Lee
    -
    資訊人員 Software Developer.
    -
    -
    -{% endblock%} diff --git a/app/templates/page-type-specimens.html b/app/templates/page-type-specimens.html deleted file mode 100644 index 5839788..0000000 --- a/app/templates/page-type-specimens.html +++ /dev/null @@ -1,69 +0,0 @@ -{% extends "base.html" %} - -{% block script %} - -{% endblock %} - -{% block main %} -
    -

    {{ _('模式標本') }}

    - - - - - - - - - - - - - - - - {% for u in unit_stats.units %} - - - - - - - - - {% endfor %} - -
    {{ _('科名') }}{{ _('學名') }}{{ _('中文名') }}{{ _('發表文獻') }}{{ _('館號') }}{{ _('模式') }}
    {{ u.family }}{{ u.scientific_name }}{{ u.common_name }}{{ u.type_reference }}{{ u.accession_number }}{{ u.type_status|capitalize }}
    -
    -{% endblock %} diff --git a/app/templates/page-visiting.html b/app/templates/page-visiting.html deleted file mode 100644 index e68006f..0000000 --- a/app/templates/page-visiting.html +++ /dev/null @@ -1,83 +0,0 @@ -{% extends "base.html" %} - -{% block script %} - - -{% endblock %} - -{% block style %} - - -{% endblock %} - -{% block main %} - -
    -
    -

    聯絡HAST

    -
    -
    -
    -
    - {#

    Default

    #} -

    - 地址:115臺北市南港區研究院路二段128號 中央研究院植物標本館
    - 電話:(02) 2787-2223 劉小姐
    - 傳真:(02) 2651-6332
    - Email:hast@gate.sinica.edu.tw -

    -
    -
    -
    -

    參訪

    -
    -
    開館時間
    -
    週一至週五9:00-12:00 13:30-16:30 星期例假及國定假日休館
    -
    參觀須知
    -
    機關團體請一個月前先電話連繫並發公文,說明參觀日期、時間、人數及所屬單位,並按預定時間前來參觀。
    -
    人員限制
    -
    每日可接待二梯次共20人次,每週最多二次共40人次。國小三年級(含)以上。
    -
    -

    *無對一般遊客開放,因個人實際研究需要研閱標本者,請另行預約。

    - -
    注意事項
    -
      -
    • 訪客入館請簽名,離館請知會館員。
    • -
    • 參觀者所攜帶之物品,如背包、雨具等,不得攜入標本陳列室。
    • -
    • 本館嚴禁攜入新鮮或未經冷凍殺蟲之動植物活體或標本。
    • -
    • 禁止在標本陳列室內飲食。
    • -
    - - -

    研閱本館標本注意事項

    - - -
      -
    • 一、訪客入館請簽名,離館請知會館員。
    • -
    • 二、本館嚴禁攜入新鮮或未經冷凍殺蟲之動植物活體或標本。
    • -
    • 三、除開水外,禁止在標本陳列室內飲食。
    • -
    • 四、取用標本正面朝上與地面平行,打開屬、種套後,小心拿取標本,切勿抽取。
    • -
    • 五、標本脫落部份請放入碎片小袋內。
    • -
    • 六、鑑定標本請用「立貼」寫上學名、鑑定者(中英文)、鑑定日期,貼在台紙上,置於桌面待本館人員處理。
    • -
    • 七、本館提供解剖顯微鏡、解剖針、立貼、各種工具書等(請洽工作人員);並提供部份資料庫列印服務(例如:本館典藏Begonia 屬植物的採集地點等)。
    • -
    • - 八、如需摘取標本部分研究材料,請遵守以下規定: -
        -
      • 須先徵得館主任同意。
      • -
      • 不可由模式標本或同種標本少於五張者取用任何部分。
      • -
      • 每張標本至多僅取一朵花之花粉,並儘量不破壞花的完整性。
      • -
      • 同一張標本不可取兩次相同材料。
      • -
      • 被取用之標本須貼上標籤,註明取用人、取用日期及取用部位。
      • -
      • 若摘取材料做 SEM、TEM 或 PCR 等研究,應將照片或發表之研究報告致贈本館,並註明採集者、採集號、放大倍率等資料。
      • -
      -
    • -
    • 九、研究報告中若有引證本館標本,應於發表之報告上加註本館代號(HAST ),並將研究著作致贈本館一份。
    • -
    • 十、本館標本之出借,原則上只對標本館或機關,而不對個人提供服務。若需借出標本,須有標本館館主任(curator)或單位首長之借函經本館主任同意。非模式標本借期以一年為限,模式標本則以三個月為限;視情況經辦妥續借手續,得以延長。
    • -
    -
    -
    -{% endblock %} diff --git a/app/templates/related_links.html b/app/templates/related_links.html deleted file mode 100644 index 8a205ca..0000000 --- a/app/templates/related_links.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "base.html" %} - -{% block main %} -
    -

    相關網站連結

    - {% for cat in g.site.related_link_categories %} -

    {{ cat.label }}

    - - {% endfor %} -
    -{% endblock %} diff --git a/app/templates/species-detail.html b/app/templates/species-detail.html deleted file mode 100644 index 2cee9c9..0000000 --- a/app/templates/species-detail.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends "base.html" %} - -{% block main %} -
    -
    -

    {{ species.full_scientific_name }}

    - {##} -

    {% if species.common_name %}{{ species.common_name}}{% endif %}

    -
    -

    Speciemens

    - - - - - - - - - - - - - {% for i in items %} - {% set named_areas = i.1.get_named_area_list('default') %} - - - - - - - - - {% endfor %} - -
    標本照HAST館號採集號採集日期國家行政區
    {{ i.0.accession_number }}{% if i.1.collector %}{{ i.1.collector.full_name }}{% endif %} {{ i.1.field_number }}{% if i.1.collect_date %}{{ i.1.collect_date.strftime('%Y-%m-%d') }}{% endif %}{% if named_areas|length %}{{named_areas[0].display_text}}{% endif %}{% if named_areas|length > 1 %}{% for na in named_areas[1:] %}{{ na.name }}{% if not loop.last %} | {% endif %}{% endfor %}{% endif %}
    -
    -

    {{ _('相關連結')}}

    -

    - {% if species.rank == 'species' %} - iNaturalist - POWO - Tropicos - {% endif %} -

    -
    -
    -{% endblock %} diff --git a/app/templates/specimen-detail.html b/app/templates/specimen-detail.html deleted file mode 100644 index 45b7bfb..0000000 --- a/app/templates/specimen-detail.html +++ /dev/null @@ -1,304 +0,0 @@ -{% extends "base.html" %} - -{% block style %} - - -{% endblock %} - -{% block script %} -{% if entity.record.longitude_decimal and entity.record.latitude_decimal %} - - -{% endif %} -{% endblock %} - -{% macro display_data(label, text='') -%} -
    {{ label }}
    -
    {{ text }}
    -{%- endmacro %} - -{% macro display_data2(label, text='') -%} -
    -
    -
    {{ label }}
    -
    -
    -
    {{ text or "" }}
    -
    -
    -{%- endmacro %} - -{% macro display_data_DEPRECATED(label, text='') -%} -
    -
    - {{ label }}: -
    -
    - {{ text }} -
    -
    -{%- endmacro %} - -{% block main %} -
    - {% if current_user.is_authenticated %}編輯{% endif %} -
    -
    {# left #} - {% with image_url = entity.get_image('_l') %}{# TODO: x #} - - {% if image_url %}{{ entity }}{% endif %} - - {% endwith %} - {# -
    -
    - - {{ entity.guid }} - -
    -
    - #} -
    -
    {# right #} -
    {{ entity.display_kind_of_unit() }}
    - {# -
    {{ _('ARK識別碼') }} {{ entity.guid|replace('https://n2t.net/', '') }}
    -
    {{ _('引用網址') }} {{entity.guid }}
    - #} - -
    -
    {{ _('館號') }}
    -
    {{entity.record.collection.organization.code }}:{{ entity.accession_number }}
    -
    學名
    -
    {% if entity.record.proxy_taxon_scientific_name %}{{ entity.record.proxy_taxon_scientific_name }}{% endif %}
    -
    高階學名
    - -
    -
    -
    {{ _('ARK識別碼') }}
    -
    {{ entity.guid|replace('https://n2t.net/', '') }}
    -
    {{ _('引用網址') }}
    -
    {{entity.guid }}
    - {# -
    {{ _('採集者/採集號') }}
    -
    {{ entity.record.collector.display_name }} {{ entity.record.field_number }}
    -
    {{ _('隨同人員') }}
    -
    {{ entity.record.companion_text }}
    -
    -
    -
    -
    -
    {{ _('採集行政區') }}
    -
    - -
    {{ _('詳細地點') }}
    -
    {{ entity.record.locality_text }}
    - #} -
    - {# -
    {{ _('館號') }}: {{entity.record.collection.organization.code }}:{{ entity.accession_number }}
    -

    {{entity.record.proxy_taxon_scientific_name }}{% if entity.record.proxy_taxon_common_name %} {{ entity.record.proxy_taxon_common_name }}{% endif %}

    - -
    {{ _('採集者') }}: {{ entity.record.collector.display_name }}
    -
    {{ _('採集日期') }}: {{ entity.record.collect_date.strftime('%Y-%m-%d') }}
    -
    {{ _('採集地點') }}: {% for i in entity.record.named_areas %} | {{ i.display_name }}{% endfor %} | {{ entity.record.locality_text }}
    - #} -
    -
    - -
    -

    {{ _('採集資訊') }}

    - {{ display_data2(_('採集者'), entity.record.collector.display_name) }} - {{ display_data2(_('隨同人員'), entity.record.companion_text) }} - {{ display_data2(_('隨同人員(英文)'), entity.record.companion_en_text) }} - {{ display_data2(_('採集號'), entity.record.field_number) }} - {{ display_data2(_('採集日期'), entity.record.collect_date.strftime('%Y-%m-%d') if entity.record.collect_date else '') }} -
    - -
    -
    -

    {{ _('地點') }}

    - {% for i in entity.record.get_named_area_list('default') %} - {{ display_data2(i.area_class.label, i.display_name )}} - {% endfor %} - {{ display_data2(_('詳細地點'), entity.record.locality_text )}} - {{ display_data2(_('經緯度'), entity.record.get_coordinates('dms').simple )}} - {{ display_data2(_('海拔(m)'), entity.record.display_altitude() )}} -
    - -
    -

    {{ _('環境描述') }}

    - {% for a in entity.record.assertions %} - {{ display_data2(a.assertion_type.label, a.value) }} - {% endfor %} -
    - -
    -

    {{ _('鑑定') }}

    - - - - - - - - - - - {% for id in entity.record.identifications %} - - - - - - - {% endfor %} - -
    {{ _('序號') }}{{ _('學名') }}{{ _('鑒定者') }}{{ _('日期') }}
    {{ id.sequence }}{{ id.taxon.full_scientific_name }} {{ id.identifier.display_name if id.identifier else "" }}{{ id.date.strftime('%Y-%m-%d') if id.date else "" }}
    -
    -
    -

    {{ _('標本') }}

    - {% for a in entity.assertions %} - {{ display_data2(a.assertion_type.label, a.value) }} - {% endfor %} -
    - -
    -

    {{ _('多媒體檔案') }}

    -
    - {% for m in entity.record.record_multimedia_objects %} -
    -
    -
    - {{ m.title }} -
    -
    -

    {{ m.record.proxy_taxon_scientific_name }}{% if m.record.proxy_taxon_common_name %} ({{ m.record.proxy_taxon_common_name }}){% endif %}

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    類別{{ m.context.title }}
    標註 - {% for a in m.annotations %} - {% if a.annotation_type.input_type == "checkbox" %} - {{ a.annotation_type.label }} - {% endif %} - {% endfor %}
    儲存格式{% if m.type_id == 1 %}{% endif %} {{ m.physical_format.title }}
    拍攝者{% if m.creator_id %}{{ m.creator.display_name }}{% elif m.creator_text %}{{ m.creator_text }}{% endif %}
    拍攝日期{% if m.date_created %}{{ m.date_created.strftime('%Y-%m-%d') }}{% endif %}
    提供者{% if m.provider_id %}{{ m.provider.display_name }}{% elif m.provider_text %}{{ m.provider_text }}{% endif %}
    版權
    關聯館號{{ m.title }}
    -
    -
    -
    - {% endfor %} -
    -
    -{% endblock %} diff --git a/app/templates/specimen-image.html b/app/templates/specimen-image.html deleted file mode 100644 index 27753e1..0000000 --- a/app/templates/specimen-image.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends "base.html" %} - -{% block style %} - -{% endblock %} - -{% block main %} -
    -
    - {% if unit %} -

    {#unit.dataset.name #}{{ unit.accession_number }}

    - - {% else %} -

    沒有照片

    - - {% endif %} -
    -
    -{% endblock %} diff --git a/app/templates/taxa-index.html b/app/templates/taxa-index.html deleted file mode 100644 index 5c25754..0000000 --- a/app/templates/taxa-index.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends "base.html" %} - -{% block script %} - -{% endblock %} - -{% block style %} - -{% endblock %} - -{% block main %} -
    - 物種(科) -
    - -
    -{% endblock %} diff --git a/docs/api_struct.md b/docs/api_struct.md index 4565e24..8f2759e 100644 --- a/docs/api_struct.md +++ b/docs/api_struct.md @@ -6,21 +6,38 @@ range [0, 20] - no tailing slash -```python -args: { - align: '{right|left}', # default: left - type: '{text|checkbox}' # default: text -} -``` - -```python - data = { - 'header': ( - ('pk', 'pk', {'align':'right'}), - ('full_name', '全名', {'align': 'right'}), - ('is_collector', '採集者', {'align': 'right'}), - ('is_identifier', '鑒定者', {'align': 'right'}), - ), - 'rows': [], -} -``` + + sourceData: { + filters: { + kingdom_name: 'Animalia', + }, + annotate: { + values: ['phylum_name'], + aggregate: 'count', + }, + count: true, +10 + + sourceData: { + filters: { + kingdom_name: 'Animalia', + }, + annotate: { + values: ['class_name'], + aggregate: 'count', + }, + count: true, +30 +## open api + +### search + +### people + +### taxa + +### named-areas + +### area-classes + +### occurrence diff --git a/docs/arch.md b/docs/arch.md index 304cb90..67fc5f5 100644 --- a/docs/arch.md +++ b/docs/arch.md @@ -2,9 +2,14 @@ ## Blueprints base: portals, basic views -frontend: 有語系 +frontpage: 有語系 +## sites + +- app/static/sites +- app/templates/sites + ## Javascript - rewrite in svelte (original in vanilla javascript) diff --git a/docs/data-phase.md b/docs/data-phase.md new file mode 100644 index 0000000..3525672 --- /dev/null +++ b/docs/data-phase.md @@ -0,0 +1,18 @@ +# Data Phase in NatureDB + +## Phase 0: Raw data + +save to Record.source_data + +## Phase 1: Free Text + +verbatim_x, field_number, accession_number, locality_text + +datetime + +## Phase 2: Model + +- Record.collector +- Taxon +- NamedArea +- Assertion