diff --git a/src/locale/zh_Hant/LC_MESSAGES/django.po b/src/locale/zh_Hant/LC_MESSAGES/django.po index fbd7799d7..55f9dc316 100644 --- a/src/locale/zh_Hant/LC_MESSAGES/django.po +++ b/src/locale/zh_Hant/LC_MESSAGES/django.po @@ -1443,6 +1443,24 @@ msgstr "更改密碼" msgid "Reviews" msgstr "審查" +#: templates/default/_includes/dashboard_tablist.html:28 +#: templates/default/reviews/review_stages.html:12 +#: templates/default/reviews/review_stages.html:18 +msgid "Review Stages" +msgstr "審查階段" + +#: templates/default/_includes/dashboard_tablist.html:28 +#: templates/default/reviews/review_stages.html:12 +#: templates/default/reviews/review_stages.html:18 +msgid "Current Review Stage Setting" +msgstr "目前審查階段設定" + +#: templates/default/_includes/dashboard_tablist.html:28 +#: templates/default/reviews/review_stages.html:12 +#: templates/default/reviews/review_stages.html:18 +msgid "Set Review Stage" +msgstr "設定審查階段" + #: templates/default/_includes/nav/dashboard_nav.html:10 msgid "Log out" msgstr "登出" diff --git a/src/proposals/templatetags/proposals.py b/src/proposals/templatetags/proposals.py index fd4472b44..66d75d0f9 100644 --- a/src/proposals/templatetags/proposals.py +++ b/src/proposals/templatetags/proposals.py @@ -1,5 +1,5 @@ from django.template import Library - +from django.conf import settings from proposals.utils import SEP_DEFAULT, SEP_LAST, format_names @@ -11,3 +11,8 @@ def speaker_names_display( proposal, sep_default=SEP_DEFAULT, sep_last=SEP_LAST): names = [info.user.speaker_name for info in proposal.speakers] return format_names(names, sep_default=sep_default, sep_last=sep_last) + + +@register.filter +def configuration_switch(value): + return settings.CONFERENCE_DEFAULT_SLUG + value \ No newline at end of file diff --git a/src/reviews/urls.py b/src/reviews/urls.py index 9250f9e51..4faaf3905 100644 --- a/src/reviews/urls.py +++ b/src/reviews/urls.py @@ -1,10 +1,12 @@ from django.conf.urls import url from .views import ReviewEditView, TalkProposalListView - +from . import views urlpatterns = [ url(r'^$', TalkProposalListView.as_view(), name='review_proposal_list'), url(r'^talk/(?P\d+)/$', - ReviewEditView.as_view(), name='review_edit'), + ReviewEditView.as_view(), + name='review_edit'), + url(r'^review-stages/$', views.review_stages, name='review_stages'), ] diff --git a/src/reviews/views.py b/src/reviews/views.py index cf7d97bbe..b975bb5b5 100644 --- a/src/reviews/views.py +++ b/src/reviews/views.py @@ -3,10 +3,14 @@ import random from django.conf import settings +from django.conf.global_settings import DATETIME_INPUT_FORMATS +from django.core.exceptions import ValidationError from django.contrib.auth.mixins import PermissionRequiredMixin +from django.contrib import messages from django.urls import reverse from django.db.models import Count from django.http import Http404 +from django.shortcuts import redirect, render from django.views.generic import ListView, UpdateView from core.utils import SequenceQuerySet @@ -16,6 +20,10 @@ from .models import REVIEW_REQUIRED_PERMISSIONS, Review, TalkProposalSnapshot from .context import reviews_state +from registry.helper import reg + +import pytz +import datetime class ReviewableMixin: def dispatch(self, request, *args, **kwargs): @@ -286,3 +294,82 @@ def get_success_url(self): if query_string: return url + '?' + query_string return url + +def review_stages(request): + current_review_stages_setting = {} + review_stages_list = [ + 'Call for Proposals', + 'Locked (proposal editing and reviewing disabled)', + 'First Round Review', 'Modification Stage', 'Second Round Review', + 'Internal Decision', 'Announcement of Acceptance' + ] + review_stages_var = [ + 'proposals.creatable', 'proposals.editable', 'proposals.withdrawable', + 'reviews.visible.to.submitters', 'reviews.stage', + 'proposals.disable.after' + ] + + if request.method == 'POST': + + for tag in review_stages_var: + key = settings.CONFERENCE_DEFAULT_SLUG + '.' + tag + if (tag == 'proposals.disable.after'): + if(request.POST['proposals.disable.after'] == ""): + continue + else: + date_time_obj = date_preprocess( + DATETIME_INPUT_FORMATS, + request.POST['proposals.disable.after']) + if(date_time_obj is None): + messages.error(request,'Please input valid date format : " + "%Y-%m-%dT%H:%M') + return render( + request, 'reviews/review_stages.html', { + 'timezones': pytz.common_timezones, + 'review_stages_list': review_stages_list, + 'current_review_stages_setting': current_review_stages_setting, + **reviews_state()._asdict() + }) + continue + else: + tz_selectd = pytz.timezone(request.POST['review_timezone']) + loc_dt = tz_selectd.localize(date_time_obj).strftime( + '%Y-%m-%d %H:%M:%S%z') + value = loc_dt + elif (tag == 'reviews.stage'): + value = int(request.POST[tag]) + else: + value = request.POST[tag] + reg[key] = value + + messages.info(request, 'This setting has been changed successfully.') + + # Render current setting to frontend + for tag in review_stages_var: + key = settings.CONFERENCE_DEFAULT_SLUG + '.' + tag + value = reg.get(key, '') + # Django template language does not support dictionary keys containing "." + if "." in tag: + tag = tag.replace(".", "_") + current_review_stages_setting[tag] = value + + return render( + request, 'reviews/review_stages.html', { + 'timezones': pytz.common_timezones, + 'review_stages_list': review_stages_list, + 'current_review_stages_setting': current_review_stages_setting, + **reviews_state()._asdict() + }) + + +def date_preprocess(DATETIME_INPUT_FORMATS, value): + # Add defined datetime formatx + DATETIME_INPUT_FORMATS += ['%Y-%m-%dT%H:%M:%S', '%Y-%m-%dT%H:%M'] + value = value.strip() + # Try to strptime against each input format. + for format in DATETIME_INPUT_FORMATS: + try: + return datetime.datetime.strptime(value, format) + except (ValueError, TypeError): + continue + return None + # raise ValidationError("Please input valid date format : " + "%Y-%m-%dT%H:%M") diff --git a/src/static/css/components/_buttons.scss b/src/static/css/components/_buttons.scss index 0dda38643..3201b9d69 100644 --- a/src/static/css/components/_buttons.scss +++ b/src/static/css/components/_buttons.scss @@ -10,6 +10,10 @@ @include button-variant($btn-natural-color, $btn-natural-bg, $btn-natural-border); } +.btn-natural-noborder { + @include button-variant($btn-natural-color, $btn-natural-bg, $btn-natural-border); +} + .btn-action{ @include button-variant($btn-action-color, $btn-action-bg, $btn-action-border); } @@ -40,6 +44,19 @@ } } +.btn-natural-noborder { + &:hover { + background-color: $btn-natural-hover-bg; + } + &, + &:focus, + &:active { + outline: none; + border-color: transparent; + } + margin-bottom: 20px; +} + .btn-natural.btn-withdraw { &, &:hover, diff --git a/src/static/css/components/_texts.scss b/src/static/css/components/_texts.scss index 659429f2c..cf494cfb1 100644 --- a/src/static/css/components/_texts.scss +++ b/src/static/css/components/_texts.scss @@ -8,3 +8,8 @@ .text-emphasize { @include roboto-medium(); } + +.input-customized-size input{ + width: 16.2em; +} + diff --git a/src/static/css/components/_toggle.scss b/src/static/css/components/_toggle.scss new file mode 100644 index 000000000..f5d121e78 --- /dev/null +++ b/src/static/css/components/_toggle.scss @@ -0,0 +1,47 @@ +.material-switch > input[type="checkbox"] { + display: none; +} + +.material-switch > label { + cursor: pointer; + height: 0px; + position: relative; + width: 40px; +} + +.material-switch > label::before { + background: rgb(0, 0, 0); + box-shadow: inset 0px 0px 10px rgba(0, 0, 0, 0.5); + border-radius: 8px; + content: ''; + height: 16px; + margin-top: -8px; + position:absolute; + opacity: 0.3; + transition: all 0.4s ease-in-out; + width: 40px; +} + +.material-switch > label::after { + background: rgb(255, 255, 255); + border-radius: 16px; + box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3); + content: ''; + height: 24px; + left: -4px; + margin-top: -8px; + position: absolute; + top: -4px; + transition: all 0.3s ease-in-out; + width: 24px; +} + +.material-switch > input[type="checkbox"]:checked + label::before { + background: inherit; + opacity: 0.5; +} + +.material-switch > input[type="checkbox"]:checked + label::after { + background: inherit; + left: 20px; +} \ No newline at end of file diff --git a/src/static/css/main.scss b/src/static/css/main.scss index 565e2116f..58ee72545 100644 --- a/src/static/css/main.scss +++ b/src/static/css/main.scss @@ -20,6 +20,7 @@ @import "components/tables"; @import "components/texts"; @import "components/lists"; +@import "components/toggle"; // Pages @import "pages/proposals"; diff --git a/src/static/css/vendors/bootstrap/_buttons.scss b/src/static/css/vendors/bootstrap/_buttons.scss index 6452b709f..da8828aba 100755 --- a/src/static/css/vendors/bootstrap/_buttons.scss +++ b/src/static/css/vendors/bootstrap/_buttons.scss @@ -110,6 +110,7 @@ a.btn { &:hover, &:focus, &:active { + outline: none; border-color: transparent; } &:hover, @@ -128,7 +129,6 @@ a.btn { } } - // Button Sizes // -------------------------------------------------- @@ -166,3 +166,4 @@ input[type="button"] { width: 100%; } } + diff --git a/src/static/js/reviews/review_stages.js b/src/static/js/reviews/review_stages.js new file mode 100644 index 000000000..48baa9a8b --- /dev/null +++ b/src/static/js/reviews/review_stages.js @@ -0,0 +1,85 @@ + +var proposals_creatable = document.getElementById("proposals.creatable"); +var proposals_editable = document.getElementById("proposals.editable"); +var proposals_withdrawable = document.getElementById("proposals.withdrawable"); +var reviews_stage = document.getElementById("reviews.stage"); +var reviews_visible_to_submitters = document.getElementById("reviews.visible.to.submitters"); + +$('.hotkey').click(function () { + if ($(this).val() == "Call for Proposals") { + Call_for_Proposals(); + } + else if ($(this).val() == "Locked (proposal editing and reviewing disabled)") { + Locked() + } + else if ($(this).val() == "First Round Review") { + First_Round_Review() + } + else if ($(this).val() == "Modification Stage") { + Modification_Stage() + } + else if ($(this).val() == "Second Round Review") { + Second_Round_Review() + } + else if ($(this).val() == "Internal Decision") { + Internal_Decision() + } + else { + Announcement_of_Acceptance() + } + + /* + Proposal Review Stage Setting + Reference : https://github.com/pycontw/pycon.tw/blob/master/src/reviews/README.md + */ + function Call_for_Proposals(){ + proposals_creatable.checked = true; + proposals_editable.checked = true; + proposals_withdrawable.checked = true; + reviews_stage.value = "0"; + reviews_visible_to_submitters.checked = false; + } + function Locked() { + proposals_creatable.checked = false; + proposals_editable.checked = false; + proposals_withdrawable.checked = false; + reviews_stage.value = "0"; + reviews_visible_to_submitters.checked = false; + } + function First_Round_Review() { + proposals_creatable.checked = false; + proposals_editable.checked = false; + proposals_withdrawable.checked = false; + reviews_stage.value = "1"; + reviews_visible_to_submitters.checked = false; + } + function Modification_Stage() { + proposals_creatable.checked = false; + proposals_editable.checked = true; + proposals_withdrawable.checked = false; + reviews_stage.value = "0"; + reviews_visible_to_submitters.checked = true; + } + function Second_Round_Review() { + proposals_creatable.checked = false; + proposals_editable.checked = false; + proposals_withdrawable.checked = false; + reviews_stage.value = "2"; + reviews_visible_to_submitters.checked = false; + } + function Internal_Decision() { + proposals_creatable.checked = false; + proposals_editable.checked = false; + proposals_withdrawable.checked = false; + reviews_stage.value = "0"; + reviews_visible_to_submitters.checked = false; + } + function Announcement_of_Acceptance() { + proposals_creatable.checked = false; + proposals_editable.checked = true; + proposals_withdrawable.checked = false; + reviews_stage.value = "0"; + reviews_visible_to_submitters.checked = true; + } + +}); diff --git a/src/templates/default/_includes/dashboard_tablist.html b/src/templates/default/_includes/dashboard_tablist.html index 05154e393..b3c72d496 100644 --- a/src/templates/default/_includes/dashboard_tablist.html +++ b/src/templates/default/_includes/dashboard_tablist.html @@ -22,4 +22,10 @@ {% endif %} {% endif %} + + {% if user.is_superuser %} +
  • + {% trans 'Review Stages' %} +
  • + {% endif %} diff --git a/src/templates/default/reviews/review_stages.html b/src/templates/default/reviews/review_stages.html new file mode 100644 index 000000000..3427d22b8 --- /dev/null +++ b/src/templates/default/reviews/review_stages.html @@ -0,0 +1,135 @@ +{% extends 'dashboard_base.html' %} + +{% load i18n static %} +{% load proposals %} +{% load compress crispy_forms_tags %} +{% load tz %} +{% get_current_timezone as TIME_ZONE %} + +{% block dashboard_tablist %} +{% include '_includes/dashboard_tablist.html' with active='admin' %} +{% endblock dashboard_tablist %} + +{% block main-content %} + +

    + {% trans 'Review Stages' %} +

    + +
    +
    + +
    +
    {% trans 'Current Review Stage Setting' %}
    +
      +
    • + {{ ".proposals.proposals.disable.after"|configuration_switch }} +
      +

      {{ current_review_stages_setting.proposals_disable_after }}

      +
      +
    • +
    +
    + +
    +

    {% trans 'Set Review Stage' %}

    +
    + {% for rs in review_stages_list %} + + {% endfor %} + +
    + {% csrf_token %} +
    +
    {% trans 'Review Stages' %} + +
    +
      +
    • + {{ ".proposals.creatable"|configuration_switch }} +
      + + + +
      +
    • +
    • + {{ ".proposals.editable"|configuration_switch }} +
      + + + +
      +
    • +
    • + {{ ".proposals.withdrawable"|configuration_switch }} +
      + + + +
      +
    • +
    • + {{ ".reviews.visible.to.submitters"|configuration_switch }} +
      + + + +
      +
    • +
    • + {{ ".reviews.stage"|configuration_switch }} +
      + +
      +
    • +
    • + {{ ".proposals.disable.after"|configuration_switch }} +
      + +
      +
    • +
    • + timezone +
      + +
      +
    • +
    +
    + +
    + {{ form|crispy }} + +
    +
    +
    +
    + +{% endblock main-content %} + +{% block extra_js %} +{% compress js %} + + +{% endcompress %} +{% endblock extra_js %} + diff --git a/src/users/urls.py b/src/users/urls.py index 56d28c523..c49ed6b57 100644 --- a/src/users/urls.py +++ b/src/users/urls.py @@ -5,32 +5,31 @@ urlpatterns = [ - url(r'^login/$', views.login, name='login'), url(r'^logout/$', views.logout, name='logout'), url(r'^profile/$', views.user_profile_update, name='user_profile_update'), - url(r'^password-change/$', views.password_change, name='password_change'), url(r'^password-change/done/$', - views.password_change_done, name='password_change_done'), - - url(r'^signup/$', - views.user_signup, name='signup'), + views.password_change_done, + name='password_change_done'), + url(r'^signup/$', views.user_signup, name='signup'), url(r'^verify/(?P[-:\w]+)/$', - views.user_verify, name='user_verify'), + views.user_verify, + name='user_verify'), url(r'^verification-request/$', - views.request_verification, name='request_verification'), - + views.request_verification, + name='request_verification'), url(r'^password-reset/$', views.password_reset, name='password_reset'), url(r'^password-reset/done/$', - views.password_reset_done, name='password_reset_done'), - url(r'^password-reset/(?P[0-9A-Za-z_\-]+)/' + views.password_reset_done, + name='password_reset_done'), + url( + r'^password-reset/(?P[0-9A-Za-z_\-]+)/' r'(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', - views.password_reset_confirm, name='password_reset_confirm'), + views.password_reset_confirm, + name='password_reset_confirm'), url(r'^password-reset/complete/$', - views.password_reset_complete, name='password_reset_complete'), - - url(r'^agreement/$', - views.coc_agree, name='coc_agreement'), - + views.password_reset_complete, + name='password_reset_complete'), + url(r'^agreement/$', views.coc_agree, name='coc_agreement'), ] diff --git a/src/users/views.py b/src/users/views.py index 1d4993d0d..eba4854e0 100644 --- a/src/users/views.py +++ b/src/users/views.py @@ -24,7 +24,6 @@ from lxml import etree import lxml.html - User = auth.get_user_model() @@ -195,7 +194,6 @@ def get_context_data(self, **kwargs): context.update(**reviews_state()._asdict()) return context - login = auth_views.LoginView.as_view(authentication_form=AuthenticationForm) logout = auth_views.LogoutView.as_view() password_change = PasswordChangeView.as_view()