diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b52b207b2..49f4bae75 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -38,7 +38,7 @@ jobs: - name: Set up Python dependencies run: | pip install -U pip - pip install -r requirements/ci.txt + pip install -r requirements/dev.txt - uses: actions/setup-node@v3 with: diff --git a/Dockerfile b/Dockerfile index 8da5a22c1..dd3332c44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,36 +1,35 @@ -FROM python:3.6-buster +# [Node Stage to get node_modolues and node dependencies] +FROM node:8.16.0-buster-slim as node_stage + +COPY ./yarn.lock yarn.lock +COPY ./package.json package.json + +RUN npm install -g yarn +RUN yarn install --dev --frozen-lockfile \ + && rm -rf $HOME/.cache/yarn/* + + +# [Python Stage for Django web server] +FROM python:3.6-slim-buster as python_stage + +COPY --from=node_stage /node_modules /usr/local/lib/node_modules +COPY --from=node_stage /usr/local/bin/node /usr/local/bin/node ENV PYTHONUNBUFFERED 1 ENV BASE_DIR /usr/local ENV APP_DIR $BASE_DIR/app -ENV NVM_INSTALLER_URL https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh -ENV NVM_DIR $BASE_DIR/nvm -ENV YARN_VERSION 1.15.2-1 -ENV NODE_VERSION 8.16.0 +# make nodejs accessible and executable globally +ENV NODE_PATH /usr/local/lib/node_modules/ +ENV PATH /usr/local/bin:$PATH -# make nodejs and yarn accessible and executable globally -ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules -ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH # Add bin directory used by `pip install --user` -ENV PATH "/home/docker/.local/bin:${PATH}" +ENV PATH /home/docker/.local/bin:$PATH # Infrastructure tools # gettext is used for django to compile .po to .mo files. RUN apt-get update -RUN apt-get install apt-utils -y -RUN apt-get update -RUN apt-get install gettext python3-pip -y - -# Install Node and Yarn from upstream -RUN curl -o- $NVM_INSTALLER_URL | bash \ - && . $NVM_DIR/nvm.sh \ - && nvm install $NODE_VERSION \ - && nvm alias default $NODE_VERSION \ - && nvm use default \ - && nvm --version \ - && npm install -g yarn \ - && yarn --version +RUN apt-get install gettext libpq-dev gcc -y # APP directory setup RUN adduser --system --disabled-login docker \ @@ -38,20 +37,15 @@ RUN adduser --system --disabled-login docker \ && chown -R docker:nogroup "$BASE_DIR" "$APP_DIR" USER docker -WORKDIR $APP_DIR # Only copy and install requirements to improve caching between builds # Install Python dependencies COPY --chown=docker:nogroup ./requirements $APP_DIR/requirements RUN pip3 install --user -r "$APP_DIR/requirements/production.txt" \ && rm -rf $HOME/.cache/pip/* -# Install Javascript dependencies -COPY --chown=docker:nogroup ./package.json $APP_DIR/package.json -COPY --chown=docker:nogroup ./yarn.lock $APP_DIR/yarn.lock -RUN yarn install --dev --frozen-lockfile \ - && rm -rf $HOME/.cache/yarn/* + # Finally, copy all the project files along with source files -COPY --chown=docker:nogroup . $APP_DIR +COPY --chown=docker:nogroup ./ $APP_DIR WORKDIR $APP_DIR/src VOLUME $APP_DIR/src/media diff --git a/dev.Dockerfile b/dev.Dockerfile index a1500c6ef..24766b616 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -1,44 +1,41 @@ -FROM python:3.6-slim-buster +# [Node Stage to get node_modolues and node dependencies] +FROM node:8.16.0-buster-slim as node_stage -ENV PYTHONUNBUFFERED 1 -WORKDIR /app +COPY ./yarn.lock yarn.lock +COPY ./package.json package.json -ENV BASE_DIR /usr/local +RUN apt-get update +RUN apt-get install python-pip -y -ENV NVM_INSTALLER_URL https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh -ENV NVM_DIR $BASE_DIR/nvm -ENV YARN_VERSION 1.15.2-1 -ENV NODE_VERSION 8.16.0 +RUN npm install -g yarn +RUN yarn install --dev --frozen-lockfile + +# [Python Stage for Django web server] +FROM python:3.6-slim-buster as python_stage + +WORKDIR /app -# make nodejs and yarn accessible and executable globally -ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH +COPY --from=node_stage /node_modules ./node_modules +COPY --from=node_stage /usr/local/bin/node /usr/local/bin/node + +ENV PYTHONUNBUFFERED 1 +ENV BASE_DIR /usr/local # Infrastructure tools # gettext is used for django to compile .po to .mo files. RUN apt-get update -RUN apt-get install apt-utils -y -RUN apt-get update -RUN apt-get install gettext python3-pip -y - -# Install Node and Yarn from upstream -RUN curl -o- $NVM_INSTALLER_URL | bash \ - && . $NVM_DIR/nvm.sh \ - && nvm install $NODE_VERSION \ - && nvm alias default $NODE_VERSION \ - && nvm use default \ - && nvm --version \ - && npm install -g yarn \ - && yarn --version - +RUN apt-get install -y \ + libpq-dev \ + gcc \ + zlib1g-dev \ + libjpeg62-turbo-dev \ + gettext + +# Only copy and install requirements to improve caching between builds # Install Python dependencies COPY ./requirements ./requirements RUN pip3 install -r ./requirements/dev.txt -# # Install Javascript dependencies -COPY ./package.json ./package.json -COPY ./yarn.lock ./yarn.lock -RUN yarn install --dev --frozen-lockfile - # for entry point COPY ./docker-entrypoint.sh /docker-entrypoint.sh RUN chmod +x /docker-entrypoint.sh diff --git a/requirements/base.txt b/requirements/base.txt index 0bd0505e6..06c6f5158 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -67,7 +67,7 @@ certifi>=2019.11.28 # Better python shell. # http://ipython.org/ -ipython==7.12.0 +ipython==7.16.3 # The most feature-rich and easy-to-use library for processing XML and HTML in # the Python language. diff --git a/requirements/ci.txt b/requirements/ci.txt deleted file mode 100644 index 02327311c..000000000 --- a/requirements/ci.txt +++ /dev/null @@ -1,2 +0,0 @@ --r dev.txt -pycrypto diff --git a/src/attendee/tests.py b/src/attendee/tests.py index e69de29bb..252861424 100644 --- a/src/attendee/tests.py +++ b/src/attendee/tests.py @@ -0,0 +1,30 @@ +import pytest +from django.conf import settings +from registry.helper import reg +from attendee.models import Attendee +from core.models import Token + + +@pytest.mark.parametrize('attendee_token,status,num_channel', [ + ('123', 400, 0), + ('1234', 200, 0), + ('1234', 200, 1), + ('1234', 200, 2), +]) +@pytest.mark.django_db +def test_attendee(drf_api_client, bare_user, attendee_token, status, num_channel): + token = Token.objects.get_or_create(user=bare_user) + drf_api_client.credentials(HTTP_AUTHORIZATION="Token " + str(token[0])) + attendee = Attendee(token="1234") + attendee.save() # insert to database + # add slug + key_prefix = f"{settings.CONFERENCE_DEFAULT_SLUG}.live." + reg["pycontw-1999.live.r1"] = "video_old_id" # unrelated + for i in range(num_channel): + reg[f"{key_prefix}r{i}"] = f"video_id_{i}" + + # test + response = drf_api_client.post('/api/attendee/verify/', data={"token": attendee_token}) + assert response.status_code == status + if status == 200: + assert len(response.json()["youtube_infos"]) == num_channel diff --git a/src/ccip/tests/api/test_ccip.py b/src/ccip/tests/api/test_ccip.py new file mode 100644 index 000000000..0828fc150 --- /dev/null +++ b/src/ccip/tests/api/test_ccip.py @@ -0,0 +1,48 @@ +import pytest + +endpoint = "/ccip/" + + +def assert_data_structure(data, key): + assert key in data + items = data.get(key, []) + for item in items: + assert 'id' in item + assert 'zh' in item + assert 'en' in item + assert 'name' in item['zh'] + assert 'name' in item['en'] + if key == "speakers": + assert 'avatar' in item + assert 'bio' in item['zh'] + assert 'bio' in item['en'] + + +def assert_data_structure_session(data): + assert 'sessions' in data + sessions = data.get('sessions', []) + # 檢查每個 session 是否包含所需的字段 + required_fields = [ + "id", "type", "start", "end", "slide", "speakers", "tags", + "en", "zh", "room", "broadcast", "qa", "live", "record" + ] + for session in sessions: + for field in required_fields: + assert field in session + assert 'title' in session['en'] + assert 'description' in session['en'] + assert 'title' in session['zh'] + assert 'description' in session['zh'] + + +@pytest.mark.django_db +def test_data_structure(client): + response = client.get(endpoint, follow=True) + assert response.status_code == 200 + data = response.json() + + assert_data_structure(data, 'session_types') + assert_data_structure(data, 'tags') + assert_data_structure(data, 'rooms') + assert_data_structure(data, 'speakers') + assert_data_structure_session(data) diff --git a/src/ccip/views.py b/src/ccip/views.py index fff4af3d3..dd59ee071 100644 --- a/src/ccip/views.py +++ b/src/ccip/views.py @@ -1,4 +1,5 @@ import operator +from datetime import timezone, timedelta from django.http import JsonResponse from django.templatetags.static import static @@ -165,11 +166,15 @@ def _transform_session(request, event, type_key, info_getter): for speaker in event_info.speakers ] + LOCAL_TIMEZONE = timezone(offset=timedelta(hours=8)) + # Explicitly show the local timezone used by OPass(CCIP) + # to avoid iOS device showing UTC+0 time without changing it to local time + # https://github.com/CCIP-App/CCIP-iOS/issues/54 session = { 'id': f'{type_key}-{event_info.pk}', 'type': type_key, - 'start': event.begin_time.value.isoformat() if event.begin_time else None, - 'end': event.end_time.value.isoformat() if event.end_time else None, + 'start': event.begin_time.value.astimezone(LOCAL_TIMEZONE).isoformat() if event.begin_time else None, + 'end': event.end_time.value.astimezone(LOCAL_TIMEZONE).isoformat() if event.end_time else None, 'slide': event_info.slide_link, 'speakers': [speaker['id'] for speaker in speakers], 'tags': [tag['id'] for tag in tags], diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po index 8effc4d5d..4722abb98 100644 --- a/src/locale/en_US/LC_MESSAGES/django.po +++ b/src/locale/en_US/LC_MESSAGES/django.po @@ -4965,3 +4965,21 @@ msgstr "" #: users/views.py:129 msgid "Password reset successful. You can now login." msgstr "Password reset successful. You can now login." + +#~ msgid "Keynote" +#~ msgstr "Keynote" + +#~ msgid "EN Slides" +#~ msgstr "EN Slides" + +# #: events/models.py:125 +# msgid "R4" +# msgstr "R4" +#~ msgid "Multifunction room" +#~ msgstr "Multifunction room" + +#~ msgid "Goodideas Studio" +#~ msgstr "Goodideas Studio" + +#~ msgid "R1, R2, R3" +#~ msgstr "R1, R2, R3" diff --git a/src/locale/zh_Hant/LC_MESSAGES/django.po b/src/locale/zh_Hant/LC_MESSAGES/django.po index d92e16faf..e475381e8 100644 --- a/src/locale/zh_Hant/LC_MESSAGES/django.po +++ b/src/locale/zh_Hant/LC_MESSAGES/django.po @@ -4842,3 +4842,21 @@ msgstr "" #: users/views.py:129 msgid "Password reset successful. You can now login." msgstr "密碼重設成功。您現在即可登入。" + +#~ msgid "Keynote" +#~ msgstr "基調演講" + +#~ msgid "EN Slides" +#~ msgstr "英文投影片" + +# #: events/models.py:126 +# msgid "R4" +# msgstr "R4" +#~ msgid "Multifunction room" +#~ msgstr "多功能廳" + +#~ msgid "Goodideas Studio" +#~ msgstr "好想工作室" + +#~ msgid "R1, R2, R3" +#~ msgstr "R1、R2、R3" diff --git a/src/reviews/views.py b/src/reviews/views.py index 5eda7064b..47675612d 100644 --- a/src/reviews/views.py +++ b/src/reviews/views.py @@ -55,6 +55,10 @@ def get_ordering(self): order_key = self.order_keys.get(params.get('order', '').lower()) return order_key or '?' + def get_category(self): + params = self.request.GET + return params.get('category') + def get_queryset(self): user = self.request.user qs = ( @@ -64,10 +68,7 @@ def get_queryset(self): .exclude(review__reviewer=user) .annotate(Count('review')) ) - # params = self.request.GET - # category = params.get('category', '').upper() - # if category: - # proposals = proposals.filter(category=category) + ordering = self.get_ordering() if ordering == '?': # We don't use order_by('?') because it is crazy slow, and instead @@ -85,8 +86,23 @@ def get_queryset(self): else: qs = qs.order_by(ordering) self.ordering = ordering + self.category = self.get_category() + return qs + def get_category_metrics(self, context): + count = 0 + categories = set() + for proposal in context["object_list"]: + if proposal.category == self.category: + count += 1 + if proposal.category not in categories: + categories.add(proposal.category) + return { + "category_options": categories, + "filtered_count": count if count else len(context["object_list"]) + } + def get_context_data(self, **kwargs): review_stage = self.reviews_state.reviews_stage verdicted_proposals = ( @@ -132,8 +148,10 @@ def get_context_data(self, **kwargs): ), 'vote': vote_mapping, 'ordering': self.ordering, + 'category': self.category, 'query_string': self.request.GET.urlencode(), **self.reviews_state._asdict(), + **self.get_category_metrics(context), }) return context diff --git a/src/sponsors/translation.py b/src/sponsors/translation.py index fcfec5f0c..7db2532c5 100644 --- a/src/sponsors/translation.py +++ b/src/sponsors/translation.py @@ -5,6 +5,7 @@ class SponsorTranslationOptions(TranslationOptions): fields = ('name', 'intro', 'subtitle',) + required_languages = {'default': ('name',)} class OpenRoleTranslationOptions(TranslationOptions): diff --git a/src/static/css/pages/_proposals.scss b/src/static/css/pages/_proposals.scss index 053fec4ac..d86d3e015 100644 --- a/src/static/css/pages/_proposals.scss +++ b/src/static/css/pages/_proposals.scss @@ -122,7 +122,7 @@ body.dashboard{ display: inline-block; text-decoration: none; padding: 0 0.5em; - color: $text-color; + color: #3c0970; } del { background-color: lighten($brand-danger, 33.5%); @@ -146,7 +146,7 @@ body.dashboard{ } ins > ins { padding: 0; - background-color: lighten($brand-success, 20%); + background-color: #eba292; } } .markdown-field { diff --git a/src/templates/default/reviews/_includes/proposal_table.html b/src/templates/default/reviews/_includes/proposal_table.html index 627a34bd6..fb4a65a2f 100644 --- a/src/templates/default/reviews/_includes/proposal_table.html +++ b/src/templates/default/reviews/_includes/proposal_table.html @@ -4,9 +4,9 @@