├── api ├── __init__.py ├── quizs │ ├── __init__.py │ └── filters.py ├── tags │ ├── __init__.py │ ├── views.py │ └── serializers.py ├── tests │ └── __init__.py ├── users │ ├── __init__.py │ └── serializers.py ├── categories │ ├── __init__.py │ ├── views.py │ └── serializers.py ├── glossary │ ├── __init__.py │ ├── filters.py │ ├── serializers.py │ ├── views.py │ └── tests.py ├── questions │ ├── __init__.py │ └── filters.py ├── contributions │ ├── __init__.py │ ├── views.py │ └── serializers.py └── serializers.py ├── app ├── __init__.py ├── asgi.py ├── wsgi.py └── urls.py ├── core ├── __init__.py ├── tests │ └── __init__.py ├── migrations │ ├── __init__.py │ ├── 0003_configuration_helloasso_url.py │ ├── 0002_alter_configuration_application_open_source_code_url.py │ └── 0004_configuration_office_address_and_url.py ├── templatetags │ ├── __init__.py │ ├── strip_html.py │ ├── user_can_edit.py │ ├── get_language_flag.py │ ├── dict_filters.py │ ├── array_choices_item_display.py │ ├── array_choices_display.py │ ├── get_verbose_name.py │ └── custom_filters.py ├── static │ ├── images │ │ └── favicon.ico │ └── css │ │ └── admin │ │ └── extra.css ├── templates │ ├── admin │ │ └── base.html │ └── layouts │ │ └── main.html └── fields.py ├── quizs ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0009_remove_quiz_author.py │ ├── 0011_quizrelationship_updated.py │ ├── 0006_rename_quiz_author.py │ ├── 0015_quiz_publish_date.py │ ├── 0013_historicalquiz_history_changed_fields.py │ ├── 0028_alter_historicalquiz_history_changed_fields.py │ ├── 0020_remove_historicalquiz_author_and_more.py │ ├── 0002_quiz_author_link.py │ ├── 0012_alter_historicalquiz_options_and_more.py │ ├── 0019_forward_data_author_to_authors.py │ ├── 0003_populate_quiz_author_link.py │ ├── 0014_alter_historicalquiz_language_alter_quiz_language.py │ ├── 0021_quiz_language_migration.py │ ├── 0017_alter_historicalquiz_created_alter_quiz_created_and_more.py │ ├── 0005_alter_quiz_timestamps.py │ ├── 0026_quiz_language_spanish_italian.py │ ├── 0010_quiz_visibility.py │ ├── 0024_alter_historicalquiz_validation_status_and_more.py │ └── 0027_quiz_visibility_translation_fix.py ├── factories.py ├── management │ └── commands │ │ └── sanitize_quiz_question_ordering.py └── filters.py ├── stats ├── __init__.py ├── tests │ └── __init__.py ├── migrations │ ├── __init__.py │ ├── 0007_linkclickevent_field_name.py │ ├── 0003_quizanswerevent_question_answer_split.py │ ├── 0004_dailystat_question_public_and_quiz_public_answer_count.py │ ├── 0002_questionanswerevent_quiz_questionfeedbackevent_quiz.py │ └── 0008_alter_linkclickevent_question_and_more.py ├── urls.py └── constants.py ├── tags ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0004_alter_tag_created.py │ ├── 0003_alter_tag_timestamps.py │ ├── 0002_add_verbose_names.py │ ├── 0005_tag_model_translation.py │ └── 0001_initial.py ├── factories.py ├── forms.py ├── filters.py ├── admin.py └── templates │ └── admin │ └── tags │ └── tag │ └── change_list.html ├── users ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0002_alter_user_managers.py │ ├── 0009_user_logs.py │ ├── 0005_alter_user_created.py │ ├── 0011_user_profile_comments_last_seen_date.py │ ├── 0004_alter_user_timestamps.py │ ├── 0010_user_profile_home_last_seen_date.py │ └── 0003_user_roles.py ├── factories.py ├── forms.py ├── tables.py ├── constants.py └── filters.py ├── www ├── __init__.py ├── admin │ ├── __init__.py │ └── urls.py ├── auth │ ├── __init__.py │ ├── forms.py │ ├── views.py │ └── urls.py ├── pages │ ├── __init__.py │ ├── urls.py │ └── views.py ├── profile │ └── __init__.py ├── quizs │ ├── __init__.py │ └── urls.py ├── tags │ ├── __init__.py │ └── urls.py ├── users │ ├── __init__.py │ ├── urls.py │ └── views.py ├── activity │ ├── __init__.py │ ├── urls.py │ └── views.py ├── categories │ ├── __init__.py │ └── urls.py ├── glossary │ ├── __init__.py │ └── urls.py ├── questions │ └── __init__.py └── contributions │ ├── __init__.py │ └── urls.py ├── activity ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0004_alter_event_actor_id_object_id_allow_null.py │ ├── 0005_alter_event_event_object_type_monthly_agg_stat.py │ └── 0002_alter_event_created_alter_event_event_object_type.py ├── factories.py └── admin.py ├── categories ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0004_alter_category_created.py │ ├── 0003_alter_category_timestamps.py │ ├── 0002_add_verbose_names.py │ ├── 0005_category_model_translation.py │ └── 0001_initial.py ├── factories.py ├── forms.py ├── admin.py ├── templates │ └── admin │ │ └── categories │ │ └── category │ │ └── change_list.html ├── tables.py └── models.py ├── glossary ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0004_remove_glossaryitem_added_and_more.py │ ├── 0003_alter_glossaryitem_timestamps.py │ ├── 0006_historicalglossaryitem_history_changed_fields.py │ ├── 0008_alter_historicalglossaryitem_history_changed_fields.py │ ├── 0007_alter_glossaryitem_created_and_more.py │ ├── 0002_add_verbose_names.py │ ├── 0001_initial.py │ └── 0010_glossaryitem_language.py ├── factories.py ├── admin.py ├── forms.py ├── filters.py └── tables.py ├── history ├── __init__.py ├── utilities.py └── tables.py ├── questions ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0009_remove_question_author_validator.py │ ├── 0010_remove_historicalquestion_added_and_more.py │ ├── 0005_alter_question_timestamps.py │ ├── 0015_question_validation_date.py │ ├── 0013_historicalquestion_history_changed_fields.py │ ├── 0028_alter_historicalquestion_history_changed_fields.py │ ├── 0016_alter_historicalquestion_created_and_more.py │ ├── 0012_alter_historicalquestion_options_and_more.py │ ├── 0006_rename_question_author_validator.py │ ├── 0017_rename_answer_audio_historicalquestion_answer_audio_url_and_more.py │ ├── 0014_alter_historicalquestion_language_and_more.py │ ├── 0022_question_language_migration.py │ ├── 0002_question_author_link_question_validator_link.py │ ├── 0026_question_language_spanish_italian.py │ ├── 0025_alter_historicalquestion_validation_status_and_more.py │ ├── 0027_question_visibility_translation_fix.py │ └── 0011_question_visibility.py └── factories.py ├── contributions ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0005_contribution_updated.py │ ├── 0010_alter_contribution_created.py │ ├── 0012_rename_contribution_comment.py │ ├── 0007_contribution_parent.py │ ├── 0014_comment_remove_status_replied.py │ ├── 0004_contribution_author.py │ ├── 0008_alter_contribution_status.py │ ├── 0016_comment_publish.py │ ├── 0009_alter_contribution_type.py │ ├── 0002_contribution_question_contribution_quiz.py │ └── 0017_alter_comment_status_default.py ├── factories.py └── filters.py ├── Procfile ├── templates ├── auth │ ├── password_reset_email_subject.txt │ ├── password_reset_email_body.html │ ├── password_reset_complete.html │ ├── password_reset.html │ ├── password_reset_sent.html │ └── login.html ├── glossary │ ├── _table_action_items.html │ └── detail_edit.html ├── contributions │ └── _table_action_items.html ├── includes │ ├── _header_notice_env.html │ ├── _header_notice_beta.html │ ├── _filter_badge_list.html │ ├── _badge_item.html │ ├── _language_form.html │ ├── _paginator.html │ └── _s3_upload_form.html ├── tags │ ├── _badge_list.html │ ├── detail_view.html │ ├── detail_edit.html │ └── detail_quizs.html ├── activity │ ├── _event_list.html │ ├── _recent_actions_home_card.html │ └── list.html ├── history │ ├── _table_changed_fields_list.html │ └── _table_id_link.html ├── profile │ ├── quizs_stats.html │ ├── questions_stats.html │ ├── quizs_view.html │ ├── history.html │ ├── info_card_view.html │ └── comments_view.html ├── categories │ ├── _badge_item.html │ ├── detail_view.html │ ├── detail_edit.html │ └── list.html ├── 404.html ├── questions │ ├── detail_quizs.html │ ├── detail_stats.html │ └── detail_comments.html ├── 500.html ├── 403.html ├── quizs │ └── detail_stats.html ├── admin │ └── history.html └── users │ └── administrator_list.html ├── .coveragerc ├── locale ├── de │ └── LC_MESSAGES │ │ └── django.mo ├── en │ └── LC_MESSAGES │ │ └── django.mo ├── es │ └── LC_MESSAGES │ │ └── django.mo ├── fr │ └── LC_MESSAGES │ │ └── django.mo └── it │ └── LC_MESSAGES │ └── django.mo ├── release-tasks.sh ├── crowdin.yml ├── .github └── workflows │ ├── auto-assign-pr.yml │ ├── semantic-pr.yml │ ├── release-please.yml │ └── automerge.yml ├── setup.cfg ├── .gitignore ├── pyproject.toml ├── cron.json ├── manage.py ├── .env.example ├── .pre-commit-config.yaml └── Pipfile /api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quizs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stats/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /activity/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/quizs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/tags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /categories/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glossary/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /history/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /questions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stats/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/admin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/auth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/pages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/profile/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/quizs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/tags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/categories/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/glossary/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/questions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /contributions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quizs/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stats/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tags/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/activity/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/categories/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/glossary/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/questions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /activity/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/contributions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /categories/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glossary/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /questions/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/contributions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /contributions/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | postdeploy: ./release-tasks.sh 2 | web: gunicorn app.wsgi --log-file - 3 | -------------------------------------------------------------------------------- /templates/auth/password_reset_email_subject.txt: -------------------------------------------------------------------------------- 1 | Réinitialisation de votre mot de passe 2 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source= 3 | . 4 | omit= 5 | /usr/* 6 | [report] 7 | #skip_covered=True 8 | #exclude_lines= 9 | -------------------------------------------------------------------------------- /core/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quiz-anthropocene/admin-backend/HEAD/core/static/images/favicon.ico -------------------------------------------------------------------------------- /locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quiz-anthropocene/admin-backend/HEAD/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quiz-anthropocene/admin-backend/HEAD/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quiz-anthropocene/admin-backend/HEAD/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quiz-anthropocene/admin-backend/HEAD/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /locale/it/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quiz-anthropocene/admin-backend/HEAD/locale/it/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /templates/glossary/_table_action_items.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% translate "View" %} 4 | -------------------------------------------------------------------------------- /templates/contributions/_table_action_items.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% translate "View" %} 4 | -------------------------------------------------------------------------------- /release-tasks.sh: -------------------------------------------------------------------------------- 1 | python manage.py migrate 2 | # python manage.py loaddata data/ressources-glossaire.yaml --model=glossary --format=yaml-pretty-flat # won't work if DEPLOY_FOLDER set 3 | -------------------------------------------------------------------------------- /activity/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from activity.models import Event 4 | 5 | 6 | class EventFactory(factory.django.DjangoModelFactory): 7 | class Meta: 8 | model = Event 9 | -------------------------------------------------------------------------------- /api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | 4 | class SimpleChoiceSerializer(serializers.Serializer): 5 | id = serializers.CharField() 6 | name = serializers.CharField() 7 | -------------------------------------------------------------------------------- /templates/includes/_header_notice_env.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | DEBUG 4 |

5 |
6 | -------------------------------------------------------------------------------- /core/templates/admin/base.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | 3 | {% block extrastyle %} 4 | {{ block.super }} 5 | 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | pull_request_title: 'refactor(l10n): New Crowdin translations to review and merge' 2 | files: 3 | - source: /locale/en/LC_MESSAGES/django.po 4 | translation: /locale/%two_letters_code%/LC_MESSAGES/django.po 5 | -------------------------------------------------------------------------------- /tags/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from tags.models import Tag 4 | 5 | 6 | class TagFactory(factory.django.DjangoModelFactory): 7 | class Meta: 8 | model = Tag 9 | 10 | name = "France" 11 | -------------------------------------------------------------------------------- /www/activity/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from www.activity.views import EventListView 4 | 5 | 6 | app_name = "activity" 7 | 8 | urlpatterns = [ 9 | path("", EventListView.as_view(), name="list"), 10 | ] 11 | -------------------------------------------------------------------------------- /core/templatetags/strip_html.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from core.utils.utilities import remove_html_tags 4 | 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.filter 10 | def strip_html(text): 11 | return remove_html_tags(text) 12 | -------------------------------------------------------------------------------- /categories/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from categories.models import Category 4 | 5 | 6 | class CategoryFactory(factory.django.DjangoModelFactory): 7 | class Meta: 8 | model = Category 9 | django_get_or_create = ["name"] 10 | 11 | name = "Energie" 12 | -------------------------------------------------------------------------------- /glossary/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from glossary.models import GlossaryItem 4 | 5 | 6 | class GlossaryItemFactory(factory.django.DjangoModelFactory): 7 | class Meta: 8 | model = GlossaryItem 9 | 10 | name = "Un mot" 11 | definition_short = "Une courte définition" 12 | -------------------------------------------------------------------------------- /templates/tags/_badge_list.html: -------------------------------------------------------------------------------- 1 | {% for tag in tag_list %} 2 | {% if tag.id %} 3 | {{ tag }} 4 | {% else %} 5 | {{ tag }} 2 | {% for event in events %} 3 |
  • 4 | {{ event.display_html | safe }} 5 | {{ event.created|date:"d F" }} 6 |
  • 7 | {% endfor %} 8 | 9 | -------------------------------------------------------------------------------- /templates/auth/password_reset_email_body.html: -------------------------------------------------------------------------------- 1 | Nous vous invitons pour cela à cliquer sur (ou copier) le lien ci-dessous pour le réinitialiser. 2 | 3 | https://{{ domain }}{% url 'auth:password_reset_confirm' uidb64=uid token=token %} 4 | 5 | Si vous n'avez pas demandé la réinitialisation de votre mot de passe, vous pouvez ignorer ce message. 6 | -------------------------------------------------------------------------------- /templates/history/_table_changed_fields_list.html: -------------------------------------------------------------------------------- 1 | {% load get_verbose_name %} 2 | 3 | {% if record.history_type != "+" %} 4 | 9 | {% endif %} 10 | -------------------------------------------------------------------------------- /templates/profile/quizs_stats.html: -------------------------------------------------------------------------------- 1 | {% extends "profile/quizs_base.html" %} 2 | {% load render_table from django_tables2 %} 3 | {% load i18n %} 4 | 5 | {% block profile_quizs_content %} 6 |
    7 |
    8 | {% render_table table %} 9 |
    10 |
    11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /templates/profile/questions_stats.html: -------------------------------------------------------------------------------- 1 | {% extends "profile/questions_base.html" %} 2 | {% load render_table from django_tables2 %} 3 | {% load i18n %} 4 | 5 | {% block profile_questions_content %} 6 |
    7 |
    8 | {% render_table table %} 9 |
    10 |
    11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /templates/categories/_badge_item.html: -------------------------------------------------------------------------------- 1 | {% if category %} 2 | {% if category.id %} 3 | {{ category }} 4 | {% else %} 5 | {{ category }} 6 | {% endif %} 7 | {% else %} 8 | 9 | {% endif %} 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .git, *migrations*, venv, app/settings.py 3 | max-line-length = 119 4 | ignore = E203, W503 5 | 6 | [isort] 7 | combine_as_imports = true 8 | ensure_newline_before_comments = true 9 | force_grid_wrap = 0 10 | include_trailing_comma = true 11 | lines_after_imports = 2 12 | line_length = 119 13 | multi_line_output = 3 14 | use_parentheses = true 15 | -------------------------------------------------------------------------------- /contributions/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from contributions.models import Comment 4 | from core import constants 5 | 6 | 7 | class CommentFactory(factory.django.DjangoModelFactory): 8 | class Meta: 9 | model = Comment 10 | 11 | text = "Une contribution" 12 | type = constants.COMMENT_TYPE_COMMENT_APP 13 | status = constants.COMMENT_STATUS_PENDING 14 | -------------------------------------------------------------------------------- /templates/includes/_header_notice_beta.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 |
    4 |

    5 | {% translate "Website under construction. Feedback is gladly welcome (bugs, improvements...)" %} 🙏🏻👉 {% translate "contact us" %} 6 |

    7 |
    8 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pr.yml: -------------------------------------------------------------------------------- 1 | name: Semantic PRs 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Validate PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v5 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /www/activity/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import ListView 2 | 3 | from activity.models import Event 4 | from core.mixins import ContributorUserRequiredMixin 5 | 6 | 7 | class EventListView(ContributorUserRequiredMixin, ListView): 8 | queryset = Event.objects.display().order_by("-created") 9 | template_name = "activity/list.html" 10 | context_object_name = "events" 11 | paginate_by = 50 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac/OSX 2 | .DS_Store 3 | 4 | # Windows 5 | Thumbs.db 6 | 7 | # Local stuff 8 | local_scripts 9 | local_data 10 | 11 | # Python 12 | **/__pycache__/** 13 | 14 | # Django 15 | db.sqlite3 16 | staticfiles 17 | 18 | # Environments 19 | .env 20 | 21 | # Tests 22 | .coverage 23 | htmlcov 24 | 25 | # Editor directories and files 26 | .idea 27 | .vscode 28 | *.suo 29 | *.ntvs* 30 | *.njsproj 31 | *.sln 32 | *.sw? 33 | -------------------------------------------------------------------------------- /templates/includes/_filter_badge_list.html: -------------------------------------------------------------------------------- 1 | {% load get_verbose_name %} 2 | 3 | {% for search_filter in search_filters %} 4 | 5 | {% get_verbose_name model_name search_filter.key %} : {{ search_filter.value }} 6 | {% if search_filter.delete_url %} {% endif %} 7 | 8 | {% endfor %} 9 | -------------------------------------------------------------------------------- /users/migrations/0002_alter_user_managers.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-04-04 22:05 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("users", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelManagers( 14 | name="user", 15 | managers=[], 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /www/users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from www.users.views import AdministratorListView, AuthorCardList, UserHomeView 4 | 5 | 6 | app_name = "users" 7 | 8 | urlpatterns = [ 9 | path("", UserHomeView.as_view(), name="home"), 10 | path("authors/", AuthorCardList.as_view(), name="author_card_list"), 11 | path("administrators/", AdministratorListView.as_view(), name="administrator_list"), 12 | ] 13 | -------------------------------------------------------------------------------- /www/auth/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm 3 | 4 | 5 | class LoginForm(AuthenticationForm): 6 | def clean_username(self): 7 | username = self.cleaned_data["username"] 8 | return username.lower() 9 | 10 | 11 | class PasswordResetForm(PasswordResetForm): 12 | email = forms.EmailField(label="Votre adresse e-mail", required=True) 13 | -------------------------------------------------------------------------------- /quizs/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.utils.text import slugify 3 | 4 | from core import constants 5 | from quizs.models import Quiz 6 | 7 | 8 | class QuizFactory(factory.django.DjangoModelFactory): 9 | class Meta: 10 | model = Quiz 11 | 12 | name = "Le quiz" 13 | slug = factory.LazyAttribute(lambda o: slugify(o.name)) 14 | validation_status = constants.VALIDATION_STATUS_VALIDATED 15 | publish = False 16 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: release-please 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | release-please: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: google-github-actions/release-please-action@v4 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | release-type: simple 20 | -------------------------------------------------------------------------------- /quizs/migrations/0009_remove_quiz_author.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-07 14:15 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("quizs", "0008_add_flatten_relation_fields"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="quiz", 15 | name="author_old", 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /app/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for app project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") 16 | 17 | application = get_asgi_application() 18 | -------------------------------------------------------------------------------- /app/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for app project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") 16 | 17 | application = get_wsgi_application() 18 | -------------------------------------------------------------------------------- /templates/activity/_recent_actions_home_card.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 |
    4 |
    5 |
    {% translate "Recent actions" %}
    6 | {% include "activity/_event_list.html" with events=events %} 7 | {% if show_more_button %} 8 | {% translate "View more" %} 9 | {% endif %} 10 |
    11 |
    12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 119 3 | 4 | [tool.flake8] 5 | # exclude = .git, *migrations*, venv, app/settings.py 6 | max-line-length = 119 7 | ignore = "E203, W503" 8 | 9 | [tool.isort] 10 | combine_as_imports = true 11 | ensure_newline_before_comments = true 12 | force_grid_wrap = 0 13 | include_trailing_comma = true 14 | known_first_party = "lemarche" 15 | lines_after_imports = 2 16 | line_length = 119 17 | multi_line_output = 3 18 | use_parentheses = true 19 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% translate "Error" %} 404{{ block.super }}{% endblock %} 5 | 6 | {% block content %} 7 |
    8 |
    9 |
    10 |

    {% translate "Error" %} 404

    11 |

    La page que vous avez demandée n'a pas pu être trouvée.

    12 |
    13 |
    14 |
    15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /templates/auth/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | 3 | {% block title %}Réinitialisation du mot de passe validée{{ block.super }}{% endblock %} 4 | 5 | {% block content %} 6 |
    7 |
    8 |
    9 |

    Votre mot de passe a été réinitialisé avec succès !

    10 |

    Me connecter

    11 |
    12 |
    13 |
    14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /core/templatetags/user_can_edit.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | 4 | register = template.Library() 5 | 6 | 7 | @register.simple_tag 8 | def user_can_edit_question(user, question): 9 | return user.can_edit_question(question) 10 | 11 | 12 | @register.simple_tag 13 | def user_can_edit_quiz(user, quiz): 14 | return user.can_edit_quiz(quiz) 15 | 16 | 17 | @register.simple_tag 18 | def user_can_edit_comment(user, comment): 19 | return user.can_edit_comment(comment) 20 | -------------------------------------------------------------------------------- /templates/history/_table_id_link.html: -------------------------------------------------------------------------------- 1 | {% if record.object_model == "Question" %} 2 | {{ record.id }} 3 | {% elif record.object_model == "Quiz" %} 4 | {{ record.id }} 5 | {% elif record.object_model == "GlossaryItem" %} 6 | {{ record.id }} 7 | {% else %} 8 | {{ record.id }} 9 | {% endif %} 10 | -------------------------------------------------------------------------------- /core/templatetags/get_language_flag.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from core import constants 4 | 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.simple_tag 10 | def get_language_flag(language): 11 | language_option = next( 12 | (language_option for language_option in constants.LANGUAGE_OPTIONS if language_option[2] == language["code"]), 13 | None, 14 | ) 15 | if language_option: 16 | return language_option[3] 17 | return "" 18 | -------------------------------------------------------------------------------- /templates/includes/_badge_item.html: -------------------------------------------------------------------------------- 1 | {% if value %} 2 | {% if field_name == "question" %} 3 | {{ value }} 4 | {% elif field_name == "quiz" %} 5 | {{ value }} 6 | {% else %} 7 | {{ value }} 8 | {% endif %} 9 | {% else %} 10 | 11 | {% endif %} 12 | -------------------------------------------------------------------------------- /categories/forms.py: -------------------------------------------------------------------------------- 1 | from ckeditor.widgets import CKEditorWidget 2 | from django import forms 3 | 4 | from categories.models import Category 5 | 6 | 7 | class CategoryEditForm(forms.ModelForm): 8 | description = forms.CharField(widget=CKEditorWidget()) 9 | 10 | class Meta: 11 | model = Category 12 | fields = ["name", "name_long", "description"] 13 | 14 | def __init__(self, *args, **kwargs): 15 | super().__init__(*args, **kwargs) 16 | self.fields["name"].disabled = True 17 | -------------------------------------------------------------------------------- /contributions/filters.py: -------------------------------------------------------------------------------- 1 | import django_filters 2 | 3 | from contributions.models import Comment 4 | from core import constants 5 | 6 | 7 | class CommentFilter(django_filters.FilterSet): 8 | publish = django_filters.ChoiceFilter(choices=constants.BOOLEAN_CHOICES) 9 | 10 | class Meta: 11 | model = Comment 12 | fields = ["type", "status", "publish"] 13 | 14 | 15 | class CommentNewFilter(django_filters.FilterSet): 16 | class Meta: 17 | model = Comment 18 | fields = ["type"] 19 | -------------------------------------------------------------------------------- /templates/questions/detail_quizs.html: -------------------------------------------------------------------------------- 1 | {% extends "questions/detail_base.html" %} 2 | {% load render_table from django_tables2 %} 3 | {% load i18n %} 4 | 5 | {% block question_detail_content %} 6 |
    7 |
    8 | {% if not table.rows|length %} 9 |
    0 {% translate "Quizs" %}
    10 | {% else %} 11 | {% render_table table %} 12 | {% endif %} 13 |
    14 |
    15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /core/templatetags/dict_filters.py: -------------------------------------------------------------------------------- 1 | """ 2 | https://docs.djangoproject.com/en/dev/howto/custom-template-tags/ 3 | """ 4 | from django import template 5 | 6 | 7 | register = template.Library() 8 | 9 | 10 | @register.filter 11 | def get_dict_item(dictionary, key): 12 | """ 13 | Find a dictionary value with a key as a variable. 14 | https://stackoverflow.com/a/8000091 15 | 16 | Usage: 17 | {% load dict_filters %} 18 | {{ dict|get_dict_item:key }} 19 | """ 20 | return dictionary.get(key) 21 | -------------------------------------------------------------------------------- /users/migrations/0009_user_logs.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2 on 2023-06-23 17:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("users", "0008_historicalusercard"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="user", 15 | name="logs", 16 | field=models.JSONField(default=list, editable=False, verbose_name="Logs historiques"), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /tags/forms.py: -------------------------------------------------------------------------------- 1 | from ckeditor.widgets import CKEditorWidget 2 | from django import forms 3 | 4 | from tags.models import Tag 5 | 6 | 7 | class TagCreateForm(forms.ModelForm): 8 | description = forms.CharField(required=False, widget=CKEditorWidget()) 9 | 10 | class Meta: 11 | model = Tag 12 | fields = ["name", "description"] 13 | 14 | 15 | class TagEditForm(TagCreateForm): 16 | def __init__(self, *args, **kwargs): 17 | super().__init__(*args, **kwargs) 18 | self.fields["name"].disabled = True 19 | -------------------------------------------------------------------------------- /core/templatetags/array_choices_item_display.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.utils.encoding import force_str 3 | 4 | from users import constants 5 | 6 | 7 | register = template.Library() 8 | 9 | 10 | @register.simple_tag 11 | def array_choices_item_display(obj, field, value): 12 | """Pretty rendering of ArrayField value.""" 13 | 14 | choices_dict = dict() 15 | 16 | if field == "roles": 17 | choices_dict = dict(constants.USER_ROLE_CHOICES) 18 | 19 | return force_str(choices_dict.get(value, "")) 20 | -------------------------------------------------------------------------------- /stats/migrations/0007_linkclickevent_field_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-03 19:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("stats", "0006_linkclickevent"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="linkclickevent", 15 | name="field_name", 16 | field=models.CharField(blank=True, max_length=50, verbose_name="Nom du champ"), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /templates/questions/detail_stats.html: -------------------------------------------------------------------------------- 1 | {% extends "questions/detail_base.html" %} 2 | {% load i18n custom_filters get_verbose_name %} 3 | 4 | {% block question_detail_content %} 5 | {% for field in question_agg_stat_dict %} 6 |
    7 |
    8 | {% get_verbose_name question_stats field %} 9 |
    10 |
    11 | {{ question_agg_stat_dict|get_obj_attr:field|default:"" }} 12 |
    13 |
    14 | {% endfor %} 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /api/glossary/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from glossary.models import GlossaryItem 4 | 5 | 6 | class GlossaryItemSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = GlossaryItem 9 | fields = [ 10 | # "id", 11 | "name", 12 | "name_alternatives", 13 | "definition_short", 14 | "description", 15 | "description_accessible_url", 16 | "language", 17 | "created", 18 | "updated", 19 | ] 20 | -------------------------------------------------------------------------------- /quizs/migrations/0011_quizrelationship_updated.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-19 16:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("quizs", "0010_quiz_visibility"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="quizrelationship", 15 | name="updated", 16 | field=models.DateTimeField(auto_now=True, verbose_name="Date de dernière modification"), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /contributions/migrations/0005_contribution_updated.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1a1 on 2022-05-22 14:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("contributions", "0004_contribution_author"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="contribution", 15 | name="updated", 16 | field=models.DateTimeField(auto_now=True, verbose_name="Date de dernière modification"), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /www/pages/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.views.generic import TemplateView 3 | 4 | from www.pages.views import HelpView, HomeView 5 | 6 | 7 | app_name = "pages" 8 | 9 | urlpatterns = [ 10 | path("", HomeView.as_view(), name="home"), 11 | path("help/", HelpView.as_view(), name="help"), 12 | path("403/", TemplateView.as_view(template_name="403.html"), name="403"), 13 | path("404/", TemplateView.as_view(template_name="404.html"), name="404"), 14 | path("500/", TemplateView.as_view(template_name="500.html"), name="500"), 15 | ] 16 | -------------------------------------------------------------------------------- /api/tags/views.py: -------------------------------------------------------------------------------- 1 | from drf_spectacular.utils import extend_schema 2 | from rest_framework import mixins, viewsets 3 | 4 | from api.tags.serializers import TagSerializer 5 | from tags.models import Tag 6 | 7 | 8 | class TagViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): 9 | queryset = Tag.objects.all().order_by("name") 10 | serializer_class = TagSerializer 11 | 12 | @extend_schema(summary="Lister tous les tags", tags=[Tag._meta.verbose_name_plural]) 13 | def list(self, request, *args, **kwargs): 14 | return super().list(request, args, kwargs) 15 | -------------------------------------------------------------------------------- /tags/migrations/0004_alter_tag_created.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-08-17 14:19 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("tags", "0003_alter_tag_timestamps"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="tag", 16 | name="created", 17 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name="Date de création"), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /users/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from users import constants 4 | from users.models import User 5 | 6 | 7 | DEFAULT_PASSWORD = "P4ssw0rd!*" 8 | 9 | 10 | class UserFactory(factory.django.DjangoModelFactory): 11 | class Meta: 12 | model = User 13 | 14 | first_name = factory.Sequence("first_name{0}".format) 15 | last_name = factory.Sequence("last_name{0}".format) 16 | email = factory.Sequence("email{0}@example.com".format) 17 | password = factory.PostGenerationMethodCall("set_password", DEFAULT_PASSWORD) 18 | roles = [constants.USER_ROLE_CONTRIBUTOR] 19 | -------------------------------------------------------------------------------- /users/migrations/0005_alter_user_created.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-08-17 14:19 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("users", "0004_alter_user_timestamps"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="user", 16 | name="created", 17 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name="Date de création"), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /cron.json: -------------------------------------------------------------------------------- 1 | { 2 | "jobs": [ 3 | { 4 | "command": "15 */6 * * * python manage.py generate_daily_stats" 5 | }, 6 | { 7 | "command": "30 3 * * * python manage.py export_data_to_github" 8 | }, 9 | { 10 | "command": "30 6,18 * * * python manage.py export_stats_to_github" 11 | }, 12 | { 13 | "command": "0 9 * * 1 python manage.py generate_weekly_stat_event" 14 | }, 15 | { 16 | "command": "0 9 1 * * python manage.py generate_monthly_stat_event" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /questions/migrations/0009_remove_question_author_validator.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-07 14:15 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("questions", "0008_add_flatten_relation_fields"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="question", 15 | name="author_old", 16 | ), 17 | migrations.RemoveField( 18 | model_name="question", 19 | name="validator_old", 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /categories/migrations/0004_alter_category_created.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-08-17 14:19 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("categories", "0003_alter_category_timestamps"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="category", 16 | name="created", 17 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name="Date de création"), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /stats/migrations/0003_quizanswerevent_question_answer_split.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-04-19 10:59 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("stats", "0002_questionanswerevent_quiz_questionfeedbackevent_quiz"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="quizanswerevent", 15 | name="question_answer_split", 16 | field=models.JSONField(default=dict, help_text="Les détails par question"), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /api/categories/views.py: -------------------------------------------------------------------------------- 1 | from drf_spectacular.utils import extend_schema 2 | from rest_framework import mixins, viewsets 3 | 4 | from api.categories.serializers import CategorySerializer 5 | from categories.models import Category 6 | 7 | 8 | class CategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): 9 | queryset = Category.objects.all() 10 | serializer_class = CategorySerializer 11 | 12 | @extend_schema(summary="Lister toutes les catégories", tags=[Category._meta.verbose_name_plural]) 13 | def list(self, request, *args, **kwargs): 14 | return super().list(request, args, kwargs) 15 | -------------------------------------------------------------------------------- /questions/migrations/0010_remove_historicalquestion_added_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-07 22:08 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("questions", "0009_remove_question_author_validator"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="historicalquestion", 15 | name="added", 16 | ), 17 | migrations.RemoveField( 18 | model_name="question", 19 | name="added", 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /contributions/migrations/0010_alter_contribution_created.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-08-17 14:19 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("contributions", "0009_alter_contribution_type"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="contribution", 16 | name="created", 17 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name="Date de création"), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% translate "Error" %} 500{{ block.super }}{% endblock %} 5 | 6 | {% block content %} 7 |
    8 |
    9 |
    10 |

    {% translate "Error" %} 500

    11 |

    Une erreur technique s'est produite. L'équipe technique a été notifiée :)

    12 | {#

    Notre équipe technique a été informée du problème et s'en occupera le plus rapidement possible.

    #} 13 |
    14 |
    15 |
    16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /api/contributions/views.py: -------------------------------------------------------------------------------- 1 | from drf_spectacular.utils import extend_schema 2 | from rest_framework import mixins, viewsets 3 | 4 | from api.contributions.serializers import CommentWriteSerializer 5 | from contributions.models import Comment 6 | 7 | 8 | class ContributionViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): 9 | queryset = Comment.objects.published() 10 | serializer_class = CommentWriteSerializer 11 | 12 | @extend_schema(summary="Nouvelle contribution", tags=[Comment._meta.verbose_name], exclude=True) 13 | def create(self, request, *args, **kwargs): 14 | return super().create(request, args, kwargs) 15 | -------------------------------------------------------------------------------- /api/tags/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from tags.models import Tag 4 | 5 | 6 | class TagSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = Tag 9 | fields = [ 10 | "id", 11 | "name", 12 | "description", 13 | "created", 14 | ] # "question_count", "question_public_validated_count", "updated" 15 | 16 | 17 | class TagStringSerializer(serializers.ModelSerializer): 18 | def to_representation(self, value): 19 | return value.name 20 | 21 | class Meta: 22 | model = Tag 23 | fields = ["name"] 24 | -------------------------------------------------------------------------------- /quizs/migrations/0006_rename_quiz_author.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-07 09:45 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("quizs", "0005_alter_quiz_timestamps"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name="quiz", 15 | old_name="author", 16 | new_name="author_old", 17 | ), 18 | migrations.RenameField( 19 | model_name="quiz", 20 | old_name="author_link", 21 | new_name="author", 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /users/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from users.models import User, UserCard 4 | 5 | 6 | class ProfileInfoCardCreateForm(forms.ModelForm): 7 | class Meta: 8 | model = UserCard 9 | fields = ["short_biography", "quiz_relationship", "website_url"] 10 | 11 | 12 | class ContributorCreateForm(forms.ModelForm): 13 | class Meta: 14 | model = User 15 | fields = ["first_name", "last_name", "email", "roles"] # password auto-generated in admin/views.py 16 | 17 | def __init__(self, *args, **kwargs): 18 | super().__init__(*args, **kwargs) 19 | self.fields["roles"].required = True 20 | -------------------------------------------------------------------------------- /core/migrations/0003_configuration_helloasso_url.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.9 on 2025-03-18 15:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0002_alter_configuration_application_open_source_code_url"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="configuration", 14 | name="helloasso_url", 15 | field=models.URLField( 16 | blank=True, help_text="Le lien vers la page HelloAsso de l'association", max_length=500 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /users/migrations/0011_user_profile_comments_last_seen_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-09-24 13:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("users", "0010_user_profile_home_last_seen_date"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="user", 14 | name="profile_comments_last_seen_date", 15 | field=models.DateTimeField( 16 | blank=True, null=True, verbose_name="Last seen date on page 'Comments on my content'" 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /glossary/migrations/0004_remove_glossaryitem_added_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1a1 on 2022-05-21 11:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("glossary", "0003_alter_glossaryitem_timestamps"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="glossaryitem", 15 | name="added", 16 | ), 17 | migrations.AddConstraint( 18 | model_name="glossaryitem", 19 | constraint=models.UniqueConstraint(fields=("name",), name="glossary_name_unique"), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /www/admin/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | from www.admin.views import AdminContributorCreateView, AdminContributorListView, AdminHistoryListView, AdminHomeView 4 | 5 | 6 | app_name = "admin" 7 | 8 | urlpatterns = [ 9 | path("", AdminHomeView.as_view(), name="home"), 10 | path( 11 | "contributors/", 12 | include( 13 | [ 14 | path("", AdminContributorListView.as_view(), name="contributor_list"), 15 | path("create/", AdminContributorCreateView.as_view(), name="contributor_create"), 16 | ] 17 | ), 18 | ), 19 | path("history/", AdminHistoryListView.as_view(), name="history"), 20 | ] 21 | -------------------------------------------------------------------------------- /api/glossary/views.py: -------------------------------------------------------------------------------- 1 | from drf_spectacular.utils import extend_schema 2 | from rest_framework import mixins, viewsets 3 | 4 | from api.glossary.filters import GlossaryItemFilter 5 | from api.glossary.serializers import GlossaryItemSerializer 6 | from glossary.models import GlossaryItem 7 | 8 | 9 | class GlossaryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): 10 | queryset = GlossaryItem.objects.all() 11 | serializer_class = GlossaryItemSerializer 12 | filterset_class = GlossaryItemFilter 13 | 14 | @extend_schema(summary="Glossaire", tags=[GlossaryItem._meta.verbose_name]) 15 | def list(self, request, *args, **kwargs): 16 | return super().list(request, args, kwargs) 17 | -------------------------------------------------------------------------------- /api/categories/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from categories.models import Category 4 | 5 | 6 | class CategorySerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = Category 9 | fields = [ 10 | "id", 11 | "name", 12 | "name_long", 13 | "description", 14 | "created", 15 | ] # "question_count", "question_public_validated_count", "updated" 16 | 17 | 18 | class CategoryStringSerializer(serializers.ModelSerializer): 19 | def to_representation(self, value): 20 | return value.name 21 | 22 | class Meta: 23 | model = Category 24 | fields = ["name"] 25 | -------------------------------------------------------------------------------- /templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% translate "Error" %} 403{{ block.super }}{% endblock %} 5 | 6 | {% block content %} 7 |
    8 |
    9 |
    10 | 11 |

    Vous ne pouvez pas continuer

    12 | 13 | {% if exception %} 14 |
    15 |

    {{ exception }}

    16 |
    17 | {% else %} 18 |

    Accès refusé

    19 | {% endif %} 20 | 21 |
    22 |
    23 |
    24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/pascalgn/automerge-action 2 | name: automerge 3 | on: 4 | pull_request: 5 | types: 6 | - labeled 7 | check_suite: 8 | types: 9 | - completed 10 | jobs: 11 | automerge: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: automerge 15 | uses: "pascalgn/automerge-action@v0.12.0" 16 | env: 17 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 18 | MERGE_LABELS: "automerge" 19 | MERGE_METHOD: "squash" 20 | MERGE_COMMIT_MESSAGE: "{pullRequest.title} - #{pullRequest.number}" 21 | MERGE_FORKS: false 22 | MERGE_RETRIES: 6 23 | MERGE_RETRY_SLEEP: 30000 24 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /templates/profile/quizs_view.html: -------------------------------------------------------------------------------- 1 | {% extends "profile/quizs_base.html" %} 2 | {% load render_table from django_tables2 %} 3 | {% load i18n %} 4 | 5 | {% block profile_quizs_content %} 6 |
    7 |
    8 |

    {% translate "All my quizs" %} {{ user_quizs.count }}

    9 |
    10 |
    11 | {% translate "Add" %} 12 |
    13 |
    14 |
    15 |
    16 | {% render_table table %} 17 |
    18 |
    19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /templates/quizs/detail_stats.html: -------------------------------------------------------------------------------- 1 | {% extends "quizs/detail_base.html" %} 2 | {% load i18n custom_filters get_verbose_name %} 3 | 4 | {% block quiz_detail_content %} 5 | {% for field in quiz_agg_stat_dict %} 6 |
    7 |
    8 | {% get_verbose_name quiz_stats field %} 9 |
    10 |
    11 | {{ quiz_agg_stat_dict|get_obj_attr:field|default:"" }} 12 |
    13 |
    14 | {% endfor %} 15 | 16 |
    17 | 18 |

    19 | Statistiques avancées 20 | (être patient…) 21 |

    22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /contributions/migrations/0012_rename_contribution_comment.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-03-20 15:53 2 | 3 | from django.conf import settings 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("questions", "0028_alter_historicalquestion_history_changed_fields"), 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ("quizs", "0028_alter_historicalquiz_history_changed_fields"), 13 | ("contributions", "0011_contribution_model_translation"), 14 | ] 15 | 16 | operations = [ 17 | migrations.RenameModel( 18 | old_name="Contribution", 19 | new_name="Comment", 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /history/utilities.py: -------------------------------------------------------------------------------- 1 | def get_diff_between_two_history_records(new_record, old_record=None, excluded_fields=None, returns="delta"): 2 | if not old_record: 3 | if new_record.prev_record: 4 | old_record = new_record.prev_record 5 | else: 6 | raise Exception("get_diff_between_two_history_records old_record not found") 7 | 8 | delta = new_record.diff_against(old_record, excluded_fields=excluded_fields) 9 | 10 | if returns == "changed_fields": 11 | # flat list of fields 12 | return delta.changed_fields 13 | elif returns == "changes": 14 | # object list : {"field": "test", "old": "previous value", new": "fresh value"} 15 | return delta.changes 16 | return delta 17 | -------------------------------------------------------------------------------- /core/templatetags/array_choices_display.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.utils.encoding import force_str 3 | 4 | from users import constants 5 | 6 | 7 | register = template.Library() 8 | 9 | 10 | @register.simple_tag 11 | def array_choices_display(obj, field): 12 | """Pretty rendering of ArrayField with choices.""" 13 | 14 | choices_dict = dict() 15 | 16 | if field == "roles": 17 | choices_dict = dict(constants.USER_ROLE_CHOICES) 18 | 19 | try: 20 | keys = obj.get(field, []) 21 | except: # noqa 22 | keys = getattr(obj, field, []) 23 | 24 | value_display_list = [force_str(choices_dict.get(key, "")) for key in (keys or [])] 25 | return ", ".join(filter(None, value_display_list)) 26 | -------------------------------------------------------------------------------- /tags/migrations/0003_alter_tag_timestamps.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-01 21:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("tags", "0002_add_verbose_names"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="tag", 15 | name="updated", 16 | field=models.DateTimeField(auto_now=True, verbose_name="Date de dernière modification"), 17 | ), 18 | migrations.AlterField( 19 | model_name="tag", 20 | name="created", 21 | field=models.DateTimeField(auto_now_add=True, verbose_name="Date de création"), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /users/migrations/0004_alter_user_timestamps.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-01 21:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("users", "0003_user_roles"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="user", 15 | name="created", 16 | field=models.DateTimeField(auto_now_add=True, verbose_name="Date de création"), 17 | ), 18 | migrations.AlterField( 19 | model_name="user", 20 | name="updated", 21 | field=models.DateTimeField(auto_now=True, verbose_name="Date de dernière modification"), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /glossary/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from core.admin import ExportMixin, admin_site 4 | from glossary.models import GlossaryItem 5 | 6 | 7 | class GlossaryAdmin(ExportMixin, admin.ModelAdmin): 8 | list_display = ( 9 | "id", 10 | "name", 11 | "definition_short", 12 | "language", 13 | "created", 14 | ) 15 | 16 | readonly_fields = ["created", "updated"] 17 | 18 | def has_add_permission(self, request, obj=None): 19 | return False 20 | 21 | def has_change_permission(self, request, obj=None): 22 | return False 23 | 24 | def has_delete_permission(self, request, obj=None): 25 | return False 26 | 27 | 28 | admin_site.register(GlossaryItem, GlossaryAdmin) 29 | -------------------------------------------------------------------------------- /questions/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from categories.factories import CategoryFactory 4 | from core import constants 5 | from questions.models import Question 6 | 7 | 8 | class QuestionFactory(factory.django.DjangoModelFactory): 9 | class Meta: 10 | model = Question 11 | 12 | text = "La question" 13 | type = constants.QUESTION_TYPE_QCM 14 | difficulty = constants.QUESTION_DIFFICULTY_EASY 15 | language = constants.LANGUAGE_FRENCH 16 | category = factory.SubFactory(CategoryFactory, name="Energie") 17 | answer_choice_a = "La réponse A" 18 | answer_choice_b = "La réponse B" 19 | answer_correct = "a" # constants.QUESTION_ANSWER_CHOICE_LIST[0] 20 | validation_status = constants.VALIDATION_STATUS_VALIDATED 21 | -------------------------------------------------------------------------------- /core/migrations/0002_alter_configuration_application_open_source_code_url.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-18 21:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("core", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="configuration", 15 | name="application_open_source_code_url", 16 | field=models.URLField( 17 | default="https://github.com/quiz-anthropocene", 18 | editable=False, 19 | help_text="Le lien vers le code de l'application", 20 | max_length=500, 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /core/fields.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.postgres.fields import ArrayField 3 | 4 | 5 | # https://stackoverflow.com/a/66059615/4293684 6 | class ChoiceArrayField(ArrayField): 7 | """ 8 | Custom ArrayField with a ChoiceField as default field. 9 | The default field is a comma-separated InputText, which is not very useful. 10 | """ 11 | 12 | def formfield(self, **kwargs): 13 | defaults = { 14 | "form_class": forms.TypedMultipleChoiceField, 15 | "choices": self.base_field.choices, 16 | "coerce": self.base_field.to_python, 17 | "widget": forms.CheckboxSelectMultiple, 18 | } 19 | defaults.update(kwargs) 20 | return super(ArrayField, self).formfield(**defaults) 21 | -------------------------------------------------------------------------------- /activity/migrations/0004_alter_event_actor_id_object_id_allow_null.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2 on 2023-04-28 18:59 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("activity", "0003_event_model_translate"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="event", 15 | name="actor_id", 16 | field=models.IntegerField(blank=True, null=True, verbose_name="Actor ID"), 17 | ), 18 | migrations.AlterField( 19 | model_name="event", 20 | name="event_object_id", 21 | field=models.IntegerField(blank=True, null=True, verbose_name="Object ID"), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /questions/migrations/0005_alter_question_timestamps.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-01 21:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("questions", "0004_add_verbose_names"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="question", 15 | name="created", 16 | field=models.DateTimeField(auto_now_add=True, verbose_name="Date de création"), 17 | ), 18 | migrations.AlterField( 19 | model_name="question", 20 | name="updated", 21 | field=models.DateTimeField(auto_now=True, verbose_name="Date de dernière modification"), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DEBUG=True # False for prod 2 | 3 | SECRET_KEY=yourappsecret 4 | 5 | # DATABASE_URL="sqlite:///db.sqlite3" 6 | DATABASE_URL = "postgres://quiz_anthropocene_team:password@localhost:/quiz_anthropocene" 7 | 8 | # Security measures 9 | SESSION_COOKIE_SECURE = False # True in prod 10 | CSRF_COOKIE_SECURE = False # True in prod 11 | SECURE_HSTS_SECONDS = 0 # should be more than 31556952 (one year) in prod 12 | SECURE_SSL_REDIRECT = False # True in prod 13 | 14 | # Github API 15 | GITHUB_ACCESS_TOKEN = "" 16 | 17 | # Notion.so API 18 | NOTION_API_SECRET = "" 19 | NOTION_QUESTION_TABLE_ID = "" 20 | NOTION_CONTRIBUTION_TABLE_ID = "" 21 | IMPORT_DATA_FROM_NOTION = False 22 | 23 | # Sendinblue API 24 | SIB_API_KEY = "" 25 | SIB_NEWSLETTER_LIST_ID= 26 | SIB_NEWSLETTER_DOI_TEMPLATE_ID= 27 | -------------------------------------------------------------------------------- /api/quizs/filters.py: -------------------------------------------------------------------------------- 1 | import django_filters 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from core import constants 5 | from tags.models import Tag 6 | from users.models import User 7 | 8 | 9 | class QuizFilter(django_filters.FilterSet): 10 | language = django_filters.MultipleChoiceFilter(label=_("Language(s)"), choices=constants.LANGUAGE_CHOICES) 11 | tags = django_filters.ModelMultipleChoiceFilter( 12 | label=_("Tag(s)"), 13 | queryset=Tag.objects.all(), 14 | ) 15 | authors = django_filters.ModelMultipleChoiceFilter( 16 | label=_("Author(s)"), 17 | queryset=User.objects.all_contributors(), 18 | ) 19 | spotlight = django_filters.BooleanFilter() 20 | # TODO: QuizFullSerializer, QuizWithQuestionOrderSerializer 21 | -------------------------------------------------------------------------------- /glossary/migrations/0003_alter_glossaryitem_timestamps.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-01 21:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("glossary", "0002_add_verbose_names"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="glossaryitem", 15 | name="created", 16 | field=models.DateTimeField(auto_now_add=True, verbose_name="Date de création"), 17 | ), 18 | migrations.AlterField( 19 | model_name="glossaryitem", 20 | name="updated", 21 | field=models.DateTimeField(auto_now=True, verbose_name="Date de dernière modification"), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /users/migrations/0010_user_profile_home_last_seen_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-09-24 12:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("users", "0009_user_logs"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="user", 14 | name="profile_home_last_seen_date", 15 | field=models.DateTimeField(blank=True, null=True, verbose_name="Last seen date on page 'My space'"), 16 | ), 17 | migrations.AlterField( 18 | model_name="user", 19 | name="logs", 20 | field=models.JSONField(default=list, editable=False, verbose_name="Historical logs"), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /glossary/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from glossary.models import GlossaryItem 4 | 5 | 6 | GLOSSARY_ITEM_FORM_FIELDS = [ 7 | field.name for field in GlossaryItem._meta.fields if field.name not in GlossaryItem.GLOSSARY_ITEM_READONLY_FIELDS 8 | ] 9 | 10 | 11 | class GlossaryItemCreateForm(forms.ModelForm): 12 | class Meta: 13 | model = GlossaryItem 14 | fields = GLOSSARY_ITEM_FORM_FIELDS 15 | widgets = { 16 | "name_alternatives": forms.Textarea(attrs={"rows": 1}), 17 | "description": forms.Textarea(attrs={"rows": 3}), 18 | } 19 | 20 | 21 | class GlossaryItemEditForm(GlossaryItemCreateForm): 22 | def __init__(self, *args, **kwargs): 23 | super().__init__(*args, **kwargs) 24 | self.fields["name"].disabled = True 25 | -------------------------------------------------------------------------------- /quizs/migrations/0015_quiz_publish_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1a1 on 2022-07-21 10:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("quizs", "0014_alter_historicalquiz_language_alter_quiz_language"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="historicalquiz", 15 | name="publish_date", 16 | field=models.DateTimeField(blank=True, null=True, verbose_name="Date de publication"), 17 | ), 18 | migrations.AddField( 19 | model_name="quiz", 20 | name="publish_date", 21 | field=models.DateTimeField(blank=True, null=True, verbose_name="Date de publication"), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /questions/migrations/0015_question_validation_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1a1 on 2022-07-21 10:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("questions", "0014_alter_historicalquestion_language_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="historicalquestion", 15 | name="validation_date", 16 | field=models.DateTimeField(blank=True, null=True, verbose_name="Date de validation"), 17 | ), 18 | migrations.AddField( 19 | model_name="question", 20 | name="validation_date", 21 | field=models.DateTimeField(blank=True, null=True, verbose_name="Date de validation"), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /tags/filters.py: -------------------------------------------------------------------------------- 1 | import django_filters 2 | from django import forms 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from tags.models import Tag 6 | 7 | 8 | TEXT_SEARCH_PLACEHOLDER = f"{_('In the following fields:')} " f"{Tag._meta.get_field('name').verbose_name}" 9 | 10 | 11 | class TagFilter(django_filters.FilterSet): 12 | q = django_filters.CharFilter( 13 | label=_("Text search"), 14 | method="text_search", 15 | widget=forms.TextInput(attrs={"placeholder": TEXT_SEARCH_PLACEHOLDER}), 16 | ) 17 | 18 | class Meta: 19 | model = Tag 20 | fields = ["q"] 21 | 22 | def text_search(self, queryset, name, value): 23 | if not value: 24 | return queryset 25 | # normal name filtering 26 | return queryset.filter(name__icontains=value) 27 | -------------------------------------------------------------------------------- /contributions/migrations/0007_contribution_parent.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1a1 on 2022-05-22 17:33 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("contributions", "0006_contribution_status_and_type"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="contribution", 16 | name="parent", 17 | field=models.ForeignKey( 18 | blank=True, 19 | null=True, 20 | on_delete=django.db.models.deletion.CASCADE, 21 | related_name="replies", 22 | to="contributions.contribution", 23 | verbose_name="En réponse à", 24 | ), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /quizs/migrations/0013_historicalquiz_history_changed_fields.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1a1 on 2022-05-26 14:38 2 | 3 | import django.contrib.postgres.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("quizs", "0012_alter_historicalquiz_options_and_more"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="historicalquiz", 16 | name="history_changed_fields", 17 | field=django.contrib.postgres.fields.ArrayField( 18 | base_field=models.CharField(max_length=50), 19 | blank=True, 20 | default=list, 21 | size=None, 22 | verbose_name="Champs modifiés", 23 | ), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /quizs/migrations/0028_alter_historicalquiz_history_changed_fields.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-23 09:36 2 | 3 | import django.contrib.postgres.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("quizs", "0027_quiz_visibility_translation_fix"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="historicalquiz", 16 | name="history_changed_fields", 17 | field=django.contrib.postgres.fields.ArrayField( 18 | base_field=models.CharField(max_length=50), 19 | blank=True, 20 | default=list, 21 | size=None, 22 | verbose_name="Changed fields", 23 | ), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /glossary/migrations/0006_historicalglossaryitem_history_changed_fields.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1a1 on 2022-05-26 13:57 2 | 3 | import django.contrib.postgres.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("glossary", "0005_historicalglossaryitem"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="historicalglossaryitem", 16 | name="history_changed_fields", 17 | field=django.contrib.postgres.fields.ArrayField( 18 | base_field=models.CharField(max_length=50), 19 | blank=True, 20 | default=list, 21 | size=None, 22 | verbose_name="Champs modifiés", 23 | ), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /quizs/migrations/0020_remove_historicalquiz_author_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-09-24 16:25 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("quizs", "0019_forward_data_author_to_authors"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="historicalquiz", 15 | name="author", 16 | ), 17 | migrations.RemoveField( 18 | model_name="historicalquiz", 19 | name="author_string", 20 | ), 21 | migrations.RemoveField( 22 | model_name="quiz", 23 | name="author", 24 | ), 25 | migrations.RemoveField( 26 | model_name="quiz", 27 | name="author_string", 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /questions/migrations/0013_historicalquestion_history_changed_fields.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1a1 on 2022-05-26 14:38 2 | 3 | import django.contrib.postgres.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("questions", "0012_alter_historicalquestion_options_and_more"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="historicalquestion", 16 | name="history_changed_fields", 17 | field=django.contrib.postgres.fields.ArrayField( 18 | base_field=models.CharField(max_length=50), 19 | blank=True, 20 | default=list, 21 | size=None, 22 | verbose_name="Champs modifiés", 23 | ), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /questions/migrations/0028_alter_historicalquestion_history_changed_fields.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-23 09:36 2 | 3 | import django.contrib.postgres.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("questions", "0027_question_visibility_translation_fix"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="historicalquestion", 16 | name="history_changed_fields", 17 | field=django.contrib.postgres.fields.ArrayField( 18 | base_field=models.CharField(max_length=50), 19 | blank=True, 20 | default=list, 21 | size=None, 22 | verbose_name="Changed fields", 23 | ), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /glossary/migrations/0008_alter_historicalglossaryitem_history_changed_fields.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-23 09:36 2 | 3 | import django.contrib.postgres.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("glossary", "0007_alter_glossaryitem_created_and_more"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="historicalglossaryitem", 16 | name="history_changed_fields", 17 | field=django.contrib.postgres.fields.ArrayField( 18 | base_field=models.CharField(max_length=50), 19 | blank=True, 20 | default=list, 21 | size=None, 22 | verbose_name="Changed fields", 23 | ), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /questions/migrations/0016_alter_historicalquestion_created_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-08-17 14:19 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("questions", "0015_question_validation_date"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="historicalquestion", 16 | name="created", 17 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name="Date de création"), 18 | ), 19 | migrations.AlterField( 20 | model_name="question", 21 | name="created", 22 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name="Date de création"), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /core/migrations/0004_configuration_office_address_and_url.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.9 on 2025-03-18 16:41 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0003_configuration_helloasso_url"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="configuration", 14 | name="office_address", 15 | field=models.CharField(blank=True, help_text="L'adresse du bureau de l'association", max_length=255), 16 | ), 17 | migrations.AddField( 18 | model_name="configuration", 19 | name="office_url", 20 | field=models.URLField( 21 | blank=True, help_text="Le lien vers la page du bureau de l'association", max_length=500 22 | ), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /glossary/filters.py: -------------------------------------------------------------------------------- 1 | import django_filters 2 | from django import forms 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from glossary.models import GlossaryItem 6 | 7 | 8 | TEXT_SEARCH_PLACEHOLDER = f"{_('In the following fields:')} " f"{GlossaryItem._meta.get_field('name').verbose_name}" 9 | 10 | 11 | class GlossaryItemFilter(django_filters.FilterSet): 12 | q = django_filters.CharFilter( 13 | label=_("Text search"), 14 | method="text_search", 15 | widget=forms.TextInput(attrs={"placeholder": TEXT_SEARCH_PLACEHOLDER}), 16 | ) 17 | 18 | class Meta: 19 | model = GlossaryItem 20 | fields = ["language", "q"] 21 | 22 | def text_search(self, queryset, name, value): 23 | if not value: 24 | return queryset 25 | # normal name filtering 26 | return queryset.filter(name__icontains=value) 27 | -------------------------------------------------------------------------------- /glossary/migrations/0007_alter_glossaryitem_created_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-08-17 14:19 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("glossary", "0006_historicalglossaryitem_history_changed_fields"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="glossaryitem", 16 | name="created", 17 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name="Date de création"), 18 | ), 19 | migrations.AlterField( 20 | model_name="historicalglossaryitem", 21 | name="created", 22 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name="Date de création"), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /stats/migrations/0004_dailystat_question_public_and_quiz_public_answer_count.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-11 21:12 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("stats", "0003_quizanswerevent_question_answer_split"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="dailystat", 15 | name="question_public_answer_count", 16 | field=models.PositiveIntegerField(default=0, help_text="Le nombre de questions publiques répondues"), 17 | ), 18 | migrations.AddField( 19 | model_name="dailystat", 20 | name="quiz_public_answer_count", 21 | field=models.PositiveIntegerField(default=0, help_text="Le nombre de quizs publiques répondus"), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /templates/includes/_language_form.html: -------------------------------------------------------------------------------- 1 | {% load i18n get_language_flag %} 2 | 3 | {% get_current_language as LANGUAGE_CODE %} 4 | {% get_available_languages as LANGUAGES %} 5 | {% get_language_info_list for LANGUAGES as languages %} 6 | 7 | {% if languages|length > 1 %} 8 |
    9 | {% csrf_token %} 10 |
    11 | 18 |
    19 |
    20 | {% endif %} 21 | -------------------------------------------------------------------------------- /contributions/migrations/0014_comment_remove_status_replied.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-03-21 15:26 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("contributions", "0013_comment_meta_and_rename_fks"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="comment", 15 | name="status", 16 | field=models.CharField( 17 | blank=True, 18 | choices=[ 19 | ("NEW", "To process"), 20 | ("PENDING", "In progress"), 21 | ("PROCESSED", "Processed"), 22 | ("IGNORED", "Ignored"), 23 | ], 24 | max_length=150, 25 | verbose_name="Status", 26 | ), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /quizs/migrations/0002_quiz_author_link.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-04-16 09:19 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ("quizs", "0001_initial"), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name="quiz", 18 | name="author_link", 19 | field=models.ForeignKey( 20 | help_text="L'auteur du quiz", 21 | blank=True, 22 | null=True, 23 | on_delete=django.db.models.deletion.SET_NULL, 24 | related_name="quizs", 25 | to=settings.AUTH_USER_MODEL, 26 | ), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /stats/urls.py: -------------------------------------------------------------------------------- 1 | from rest_framework import routers 2 | 3 | from stats.views import ( 4 | LinkClickEventViewSet, 5 | QuestionAnswerEventViewSet, 6 | QuestionFeedbackEventViewSet, 7 | QuizAnswerEventViewSet, 8 | QuizFeedbackEventViewSet, 9 | ) 10 | 11 | 12 | app_name = "stats" 13 | 14 | router = routers.DefaultRouter() 15 | router.register(r"question-answer-event", QuestionAnswerEventViewSet, basename="question-answer-event") 16 | router.register(r"question-feedback-event", QuestionFeedbackEventViewSet, basename="question-feedback-event") 17 | router.register(r"quiz-answer-event", QuizAnswerEventViewSet, basename="quiz-answer-event") 18 | router.register(r"quiz-feedback-event", QuizFeedbackEventViewSet, basename="quiz-feedback-event") 19 | router.register(r"link-click-event", LinkClickEventViewSet, basename="link-click-event") 20 | 21 | urlpatterns = [] 22 | 23 | urlpatterns += router.urls 24 | -------------------------------------------------------------------------------- /templates/includes/_paginator.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 26 | -------------------------------------------------------------------------------- /categories/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from categories.models import Category 4 | from core.admin import ExportMixin, admin_site 5 | 6 | 7 | class CategoryAdmin(ExportMixin, admin.ModelAdmin): 8 | list_display = ( 9 | "id", 10 | "name", 11 | "name_long", 12 | "question_count", 13 | "question_public_validated_count", 14 | "created", 15 | ) 16 | search_fields = ("name",) 17 | ordering = ("id",) 18 | actions = [ 19 | "export_as_csv", 20 | "export_as_json", 21 | "export_as_yaml", 22 | "export_all_category_as_yaml", 23 | ] 24 | 25 | readonly_fields = ["created", "updated"] 26 | 27 | def get_queryset(self, request): 28 | qs = super().get_queryset(request) 29 | qs = qs.prefetch_related("questions") 30 | return qs 31 | 32 | 33 | admin_site.register(Category, CategoryAdmin) 34 | -------------------------------------------------------------------------------- /contributions/migrations/0004_contribution_author.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-01 13:46 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ("contributions", "0003_add_verbose_name"), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name="contribution", 18 | name="author", 19 | field=models.ForeignKey( 20 | blank=True, 21 | null=True, 22 | on_delete=django.db.models.deletion.SET_NULL, 23 | related_name="contributions", 24 | to=settings.AUTH_USER_MODEL, 25 | verbose_name="Auteur", 26 | ), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /contributions/migrations/0008_alter_contribution_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1a1 on 2022-05-25 21:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("contributions", "0007_contribution_parent"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="contribution", 15 | name="status", 16 | field=models.CharField( 17 | blank=True, 18 | choices=[ 19 | ("NEW", "À traiter"), 20 | ("PENDING", "En cours"), 21 | ("PROCESSED", "Traité"), 22 | ("REPLIED", "Répondu"), 23 | ("IGNORED", "Ignoré"), 24 | ], 25 | max_length=150, 26 | verbose_name="Statut", 27 | ), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /quizs/migrations/0012_alter_historicalquiz_options_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1a1 on 2022-05-21 11:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("quizs", "0011_quizrelationship_updated"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="historicalquiz", 15 | options={ 16 | "get_latest_by": ("history_date", "history_id"), 17 | "ordering": ("-history_date", "-history_id"), 18 | "verbose_name": "historical Quiz", 19 | "verbose_name_plural": "historical Quizs", 20 | }, 21 | ), 22 | migrations.AlterField( 23 | model_name="historicalquiz", 24 | name="history_date", 25 | field=models.DateTimeField(db_index=True), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /tags/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from core.admin import ExportMixin, admin_site 4 | from tags.models import Tag 5 | 6 | 7 | class TagAdmin(ExportMixin, admin.ModelAdmin): 8 | list_display = ( 9 | "id", 10 | "name", 11 | "question_count", 12 | "question_public_validated_count", 13 | "quiz_count", 14 | "quiz_public_published_count", 15 | "created", 16 | ) 17 | search_fields = ("name",) 18 | ordering = ("name",) 19 | actions = [ 20 | "export_as_csv", 21 | "export_as_json", 22 | "export_as_yaml", 23 | "export_all_tag_as_yaml", 24 | ] 25 | 26 | readonly_fields = ["created", "updated"] 27 | 28 | def get_queryset(self, request): 29 | qs = super().get_queryset(request) 30 | qs = qs.prefetch_related("questions", "quizs") 31 | return qs 32 | 33 | 34 | admin_site.register(Tag, TagAdmin) 35 | -------------------------------------------------------------------------------- /tags/migrations/0002_add_verbose_names.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-04-30 16:12 2 | 3 | import ckeditor.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("tags", "0001_initial"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="tag", 16 | name="created", 17 | field=models.DateField(auto_now_add=True, verbose_name="Date de création"), 18 | ), 19 | migrations.AlterField( 20 | model_name="tag", 21 | name="description", 22 | field=ckeditor.fields.RichTextField(blank=True, verbose_name="Description"), 23 | ), 24 | migrations.AlterField( 25 | model_name="tag", 26 | name="name", 27 | field=models.CharField(max_length=50, verbose_name="Nom"), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /quizs/migrations/0019_forward_data_author_to_authors.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-08-26 10:48 2 | 3 | from django.db import migrations, models 4 | from django.db.models.functions import Concat 5 | 6 | 7 | def forward(apps, schema_editor): 8 | Quiz = apps.get_model("quizs", "Quiz") 9 | for quiz in Quiz.objects.all(): 10 | quiz.authors.set([quiz.author]) 11 | quiz_author_list = list( 12 | quiz.authors.annotate(fullname=Concat("first_name", models.Value(" "), "last_name")).values_list( 13 | "fullname", flat=True 14 | ) 15 | ) 16 | Quiz.objects.filter(id=quiz.id).update(author_list=quiz_author_list) # avoid updating 'updated' field 17 | 18 | 19 | class Migration(migrations.Migration): 20 | 21 | dependencies = [ 22 | ("quizs", "0018_quizauthor_historicalquiz_author_list_and_more"), 23 | ] 24 | 25 | operations = [migrations.RunPython(forward)] 26 | -------------------------------------------------------------------------------- /questions/migrations/0012_alter_historicalquestion_options_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1a1 on 2022-05-21 11:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("questions", "0011_question_visibility"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="historicalquestion", 15 | options={ 16 | "get_latest_by": ("history_date", "history_id"), 17 | "ordering": ("-history_date", "-history_id"), 18 | "verbose_name": "historical Question", 19 | "verbose_name_plural": "historical Questions", 20 | }, 21 | ), 22 | migrations.AlterField( 23 | model_name="historicalquestion", 24 | name="history_date", 25 | field=models.DateTimeField(db_index=True), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /categories/migrations/0003_alter_category_timestamps.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-01 21:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("categories", "0002_add_verbose_names"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="category", 15 | name="updated", 16 | field=models.DateTimeField(auto_now=True, verbose_name="Date de dernière modification"), 17 | ), 18 | migrations.AlterField( 19 | model_name="category", 20 | name="created", 21 | field=models.DateTimeField(auto_now_add=True, verbose_name="Date de création"), 22 | ), 23 | migrations.AlterModelOptions( 24 | name="category", 25 | options={"ordering": ["pk"], "verbose_name": "Catégorie", "verbose_name_plural": "Catégories"}, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /tags/migrations/0005_tag_model_translation.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-22 18:47 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("tags", "0004_alter_tag_created"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="tag", 16 | name="created", 17 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name="Creation date"), 18 | ), 19 | migrations.AlterField( 20 | model_name="tag", 21 | name="name", 22 | field=models.CharField(max_length=50, verbose_name="Name"), 23 | ), 24 | migrations.AlterField( 25 | model_name="tag", 26 | name="updated", 27 | field=models.DateTimeField(auto_now=True, verbose_name="Last update date"), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /activity/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from activity.models import Event 4 | from core.admin import admin_site 5 | 6 | 7 | class EventAdmin(admin.ModelAdmin): 8 | list_display = ( 9 | "id", 10 | "actor_name", 11 | "event_verb", 12 | "event_object_type", 13 | "event_object_name", 14 | "created", 15 | ) 16 | list_filter = ( 17 | "event_object_type", 18 | "event_verb", 19 | "actor_name", 20 | ) 21 | search_fields = ( 22 | "actor_name", 23 | "event_object_type", 24 | "event_object_name", 25 | ) 26 | ordering = ("-created",) 27 | 28 | def has_add_permission(self, request, obj=None): 29 | return False 30 | 31 | def has_change_permission(self, request, obj=None): 32 | return False 33 | 34 | def has_delete_permission(self, request, obj=None): 35 | return False 36 | 37 | 38 | admin_site.register(Event, EventAdmin) 39 | -------------------------------------------------------------------------------- /api/glossary/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.urls import reverse 3 | 4 | from core import constants 5 | from glossary.factories import GlossaryItemFactory 6 | 7 | 8 | class GlossaryApiTest(TestCase): 9 | @classmethod 10 | def setUpTestData(cls): 11 | GlossaryItemFactory() 12 | GlossaryItemFactory(name="IPCC", language=constants.LANGUAGE_ENGLISH) 13 | 14 | def test_glossary_list(self): 15 | response = self.client.get(reverse("api:glossary-list")) 16 | self.assertEqual(response.status_code, 200) 17 | self.assertIsInstance(response.data["results"], list) 18 | self.assertEqual(len(response.data["results"]), 2) 19 | 20 | def test_glossary_list_filter_by_language(self): 21 | response = self.client.get(reverse("api:glossary-list"), {"language": constants.LANGUAGE_ENGLISH}) 22 | self.assertEqual(response.status_code, 200) 23 | self.assertEqual(len(response.data["results"]), 1) 24 | -------------------------------------------------------------------------------- /users/tables.py: -------------------------------------------------------------------------------- 1 | import django_tables2 as tables 2 | 3 | from core.tables import DEFAULT_ATTRS, DEFAULT_TEMPLATE, ArrayColumn 4 | from questions.models import Question 5 | from quizs.models import Quiz 6 | from users.models import User 7 | 8 | 9 | class ContributorTable(tables.Table): 10 | question_count = tables.Column(verbose_name=Question._meta.verbose_name_plural) 11 | quiz_count = tables.Column(verbose_name=Quiz._meta.verbose_name_plural) 12 | roles = ArrayColumn() 13 | 14 | class Meta: 15 | model = User 16 | fields = ["first_name", "last_name", "email", "roles", "question_count", "quiz_count", "last_login", "created"] 17 | template_name = DEFAULT_TEMPLATE 18 | attrs = DEFAULT_ATTRS 19 | 20 | 21 | class AdministratorTable(tables.Table): 22 | class Meta: 23 | model = User 24 | fields = ["first_name", "last_name", "email", "last_login"] 25 | template_name = DEFAULT_TEMPLATE 26 | attrs = DEFAULT_ATTRS 27 | -------------------------------------------------------------------------------- /www/auth/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import views as auth_views 2 | from django.urls import reverse_lazy 3 | 4 | from www.auth.forms import LoginForm, PasswordResetForm 5 | 6 | 7 | class LoginView(auth_views.LoginView): 8 | template_name = "auth/login.html" 9 | form_class = LoginForm 10 | redirect_authenticated_user = True 11 | # success_url = settings.LOGIN_REDIRECT_URL 12 | 13 | 14 | class PasswordResetView(auth_views.PasswordResetView): 15 | template_name = "auth/password_reset.html" 16 | form_class = PasswordResetForm 17 | success_url = reverse_lazy("auth:password_reset_sent") # see get_success_url() below 18 | email_template_name = "auth/password_reset_email_body.html" 19 | subject_template_name = "auth/password_reset_email_subject.txt" 20 | 21 | def get_success_url(self): 22 | success_url = super().get_success_url() 23 | user_email = self.request.POST.get("email") 24 | return f"{success_url}?email={user_email}" 25 | -------------------------------------------------------------------------------- /activity/migrations/0005_alter_event_event_object_type_monthly_agg_stat.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2 on 2023-06-06 17:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("activity", "0004_alter_event_actor_id_object_id_allow_null"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="event", 15 | name="event_object_type", 16 | field=models.CharField( 17 | blank=True, 18 | choices=[ 19 | ("QUESTION", "Question"), 20 | ("QUIZ", "Quiz"), 21 | ("USER", "Contributor"), 22 | ("WEEKLY_AGG_STAT", "Weekly statistics"), 23 | ("MONTHLY_AGG_STAT", "Monthly statistics"), 24 | ], 25 | max_length=50, 26 | verbose_name="Object type", 27 | ), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /questions/migrations/0006_rename_question_author_validator.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-07 09:45 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("questions", "0005_alter_question_timestamps"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name="question", 15 | old_name="author", 16 | new_name="author_old", 17 | ), 18 | migrations.RenameField( 19 | model_name="question", 20 | old_name="author_link", 21 | new_name="author", 22 | ), 23 | migrations.RenameField( 24 | model_name="question", 25 | old_name="validator", 26 | new_name="validator_old", 27 | ), 28 | migrations.RenameField( 29 | model_name="question", 30 | old_name="validator_link", 31 | new_name="validator", 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /quizs/migrations/0003_populate_quiz_author_link.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-04-16 10:17 2 | 3 | from django.db import migrations 4 | 5 | 6 | def link_authors(apps, schema_editor): 7 | Quiz = apps.get_model("quizs", "Quiz") 8 | User = apps.get_model("users", "User") 9 | for quiz in Quiz.objects.all(): 10 | author = None 11 | quiz_author_split = quiz.author.split(" ") 12 | try: 13 | author = User.objects.get(first_name=quiz_author_split[0], last_name=quiz_author_split[1]) 14 | except: # noqa 15 | try: 16 | author = User.objects.get(first_name=quiz.author) 17 | except: # noqa 18 | pass 19 | if author: 20 | quiz.author_link = author 21 | quiz.save() 22 | 23 | 24 | class Migration(migrations.Migration): 25 | 26 | dependencies = [ 27 | ("quizs", "0002_quiz_author_link"), 28 | ] 29 | 30 | operations = [ 31 | migrations.RunPython(link_authors), 32 | ] 33 | -------------------------------------------------------------------------------- /users/migrations/0003_user_roles.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-04-29 17:28 2 | 3 | from django.db import migrations, models 4 | 5 | import core.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("users", "0002_alter_user_managers"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name="user", 17 | name="roles", 18 | field=core.fields.ChoiceArrayField( 19 | base_field=models.CharField( 20 | choices=[ 21 | ("CONTRIBUTOR", "Contributeur"), 22 | ("SUPER-CONTRIBUTOR", "Super Contributeur"), 23 | ("ADMINISTRATOR", "Administrateur"), 24 | ], 25 | max_length=20, 26 | ), 27 | blank=True, 28 | default=list, 29 | size=None, 30 | verbose_name="Rôles", 31 | ), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /contributions/migrations/0016_comment_publish.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-03-22 11:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("contributions", "0015_historicalcomment"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="comment", 15 | name="publish", 16 | field=models.BooleanField( 17 | default=False, 18 | help_text="Display the comment publicly (and its reply if it exists)", 19 | verbose_name="Published?", 20 | ), 21 | ), 22 | migrations.AddField( 23 | model_name="historicalcomment", 24 | name="publish", 25 | field=models.BooleanField( 26 | default=False, 27 | help_text="Display the comment publicly (and its reply if it exists)", 28 | verbose_name="Published?", 29 | ), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /history/tables.py: -------------------------------------------------------------------------------- 1 | import django_tables2 as tables 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from core.tables import DEFAULT_ATTRS, DEFAULT_TEMPLATE 5 | 6 | 7 | class HistoryTable(tables.Table): 8 | history_date = tables.DateTimeColumn(verbose_name=_("Date")) 9 | history_user = tables.Column(verbose_name=_("Author")) 10 | object_model = tables.Column(verbose_name=_("Type")) 11 | # object_id = tables.Column(verbose_name=_("ID"), accessor="id") 12 | object_id = tables.TemplateColumn( 13 | verbose_name=_("ID"), 14 | template_name="history/_table_id_link.html", 15 | ) 16 | history_type = tables.Column(verbose_name=_("Action")) 17 | # history_id = tables.Column() 18 | history_changed_fields = tables.TemplateColumn( 19 | verbose_name=_("Changed fields"), 20 | template_name="history/_table_changed_fields_list.html", 21 | attrs={"td": {"style": "max-width:500px"}}, 22 | ) 23 | 24 | class Meta: 25 | template_name = DEFAULT_TEMPLATE 26 | attrs = DEFAULT_ATTRS 27 | -------------------------------------------------------------------------------- /templates/tags/detail_view.html: -------------------------------------------------------------------------------- 1 | {% extends "tags/detail_base.html" %} 2 | {% load i18n custom_filters get_verbose_name %} 3 | 4 | {% block tag_detail_content %} 5 |
    6 |
    7 |
    8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% for field in tag_dict %} 17 | 18 | 19 | 20 | 21 | {% endfor %} 22 | 23 |
    {% translate "Field" %}{% translate "Value" %}
    {% get_verbose_name tag field %}{{ tag_dict|get_obj_attr:field|default:"" }}
    24 |
    25 |
    26 |
    27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /templates/categories/detail_view.html: -------------------------------------------------------------------------------- 1 | {% extends "categories/detail_base.html" %} 2 | {% load i18n custom_filters get_verbose_name %} 3 | 4 | {% block category_detail_content %} 5 |
    6 |
    7 |
    8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% for field in category_dict %} 17 | 18 | 19 | 20 | 21 | {% endfor %} 22 | 23 |
    {% translate "Field" %}{% translate "Value" %}
    {% get_verbose_name category field %}{{ category_dict|get_obj_attr:field|default:"" }}
    24 |
    25 |
    26 |
    27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /questions/migrations/0017_rename_answer_audio_historicalquestion_answer_audio_url_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.3 on 2022-12-07 22:40 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("questions", "0016_alter_historicalquestion_created_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name="historicalquestion", 15 | old_name="answer_audio", 16 | new_name="answer_audio_url", 17 | ), 18 | migrations.RenameField( 19 | model_name="historicalquestion", 20 | old_name="answer_video", 21 | new_name="answer_video_url", 22 | ), 23 | migrations.RenameField( 24 | model_name="question", 25 | old_name="answer_audio", 26 | new_name="answer_audio_url", 27 | ), 28 | migrations.RenameField( 29 | model_name="question", 30 | old_name="answer_video", 31 | new_name="answer_video_url", 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /www/pages/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseRedirect 2 | from django.urls import reverse_lazy 3 | from django.views.generic import TemplateView 4 | 5 | from activity.models import Event 6 | from core.mixins import ContributorUserRequiredMixin 7 | 8 | 9 | class HomeView(TemplateView): # ContributorUserRequiredMixin ? 10 | template_name = "pages/home.html" 11 | 12 | def get(self, request, *args, **kwargs): 13 | if not request.user.is_authenticated: 14 | return HttpResponseRedirect(reverse_lazy("auth:login")) 15 | if request.user.is_authenticated and not request.user.has_role_contributor: 16 | return HttpResponseRedirect(reverse_lazy("pages:403")) 17 | return super().get(request, *args, **kwargs) 18 | 19 | def get_context_data(self, **kwargs): 20 | context = super().get_context_data(**kwargs) 21 | context["last_10_events"] = Event.objects.display(source="HOME").order_by("-created")[:10] 22 | return context 23 | 24 | 25 | class HelpView(ContributorUserRequiredMixin, TemplateView): 26 | template_name = "pages/help.html" 27 | -------------------------------------------------------------------------------- /core/templatetags/get_verbose_name.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.apps import apps 3 | from django.utils.safestring import SafeString 4 | 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.simple_tag 10 | def get_verbose_name(object, field_name=None): 11 | if type(object) in [str, SafeString]: 12 | if object == "Question": 13 | object = apps.get_model("questions", object) 14 | elif object == "Quiz": 15 | object = apps.get_model("quizs", object) 16 | elif object == "Tag": 17 | object = apps.get_model("tags", object) 18 | elif object == "Contribution": 19 | object = apps.get_model("contributions", object) 20 | elif object == "GlossaryItem": 21 | object = apps.get_model("glossary", object) 22 | try: 23 | if field_name: 24 | # if 'verbose_name' is not defined, it will return the 'field_name' 25 | return object._meta.get_field(field_name).verbose_name 26 | else: 27 | return object._meta.verbose_name 28 | except: # noqa 29 | return field_name 30 | -------------------------------------------------------------------------------- /core/templates/layouts/main.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block title %}{% endblock %} 11 | 12 | 13 | {% block extracss %} 14 | {% endblock %} 15 | 16 | 17 | 23 | 24 | 25 | 26 | {% block nav %} 27 | {% endblock nav %} 28 | 29 |
    30 | {% block content %} 31 | {% endblock %} 32 |
    33 | 34 | {% block footer %} 35 | {% endblock footer %} 36 | 37 | {% block extrajs %} 38 | {% endblock %} 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /quizs/migrations/0014_alter_historicalquiz_language_alter_quiz_language.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1a1 on 2022-07-07 17:08 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("quizs", "0013_historicalquiz_history_changed_fields"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="historicalquiz", 15 | name="language", 16 | field=models.CharField( 17 | choices=[("Français", "Français"), ("English", "English"), ("Deutsch", "Deutsch")], 18 | default="Français", 19 | max_length=50, 20 | verbose_name="Langue", 21 | ), 22 | ), 23 | migrations.AlterField( 24 | model_name="quiz", 25 | name="language", 26 | field=models.CharField( 27 | choices=[("Français", "Français"), ("English", "English"), ("Deutsch", "Deutsch")], 28 | default="Français", 29 | max_length=50, 30 | verbose_name="Langue", 31 | ), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /activity/migrations/0002_alter_event_created_alter_event_event_object_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-08-17 14:19 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("activity", "0001_initial"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="event", 16 | name="created", 17 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name="Date de création"), 18 | ), 19 | migrations.AlterField( 20 | model_name="event", 21 | name="event_object_type", 22 | field=models.CharField( 23 | blank=True, 24 | choices=[ 25 | ("QUESTION", "Question"), 26 | ("QUIZ", "Quiz"), 27 | ("USER", "Contributeur"), 28 | ("WEEKLY_AGG_STAT", "Statistiques de la semaine"), 29 | ], 30 | max_length=50, 31 | verbose_name="Type d'objet", 32 | ), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /categories/migrations/0002_add_verbose_names.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-04-30 16:12 2 | 3 | import ckeditor.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("categories", "0001_initial"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="category", 16 | name="created", 17 | field=models.DateField(auto_now_add=True, verbose_name="Date de création"), 18 | ), 19 | migrations.AlterField( 20 | model_name="category", 21 | name="description", 22 | field=ckeditor.fields.RichTextField(blank=True, verbose_name="Description"), 23 | ), 24 | migrations.AlterField( 25 | model_name="category", 26 | name="name", 27 | field=models.CharField(max_length=50, verbose_name="Nom"), 28 | ), 29 | migrations.AlterField( 30 | model_name="category", 31 | name="name_long", 32 | field=models.CharField(max_length=150, verbose_name="Nom (version longue)"), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /quizs/migrations/0021_quiz_language_migration.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-18 19:54 2 | 3 | from django.db import migrations 4 | 5 | 6 | def migrate_quiz_language(apps, schema_editor): 7 | Quiz = apps.get_model("quizs", "Quiz") 8 | Quiz.objects.filter(language="Français").update(language="FRENCH") 9 | Quiz.objects.filter(language="English").update(language="ENGLISH") 10 | Quiz.objects.filter(language="Deutsch").update(language="GERMAN") 11 | 12 | 13 | def migrate_historical_quiz_language(apps, schema_editor): 14 | HistoricalQuiz = apps.get_model("quizs", "HistoricalQuiz") 15 | HistoricalQuiz.objects.filter(language="Français").update(language="FRENCH") 16 | HistoricalQuiz.objects.filter(language="English").update(language="ENGLISH") 17 | HistoricalQuiz.objects.filter(language="Deutsch").update(language="GERMAN") 18 | 19 | 20 | class Migration(migrations.Migration): 21 | 22 | dependencies = [ 23 | ("quizs", "0020_remove_historicalquiz_author_and_more"), 24 | ] 25 | 26 | operations = [ 27 | migrations.RunPython(migrate_quiz_language), 28 | migrations.RunPython(migrate_historical_quiz_language), 29 | ] 30 | -------------------------------------------------------------------------------- /questions/migrations/0014_alter_historicalquestion_language_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1a1 on 2022-07-07 17:08 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("questions", "0013_historicalquestion_history_changed_fields"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="historicalquestion", 15 | name="language", 16 | field=models.CharField( 17 | choices=[("Français", "Français"), ("English", "English"), ("Deutsch", "Deutsch")], 18 | default="Français", 19 | max_length=50, 20 | verbose_name="Langue", 21 | ), 22 | ), 23 | migrations.AlterField( 24 | model_name="question", 25 | name="language", 26 | field=models.CharField( 27 | choices=[("Français", "Français"), ("English", "English"), ("Deutsch", "Deutsch")], 28 | default="Français", 29 | max_length=50, 30 | verbose_name="Langue", 31 | ), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /www/categories/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from django.views.generic.base import RedirectView 3 | 4 | from www.categories.views import ( 5 | CategoryDetailEditView, 6 | CategoryDetailQuestionListView, 7 | CategoryDetailView, 8 | CategoryListView, 9 | ) 10 | 11 | 12 | app_name = "categories" 13 | 14 | urlpatterns = [ 15 | path("", CategoryListView.as_view(), name="list"), 16 | # path("/", CategoryDetailView.as_view(), name="detail"), 17 | path( 18 | "/", 19 | include( 20 | [ 21 | # path("", CategoryDetailView.as_view(), name="detail"), 22 | path( 23 | "", 24 | RedirectView.as_view(pattern_name="categories:detail_view", permanent=False), 25 | name="detail", 26 | ), 27 | path("view/", CategoryDetailView.as_view(), name="detail_view"), 28 | path("edit/", CategoryDetailEditView.as_view(), name="detail_edit"), 29 | path("questions/", CategoryDetailQuestionListView.as_view(), name="detail_questions"), 30 | ] 31 | ), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /tags/templates/admin/tags/tag/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | 3 | {% load static %} 4 | 5 | {% block extrahead %} 6 | {{ block.super }} 7 | 13 | {% endblock %} 14 | 15 | {% block content %} 16 |
    17 | ⚠️ Rappel : Actuellement les tags sont créés dans cet interface administrateur.
    18 | Mais ils n'apparaîtront pas tout de suite dans l'interface de l'application !
    19 | Il faut encore exporter la donnée vers Github, un bouton à l'Accueil permet de le faire en 1 clic.
    20 | Plus de détails sur la procédure *actuelle* ici. 21 |
    22 | 23 |
    24 | 25 | 26 | {% block content_title %}{{ block.super }}{% endblock %} 27 | {{ block.super }} 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /templates/includes/_s3_upload_form.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 |
    4 |
    5 |
    6 |
    7 | 8 | 9 |
    {% translate "Choose an image" %} {% translate "or" %} {% translate "drag and drop here" %}.
    10 |
    {% translate "Maximum size" %} : {{ s3_upload_config.max_file_size }} {% translate "MB" %}
    11 |
    {% translate "File type" %} : PNG, JPEG, SVG, GIF
    12 | 13 |
    14 |
    15 |
    16 | 21 | -------------------------------------------------------------------------------- /api/questions/filters.py: -------------------------------------------------------------------------------- 1 | import django_filters 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from categories.models import Category 5 | from core import constants 6 | from tags.models import Tag 7 | from users.models import User 8 | 9 | 10 | class QuestionFilter(django_filters.FilterSet): 11 | type = django_filters.MultipleChoiceFilter(label=_("Type(s)"), choices=constants.QUESTION_TYPE_CHOICES) 12 | difficulty = django_filters.MultipleChoiceFilter( 13 | label=_("Difficulty level(s)"), 14 | choices=constants.QUESTION_DIFFICULTY_CHOICES, 15 | ) 16 | language = django_filters.MultipleChoiceFilter(label=_("Language(s)"), choices=constants.LANGUAGE_CHOICES) 17 | category = django_filters.ModelMultipleChoiceFilter( 18 | label=_("Category(s)"), 19 | queryset=Category.objects.all(), 20 | ) 21 | tags = django_filters.ModelMultipleChoiceFilter( 22 | label=_("Tag(s)"), 23 | queryset=Tag.objects.all(), 24 | ) 25 | author = django_filters.ModelMultipleChoiceFilter( 26 | label=_("Author(s)"), 27 | queryset=User.objects.all_contributors(), 28 | ) 29 | # TODO: QuestionFullStringSerializer, random 30 | -------------------------------------------------------------------------------- /contributions/migrations/0009_alter_contribution_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1a1 on 2022-05-25 21:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("contributions", "0008_alter_contribution_status"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="contribution", 15 | name="type", 16 | field=models.CharField( 17 | blank=True, 18 | choices=[ 19 | ("NEW_QUESTION", "Nouvelle question"), 20 | ("NEW_QUIZ", "Nouveau quiz"), 21 | ("COMMENT_APP", "Commentaire application"), 22 | ("COMMENT_QUESTION", "Commentaire question"), 23 | ("COMMENT_QUIZ", "Commentaire quiz"), 24 | ("COMMENT_CONTRIBUTOR", "Commentaire contributeur"), 25 | ("REPLY", "Réponse contributeur"), 26 | ("ERROR_APP", "Erreur application"), 27 | ], 28 | max_length=150, 29 | verbose_name="Type", 30 | ), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /categories/templates/admin/categories/category/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | 3 | {% load static %} 4 | 5 | {% block extrahead %} 6 | {{ block.super }} 7 | 13 | {% endblock %} 14 | 15 | {% block content %} 16 |
    17 | ⚠️ Rappel : Actuellement les catégories sont créés dans cet interface administrateur.
    18 | Mais elles n'apparaîtront pas tout de suite dans l'interface de l'application !
    19 | Il faut encore exporter la donnée vers Github, un bouton à l'Accueil permet de le faire en 1 clic.
    20 | Plus de détails sur la procédure *actuelle* ici. 21 |
    22 | 23 |
    24 | 25 | 26 | {% block content_title %}{{ block.super }}{% endblock %} 27 | {{ block.super }} 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /tags/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-03-30 22:31 2 | 3 | import ckeditor.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="Tag", 16 | fields=[ 17 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 18 | ("name", models.CharField(help_text="Le nom du tag", max_length=50)), 19 | ("description", ckeditor.fields.RichTextField(blank=True, help_text="Une description du tag")), 20 | ("created", models.DateField(auto_now_add=True, help_text="La date de création du tag")), 21 | ], 22 | options={ 23 | "verbose_name": "Tag", 24 | "verbose_name_plural": "Tags", 25 | "ordering": ["pk"], 26 | }, 27 | ), 28 | migrations.AddConstraint( 29 | model_name="tag", 30 | constraint=models.UniqueConstraint(fields=("name",), name="tag_name_unique"), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /www/glossary/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from django.views.generic.base import RedirectView 3 | 4 | from www.glossary.views import ( 5 | GlossaryItemCreateView, 6 | GlossaryItemDetailEditView, 7 | GlossaryItemDetailHistoryView, 8 | GlossaryItemDetailView, 9 | GlossaryListView, 10 | ) 11 | 12 | 13 | app_name = "glossary" 14 | 15 | urlpatterns = [ 16 | path("", GlossaryListView.as_view(), name="list"), 17 | path( 18 | "/", 19 | include( 20 | [ 21 | # path("", GlossaryItemDetailView.as_view(), name="detail"), 22 | path( 23 | "", 24 | RedirectView.as_view(pattern_name="glossary:detail_view", permanent=False), 25 | name="detail", 26 | ), 27 | path("view/", GlossaryItemDetailView.as_view(), name="detail_view"), 28 | path("edit/", GlossaryItemDetailEditView.as_view(), name="detail_edit"), 29 | path("history/", GlossaryItemDetailHistoryView.as_view(), name="detail_history"), 30 | ] 31 | ), 32 | ), 33 | path("create/", GlossaryItemCreateView.as_view(), name="create"), 34 | ] 35 | -------------------------------------------------------------------------------- /api/users/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from users.models import User 4 | 5 | 6 | class UserSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = User 9 | fields = [ 10 | "id", 11 | "first_name", 12 | "last_name", 13 | # "email", "roles" 14 | "created", 15 | ] # "question_count", "quiz_count", "updated" 16 | 17 | 18 | class UserWithCountSerializer(serializers.ModelSerializer): 19 | question_count = serializers.IntegerField(source="question_public_validated_count") 20 | quiz_count = serializers.IntegerField(source="quiz_public_published_count") 21 | 22 | class Meta: 23 | model = User 24 | fields = [ 25 | "id", 26 | "first_name", 27 | "last_name", 28 | # "email", "roles" 29 | "question_count", 30 | "quiz_count", 31 | "created", 32 | ] # "updated" 33 | 34 | 35 | class UserStringSerializer(serializers.ModelSerializer): 36 | def to_representation(self, value): 37 | return value.full_name 38 | 39 | class Meta: 40 | model = User 41 | fields = ["full_name"] 42 | -------------------------------------------------------------------------------- /www/auth/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import views as auth_views 2 | from django.urls import path, reverse_lazy 3 | 4 | from www.auth.views import LoginView, PasswordResetView 5 | 6 | 7 | app_name = "auth" 8 | 9 | urlpatterns = [ 10 | path("login/", LoginView.as_view(), name="login"), 11 | path("logout/", auth_views.LogoutView.as_view(template_name="auth/logged_out.html"), name="logout"), 12 | path("password-reset/", PasswordResetView.as_view(), name="password_reset"), 13 | path( 14 | "password-reset/sent/", 15 | auth_views.PasswordResetDoneView.as_view(template_name="auth/password_reset_sent.html"), 16 | name="password_reset_sent", 17 | ), # name="password_reset_done" 18 | path( 19 | "password-reset///", 20 | auth_views.PasswordResetConfirmView.as_view( 21 | template_name="auth/password_reset_confirm.html", success_url=reverse_lazy("auth:password_reset_complete") 22 | ), 23 | name="password_reset_confirm", 24 | ), 25 | path( 26 | "password-reset/done/", 27 | auth_views.PasswordResetCompleteView.as_view(template_name="auth/password_reset_complete.html"), 28 | name="password_reset_complete", 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /www/contributions/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from django.views.generic.base import RedirectView 3 | 4 | from www.contributions.views import ( 5 | CommentDetailEditView, 6 | CommentDetailHistoryView, 7 | CommentDetailReplyCreateView, 8 | CommentDetailView, 9 | CommentListView, 10 | ) 11 | 12 | 13 | app_name = "contributions" 14 | 15 | urlpatterns = [ 16 | path("", CommentListView.as_view(), name="list"), 17 | path( 18 | "/", 19 | include( 20 | [ 21 | # path("", CommentDetailView.as_view(), name="detail"), 22 | path( 23 | "", 24 | RedirectView.as_view(pattern_name="contributions:detail_view", permanent=False), 25 | name="detail", 26 | ), 27 | path("view/", CommentDetailView.as_view(), name="detail_view"), 28 | path("edit/", CommentDetailEditView.as_view(), name="detail_edit"), 29 | path("reply/", CommentDetailReplyCreateView.as_view(), name="detail_reply_create"), 30 | path("history/", CommentDetailHistoryView.as_view(), name="detail_history"), 31 | ] 32 | ), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /stats/migrations/0002_questionanswerevent_quiz_questionfeedbackevent_quiz.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-04-19 09:39 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("quizs", "0003_populate_quiz_author_link"), 11 | ("stats", "0001_initial"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name="questionanswerevent", 17 | name="quiz", 18 | field=models.ForeignKey( 19 | blank=True, 20 | null=True, 21 | on_delete=django.db.models.deletion.CASCADE, 22 | related_name="question_stats", 23 | to="quizs.quiz", 24 | ), 25 | ), 26 | migrations.AddField( 27 | model_name="questionfeedbackevent", 28 | name="quiz", 29 | field=models.ForeignKey( 30 | blank=True, 31 | null=True, 32 | on_delete=django.db.models.deletion.CASCADE, 33 | related_name="question_feedbacks", 34 | to="quizs.quiz", 35 | ), 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /contributions/migrations/0002_contribution_question_contribution_quiz.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-04-04 22:05 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("questions", "0001_initial"), 11 | ("quizs", "0001_initial"), 12 | ("contributions", "0001_initial"), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name="contribution", 18 | name="question", 19 | field=models.ForeignKey( 20 | blank=True, 21 | null=True, 22 | on_delete=django.db.models.deletion.CASCADE, 23 | related_name="contributions", 24 | to="questions.question", 25 | ), 26 | ), 27 | migrations.AddField( 28 | model_name="contribution", 29 | name="quiz", 30 | field=models.ForeignKey( 31 | blank=True, 32 | null=True, 33 | on_delete=django.db.models.deletion.CASCADE, 34 | related_name="contributions", 35 | to="quizs.quiz", 36 | ), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /questions/migrations/0022_question_language_migration.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-18 19:54 2 | 3 | from django.db import migrations 4 | 5 | 6 | def migrate_question_language(apps, schema_editor): 7 | Question = apps.get_model("questions", "Question") 8 | Question.objects.filter(language="Français").update(language="FRENCH") 9 | Question.objects.filter(language="English").update(language="ENGLISH") 10 | Question.objects.filter(language="Deutsch").update(language="GERMAN") 11 | 12 | 13 | def migrate_historical_question_language(apps, schema_editor): 14 | HistoricalQuestion = apps.get_model("questions", "HistoricalQuestion") 15 | HistoricalQuestion.objects.filter(language="Français").update(language="FRENCH") 16 | HistoricalQuestion.objects.filter(language="English").update(language="ENGLISH") 17 | HistoricalQuestion.objects.filter(language="Deutsch").update(language="GERMAN") 18 | 19 | 20 | class Migration(migrations.Migration): 21 | 22 | dependencies = [ 23 | ("questions", "0021_alter_historicalquestion_answer_audio_url_and_more"), 24 | ] 25 | 26 | operations = [ 27 | migrations.RunPython(migrate_question_language), 28 | migrations.RunPython(migrate_historical_question_language), 29 | ] 30 | -------------------------------------------------------------------------------- /templates/tags/detail_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "tags/detail_base.html" %} 2 | {% load i18n django_bootstrap5 %} 3 | 4 | {% block tag_detail_content %} 5 |
    6 |
    7 |
    8 | {% csrf_token %} 9 | 10 |
    11 |
    12 | {% bootstrap_form form alert_error_type="all" %} 13 |
    14 |
    15 | 16 |
    17 |
    18 | {% bootstrap_button button_type="submit" button_class="btn-primary" content=_("Save") %} 19 | {# {% bootstrap_button button_type="reset" button_class="btn-secondary" content=_("Cancel") %} #} 20 | {% translate "Cancel" %} 21 |
    22 |
    23 | 24 |
    25 |
    26 |
    27 |
    28 |
    29 | {% endblock %} 30 | 31 | {% block extra_js %} 32 | {{ form.media.js }} 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /templates/categories/detail_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "categories/detail_base.html" %} 2 | {% load i18n django_bootstrap5 %} 3 | 4 | {% block category_detail_content %} 5 |
    6 |
    7 |
    8 | {% csrf_token %} 9 | 10 |
    11 |
    12 | {% bootstrap_form form alert_error_type="all" %} 13 |
    14 |
    15 | 16 |
    17 |
    18 | {% bootstrap_button button_type="submit" button_class="btn-primary" content=_("Save") %} 19 | {# {% bootstrap_button button_type="reset" button_class="btn-secondary" content=_("Cancel") %} #} 20 | {% translate "Cancel" %} 21 |
    22 |
    23 | 24 |
    25 |
    26 |
    27 |
    28 |
    29 | {% endblock %} 30 | 31 | {% block extra_js %} 32 | {{ form.media.js }} 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /templates/categories/list.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | {% load render_table from django_tables2 %} 3 | {% load i18n django_bootstrap5 %} 4 | 5 | {% block title %}{% translate "Categories" %}{{ block.super }}{% endblock %} 6 | 7 | {% block breadcrumbs %} 8 |
    9 |
    10 |
    11 | 17 |
    18 |
    19 |
    20 | {% endblock %} 21 | 22 | {% block content %} 23 |
    24 |
    25 |
    26 |

    {% translate "All the categories" %} {{ categories.count }}

    27 |
    28 |
    29 | 30 |
    31 |
    32 | {% render_table table %} 33 |
    34 |
    35 |
    36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /www/tags/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from django.views.generic.base import RedirectView 3 | 4 | from www.tags.views import ( 5 | TagCreateView, 6 | TagDetailEditView, 7 | TagDetailQuestionListView, 8 | TagDetailQuizListView, 9 | TagDetailView, 10 | TagListView, 11 | ) 12 | 13 | 14 | app_name = "tags" 15 | 16 | urlpatterns = [ 17 | path("", TagListView.as_view(), name="list"), 18 | # path("/", TagDetailView.as_view(), name="detail"), 19 | path( 20 | "/", 21 | include( 22 | [ 23 | # path("", TagDetailView.as_view(), name="detail"), 24 | path( 25 | "", 26 | RedirectView.as_view(pattern_name="tags:detail_view", permanent=False), 27 | name="detail", 28 | ), 29 | path("view/", TagDetailView.as_view(), name="detail_view"), 30 | path("edit/", TagDetailEditView.as_view(), name="detail_edit"), 31 | path("questions/", TagDetailQuestionListView.as_view(), name="detail_questions"), 32 | path("quizs/", TagDetailQuizListView.as_view(), name="detail_quizs"), 33 | ] 34 | ), 35 | ), 36 | path("create/", TagCreateView.as_view(), name="create"), 37 | ] 38 | -------------------------------------------------------------------------------- /api/contributions/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from contributions.models import Comment 4 | 5 | 6 | COMMENT_READ_FIELDS = ["text", "description", "type", "replies", "created"] 7 | COMMENT_REPLY_READ_FIELDS = COMMENT_READ_FIELDS.copy() 8 | COMMENT_REPLY_READ_FIELDS.remove("replies") 9 | COMMENT_FULL_FIELDS = ["text", "description", "type", "question", "quiz", "status", "publish", "created"] 10 | COMMENT_WRITE_FIELDS = COMMENT_FULL_FIELDS.copy() 11 | COMMENT_WRITE_FIELDS.remove("status") 12 | COMMENT_WRITE_FIELDS.remove("publish") 13 | 14 | 15 | class CommentReplyReadSerializer(serializers.ModelSerializer): 16 | class Meta: 17 | model = Comment 18 | fields = COMMENT_REPLY_READ_FIELDS 19 | 20 | 21 | class CommentReadSerializer(serializers.ModelSerializer): 22 | replies = CommentReplyReadSerializer(read_only=True, many=True, source="replies_published") 23 | 24 | class Meta: 25 | model = Comment 26 | fields = COMMENT_READ_FIELDS 27 | 28 | 29 | class CommentWriteSerializer(serializers.ModelSerializer): 30 | class Meta: 31 | model = Comment 32 | fields = COMMENT_WRITE_FIELDS 33 | 34 | 35 | class CommentFullSerializer(serializers.ModelSerializer): 36 | class Meta: 37 | model = Comment 38 | fields = COMMENT_FULL_FIELDS 39 | -------------------------------------------------------------------------------- /templates/glossary/detail_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "glossary/detail_base.html" %} 2 | {% load i18n django_bootstrap5 %} 3 | 4 | {% block glossary_item_detail_content %} 5 |
    6 |
    7 |
    8 | {% csrf_token %} 9 | 10 |
    11 |
    12 | {% bootstrap_form form alert_error_type="all" %} 13 |
    14 |
    15 | 16 |
    17 |
    18 | {% bootstrap_button button_type="submit" button_class="btn-primary" content=_("Save") %} 19 | {# {% bootstrap_button button_type="reset" button_class="btn-secondary" content=_("Cancel") %} #} 20 | {% translate "Cancel" %} 21 |
    22 |
    23 | 24 |
    25 |
    26 |
    27 |
    28 |
    29 | {% endblock %} 30 | 31 | {% block extra_js %} 32 | {{ form.media.js }} 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /templates/tags/detail_quizs.html: -------------------------------------------------------------------------------- 1 | {% extends "tags/detail_base.html" %} 2 | {% load i18n custom_filters %} 3 | 4 | {% block tag_detail_content %} 5 |
    6 |
    7 |
    8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% for quiz in quizs %} 18 | 19 | 22 | 23 | 26 | 27 | {% endfor %} 28 | 29 |
    ID{% translate "Quiz" %}{% translate "Tags" %}
    20 | {{ quiz.id }} 21 | {{ quiz.name }} 24 | {% include "tags/_badge_list.html" with tag_list=quiz.tags.all %} 25 |
    30 |
    31 |
    32 |
    33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /templates/admin/history.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | {% load render_table from django_tables2 %} 3 | {% load i18n %} 4 | 5 | {% block title %}{% translate "Administration" %} : historique{{ block.super }}{% endblock %} 6 | 7 | {% block breadcrumbs %} 8 |
    9 |
    10 |
    11 | 18 |
    19 |
    20 |
    21 | {% endblock %} 22 | 23 | {% block content %} 24 |
    25 |
    26 |
    27 |

    50 dernières modifications

    28 |
    29 |
    30 |
    31 |
    32 | {% render_table table %} 33 |
    34 |
    35 |
    36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /quizs/management/commands/sanitize_quiz_question_ordering.py: -------------------------------------------------------------------------------- 1 | from django.core.management import BaseCommand 2 | 3 | from api.models import Quiz 4 | 5 | 6 | class Command(BaseCommand): 7 | """ 8 | Usage: 9 | - python manage.py sanitize_quiz_question_ordering 10 | - python manage.py sanitize_quiz_question_ordering --fix 11 | """ 12 | 13 | def add_arguments(self, parser): 14 | parser.add_argument( 15 | "--fix", 16 | default=False, 17 | action="store_true", 18 | dest="fix", 19 | help="Fix ordering errors", 20 | ) 21 | 22 | def handle(self, *args, **kwargs): 23 | for quiz in Quiz.objects.all(): 24 | print("-----", quiz.id, quiz.name) 25 | for index, qq in enumerate(quiz.quizquestion_set.all()): 26 | order_correct = qq.order == index + 1 27 | if not order_correct: 28 | print( 29 | f"question {qq.question.id} /", 30 | f"current order: {qq.order} /", 31 | f"correct order: {index + 1}", 32 | ) 33 | if kwargs.get("fix"): 34 | qq.order = index + 1 35 | qq.save() 36 | print("... fixed") 37 | -------------------------------------------------------------------------------- /quizs/migrations/0017_alter_historicalquiz_created_alter_quiz_created_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-08-17 14:19 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("quizs", "0016_quiz_validation_fields"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="historicalquiz", 16 | name="created", 17 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name="Date de création"), 18 | ), 19 | migrations.AlterField( 20 | model_name="quiz", 21 | name="created", 22 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name="Date de création"), 23 | ), 24 | migrations.AlterField( 25 | model_name="quizquestion", 26 | name="created", 27 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name="Date de création"), 28 | ), 29 | migrations.AlterField( 30 | model_name="quizrelationship", 31 | name="created", 32 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name="Date de création"), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /users/constants.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext_lazy as _ 2 | 3 | 4 | USER_ROLE_CONTRIBUTOR = "CONTRIBUTOR" 5 | USER_ROLE_SUPER_CONTRIBUTOR = "SUPER-CONTRIBUTOR" 6 | USER_ROLE_ADMINISTRATOR = "ADMINISTRATOR" 7 | USER_ROLE_CHOICES = ( 8 | (USER_ROLE_CONTRIBUTOR, _("Contributor")), 9 | (USER_ROLE_SUPER_CONTRIBUTOR, _("Super Contributor")), 10 | (USER_ROLE_ADMINISTRATOR, _("Administrator")), 11 | ) 12 | 13 | IS_ADMIN_MESSAGE = _("You are an administrator") 14 | ADMIN_REQUIRED_MESSAGE = _("You don't have the necessary rights") 15 | ADMIN_REQUIRED_EDIT_FIELD_MESSAGE = _("You don't have the necessary rights to edit this field") 16 | ONLY_ADMIN_ALLOWED_MESSAGE = _("Only an administrator can do it") 17 | ONLY_PRIVATE_QUESTION_AUTHOR_ALLOWED_MESSAGE = _("Only the question author can edit a private question") 18 | ONLY_PRIVATE_QUIZ_AUTHOR_ALLOWED_MESSAGE = _("Only the quiz author can edit a private quiz") 19 | ONLY_QUESTION_AUTHOR_OR_SUPER_CONTRIBUTOR_ALLOWED_MESSAGE = _( 20 | "Only the question author or a super-contributor can edit a public question" 21 | ) 22 | ONLY_QUIZ_AUTHOR_OR_SUPER_CONTRIBUTOR_ALLOWED_MESSAGE = _( 23 | "Only the quiz author or a super-contributor can edit a public quiz" 24 | ) 25 | ADMIN_REQUIRED_EDIT_FIELD_MESSAGE_FULL = f"{ADMIN_REQUIRED_EDIT_FIELD_MESSAGE}. {ONLY_ADMIN_ALLOWED_MESSAGE}." 26 | -------------------------------------------------------------------------------- /core/static/css/admin/extra.css: -------------------------------------------------------------------------------- 1 | .action-text { 2 | border-left: 5px solid #cce5ff; 3 | padding-left: 10px; 4 | } 5 | 6 | .loading { 7 | height: 0; 8 | width: 0; 9 | padding: 5px; 10 | border: 6px solid #ccc; 11 | border-right-color: #888; 12 | border-radius: 22px; 13 | -webkit-animation: rotate 1s infinite linear; 14 | /* left, top and position just for the demo! */ 15 | } 16 | 17 | @-webkit-keyframes rotate { 18 | /* 100% keyframe for clockwise. 19 | use 0% instead for anticlockwise */ 20 | 100% { 21 | -webkit-transform: rotate(360deg); 22 | } 23 | } 24 | 25 | 26 | /* filter_vertical questions full width */ 27 | .form-row .stacked, 28 | .form-row .stacked .selector-available, 29 | .form-row .stacked .selector-chosen, 30 | .form-row .stacked .selector-available select, 31 | .form-row .stacked .selector-chosen select { 32 | width: 100%; 33 | } 34 | .form-row .stacked .selector-available select, 35 | .form-row .stacked .selector-chosen select { 36 | height: 20em; 37 | } 38 | 39 | 40 | /* Hide add/edit buttons next to related forms */ 41 | div.related-widget-wrapper > a { 42 | display: none; 43 | } 44 | 45 | 46 | /* Quiz questions many-to-many */ 47 | div.field-question div.related-widget-wrapper { 48 | width: 80%; 49 | } 50 | div.field-question div.related-widget-wrapper > select { 51 | width: 75%; 52 | } 53 | -------------------------------------------------------------------------------- /core/templatetags/custom_filters.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | 4 | register = template.Library() 5 | 6 | 7 | @register.filter(name="isinstance") 8 | def isinstance_filter(val, instance_type): 9 | return isinstance(val, eval(instance_type)) 10 | 11 | 12 | @register.filter(name="get_obj_attr") 13 | def get_obj_attr_filter(obj, key): 14 | """ 15 | Find a dictionary value with a key as a variable. 16 | https://stackoverflow.com/a/8000091 17 | 18 | Usage: 19 | {% load custom_filters %} 20 | {{ instance|get_obj_attr:key }} 21 | {{ dict|get_obj_attr:key }} 22 | """ 23 | try: 24 | return getattr(obj, f"get_{key}_display") 25 | except: # noqa 26 | return obj.get(key) 27 | 28 | 29 | @register.filter(name="get_list_item") 30 | def get_list_item_filter(obj, index): 31 | """ 32 | Find a dictionary value with a key as a variable. 33 | https://stackoverflow.com/a/8000091 34 | 35 | Usage: 36 | {% load custom_filters %} 37 | {{ list|get_list_item:index }} 38 | """ 39 | try: 40 | return obj[index] 41 | except: # noqa 42 | return {} 43 | 44 | 45 | @register.filter(name="flatten_list") 46 | def flatten_list(obj): 47 | if type(obj) is list: 48 | return ", ".join([str(item) for item in obj]) 49 | return obj 50 | -------------------------------------------------------------------------------- /templates/activity/list.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | {% load render_table from django_tables2 %} 3 | {% load i18n django_bootstrap5 %} 4 | 5 | {% block title %}{% translate "Recent actions" %}{{ block.super }}{% endblock %} 6 | 7 | {% block breadcrumbs %} 8 |
    9 |
    10 |
    11 | 17 |
    18 |
    19 |
    20 | {% endblock %} 21 | 22 | {% block content %} 23 |
    24 |
    25 |
    26 |

    {% translate "Recent actions" %}

    27 |
    28 |
    29 | 30 |
    31 |
    32 | {% include "activity/_event_list.html" with events=events %} 33 | {% include "includes/_paginator.html" with page_obj=page_obj %} 34 |
    35 |
    36 |
    37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /app/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.i18n import i18n_patterns 3 | from django.urls import include, path 4 | 5 | from core.admin import admin_site 6 | 7 | 8 | contribution_patterns = i18n_patterns( 9 | path("", include("www.pages.urls")), 10 | path("accounts/", include("www.auth.urls")), 11 | path("profile/", include("www.profile.urls")), 12 | path("questions/", include("www.questions.urls")), 13 | path("quizs/", include("www.quizs.urls")), 14 | path("categories/", include("www.categories.urls")), 15 | path("tags/", include("www.tags.urls")), 16 | path("comments/", include("www.contributions.urls")), 17 | path("glossary/", include("www.glossary.urls")), 18 | path("activity/", include("www.activity.urls")), 19 | path("users/", include("www.users.urls")), 20 | path("admin/", include("www.admin.urls")), 21 | ) 22 | 23 | urlpatterns = contribution_patterns + [ 24 | # set_language 25 | path("i18n/", include("django.conf.urls.i18n")), 26 | # admin 27 | path("django/", admin_site.urls), 28 | # api 29 | path("api/", include("api.urls")), 30 | # stats 31 | path("stats/", include("stats.urls")), 32 | ] 33 | 34 | if settings.DEBUG: # and "debug_toolbar" in settings.INSTALLED_APPS: 35 | urlpatterns += [path("__debug__/", include("debug_toolbar.urls"))] 36 | -------------------------------------------------------------------------------- /templates/profile/history.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | {% load render_table from django_tables2 %} 3 | {% load i18n %} 4 | 5 | {% block title %}{% translate "My modification history" %}{{ block.super }}{% endblock %} 6 | 7 | {% block breadcrumbs %} 8 |
    9 |
    10 |
    11 | 18 |
    19 |
    20 |
    21 | {% endblock %} 22 | 23 | {% block content %} 24 |
    25 |
    26 |
    27 |

    {% translate "Last 50 modifications" %}

    28 |
    29 |
    30 |
    31 |
    32 | {% render_table table %} 33 |
    34 |
    35 |
    36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | 4 | # How to run pre-commit inside folder ? 5 | # https://github.com/pre-commit/pre-commit/issues/1110 6 | 7 | # How-to install locally ? 8 | # pre-commit install 9 | 10 | repos: 11 | - repo: https://github.com/ambv/black 12 | rev: 22.8.0 13 | hooks: 14 | - id: black 15 | language_version: python3 16 | - repo: https://github.com/pycqa/isort 17 | rev: 5.12.0 18 | hooks: 19 | - id: isort 20 | - repo: https://github.com/pre-commit/pre-commit-hooks 21 | rev: v2.4.0 22 | hooks: 23 | - id: flake8 24 | args: ["--max-line-length=119"] 25 | - repo: https://github.com/pre-commit/pre-commit-hooks 26 | rev: v4.3.0 27 | hooks: 28 | - id: check-yaml 29 | - repo: https://github.com/pre-commit/pre-commit-hooks 30 | rev: v4.3.0 31 | hooks: 32 | - id: end-of-file-fixer 33 | # - repo: https://github.com/pre-commit/mirrors-eslint 34 | # rev: v8.11.0 35 | # hooks: 36 | # - id: eslint 37 | # files: frontend/.*$ 38 | # additional_dependencies: 39 | # - eslint 40 | # - eslint-config-airbnb-base 41 | # - eslint-loader 42 | # - eslint-plugin-vue 43 | # - babel-eslint 44 | # verbose: true 45 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | boto3 = ">=1.26.43" 8 | django = "4.2.9" 9 | asgiref = ">=3.6.0" 10 | certifi = ">=2022.12.7" 11 | dj-database-url = ">=1.2.0" 12 | django-anymail = {extras = ["sendinblue"], version = "~=10.1"} 13 | django-autocomplete-light = "~=3.9.7" 14 | django-bootstrap5 = "~=23.3" 15 | django-ckeditor = "~=6.7.0" 16 | django-cors-headers = "~=4.2.0" 17 | django-extensions = "~=3.2.3" 18 | django-fieldsets-with-inlines = "~=0.6" 19 | django-filter = "~=23.3" 20 | django-import-export = "~=3.3.1" 21 | django-simple-history = "~=3.4.0" 22 | django-tables2 = "~=2.6.0" 23 | djangorestframework = "~=3.14.0" 24 | django-solo = "~=2.1.0" 25 | drf-spectacular = "~=0.26.5" 26 | gunicorn = ">=20.0.1" 27 | ipython = ">=8.8.0" 28 | pandas = ">=1.5.2" 29 | pillow = ">=9.4.0" 30 | psycopg2-binary = "~=2.9.7" 31 | PyGithub = ">=1.57" 32 | python-dotenv = "~=1.0.0" 33 | pytz = ">=2022.7" 34 | PyYAML = ">=6.0" 35 | sentry-sdk = "~=1.31.0" 36 | sqlparse = ">=0.4.3" 37 | whitenoise = ">=6.3.0" 38 | 39 | [dev-packages] 40 | black = "*" 41 | coverage = "*" 42 | django-debug-toolbar = "*" 43 | factory-boy = "*" 44 | flake8 = "*" 45 | freezegun = "*" 46 | isort = "*" 47 | pre-commit = "*" 48 | 49 | [requires] 50 | python_version = "3.9" 51 | 52 | [pipenv] 53 | allow_prereleases = true 54 | -------------------------------------------------------------------------------- /stats/migrations/0008_alter_linkclickevent_question_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-03 19:55 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("questions", "0017_rename_answer_audio_historicalquestion_answer_audio_url_and_more"), 11 | ("quizs", "0020_remove_historicalquiz_author_and_more"), 12 | ("stats", "0007_linkclickevent_field_name"), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name="linkclickevent", 18 | name="question", 19 | field=models.ForeignKey( 20 | blank=True, 21 | null=True, 22 | on_delete=django.db.models.deletion.CASCADE, 23 | related_name="link_clicks", 24 | to="questions.question", 25 | ), 26 | ), 27 | migrations.AlterField( 28 | model_name="linkclickevent", 29 | name="quiz", 30 | field=models.ForeignKey( 31 | blank=True, 32 | null=True, 33 | on_delete=django.db.models.deletion.CASCADE, 34 | related_name="link_clicks", 35 | to="quizs.quiz", 36 | ), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /stats/constants.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext_lazy as _ 2 | 3 | 4 | FEEDBACK_LIKE = "like" 5 | FEEDBACK_DISLIKE = "dislike" 6 | FEEDBACK_CHOICES = [ 7 | (FEEDBACK_LIKE, _("Positive")), 8 | (FEEDBACK_DISLIKE, _("Negative")), 9 | ] 10 | 11 | QUESTION_SOURCE_QUESTION = "question" 12 | QUESTION_SOURCE_QUIZ = "quiz" 13 | QUESTION_SOURCE_CHOICES = [ 14 | (QUESTION_SOURCE_QUESTION, "Question"), 15 | (QUESTION_SOURCE_QUIZ, "Quiz"), 16 | ] 17 | 18 | DEFAULT_DAILY_STAT_HOUR_SPLIT = { 19 | str(h): { 20 | "question_answer_count": 0, 21 | "question_answer_from_quiz_count": 0, 22 | "quiz_answer_count": 0, 23 | "question_feedback_count": 0, 24 | "question_feedback_from_quiz_count": 0, 25 | "quiz_feedback_count": 0, 26 | } 27 | for h in range(24) # '0' à '23' 28 | } 29 | 30 | AGGREGATION_FIELD_CHOICE_LIST = [ 31 | "question_answer_count", 32 | "quiz_answer_count", 33 | "question_feedback_count", 34 | # "quiz_feedback_count", 35 | ] 36 | AGGREGATION_QUIZ_FIELD_CHOICE_LIST = ["quiz_answer_count", "quiz_feedback_count"] 37 | AGGREGATION_SCALE_CHOICE_LIST = ["day", "week", "month"] 38 | AGGREGATION_SINCE_CHOICE_LIST = ["total", "last_30_days", "month", "week"] 39 | AGGREGATION_SINCE_DATE_DEFAULT = "2020-01-01" 40 | 41 | 42 | def daily_stat_hour_split_jsonfield_default_value(): 43 | return DEFAULT_DAILY_STAT_HOUR_SPLIT 44 | -------------------------------------------------------------------------------- /categories/migrations/0005_category_model_translation.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-22 18:52 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("categories", "0004_alter_category_created"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelOptions( 15 | name="category", 16 | options={"ordering": ["pk"], "verbose_name": "Category", "verbose_name_plural": "Categories"}, 17 | ), 18 | migrations.AlterField( 19 | model_name="category", 20 | name="created", 21 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name="Creation date"), 22 | ), 23 | migrations.AlterField( 24 | model_name="category", 25 | name="name", 26 | field=models.CharField(max_length=50, verbose_name="Name"), 27 | ), 28 | migrations.AlterField( 29 | model_name="category", 30 | name="name_long", 31 | field=models.CharField(max_length=150, verbose_name="Name (long version)"), 32 | ), 33 | migrations.AlterField( 34 | model_name="category", 35 | name="updated", 36 | field=models.DateTimeField(auto_now=True, verbose_name="Last update date"), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /glossary/migrations/0002_add_verbose_names.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-01 21:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("glossary", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="glossaryitem", 15 | name="definition_short", 16 | field=models.CharField(max_length=150, verbose_name="Définition (succinte)"), 17 | ), 18 | migrations.AlterField( 19 | model_name="glossaryitem", 20 | name="description", 21 | field=models.TextField(blank=True, verbose_name="Description"), 22 | ), 23 | migrations.AlterField( 24 | model_name="glossaryitem", 25 | name="description_accessible_url", 26 | field=models.URLField(blank=True, max_length=500, verbose_name="Lien pour aller plus loin"), 27 | ), 28 | migrations.AlterField( 29 | model_name="glossaryitem", 30 | name="name", 31 | field=models.CharField(max_length=50, verbose_name="Mot ou sigle"), 32 | ), 33 | migrations.AlterField( 34 | model_name="glossaryitem", 35 | name="name_alternatives", 36 | field=models.TextField(blank=True, verbose_name="Noms alternatifs"), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /quizs/migrations/0005_alter_quiz_timestamps.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-01 21:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("quizs", "0004_add_verbose_names"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="quiz", 15 | name="created", 16 | field=models.DateTimeField(auto_now_add=True, verbose_name="Date de création"), 17 | ), 18 | migrations.AlterField( 19 | model_name="quiz", 20 | name="updated", 21 | field=models.DateTimeField(auto_now=True, verbose_name="Date de dernière modification"), 22 | ), 23 | migrations.AlterField( 24 | model_name="quizquestion", 25 | name="created", 26 | field=models.DateTimeField(auto_now_add=True, verbose_name="Date de création"), 27 | ), 28 | migrations.AlterField( 29 | model_name="quizquestion", 30 | name="updated", 31 | field=models.DateTimeField(auto_now=True, verbose_name="Date de dernière modification"), 32 | ), 33 | migrations.AlterField( 34 | model_name="quizrelationship", 35 | name="created", 36 | field=models.DateTimeField(auto_now_add=True, verbose_name="Date de création"), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /contributions/migrations/0017_alter_comment_status_default.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-03-23 23:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("contributions", "0016_comment_publish"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="comment", 15 | name="status", 16 | field=models.CharField( 17 | choices=[ 18 | ("NEW", "To process"), 19 | ("PENDING", "In progress"), 20 | ("PROCESSED", "Processed"), 21 | ("IGNORED", "Ignored"), 22 | ], 23 | default="NEW", 24 | max_length=150, 25 | verbose_name="Status", 26 | ), 27 | ), 28 | migrations.AlterField( 29 | model_name="historicalcomment", 30 | name="status", 31 | field=models.CharField( 32 | choices=[ 33 | ("NEW", "To process"), 34 | ("PENDING", "In progress"), 35 | ("PROCESSED", "Processed"), 36 | ("IGNORED", "Ignored"), 37 | ], 38 | default="NEW", 39 | max_length=150, 40 | verbose_name="Status", 41 | ), 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /questions/migrations/0002_question_author_link_question_validator_link.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-04-16 09:19 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ("questions", "0001_initial"), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name="question", 18 | name="author_link", 19 | field=models.ForeignKey( 20 | help_text="L'auteur de la question", 21 | blank=True, 22 | null=True, 23 | on_delete=django.db.models.deletion.SET_NULL, 24 | related_name="questions", 25 | to=settings.AUTH_USER_MODEL, 26 | ), 27 | ), 28 | migrations.AddField( 29 | model_name="question", 30 | name="validator_link", 31 | field=models.ForeignKey( 32 | blank=True, 33 | help_text="La personne qui a validée la question", 34 | null=True, 35 | on_delete=django.db.models.deletion.SET_NULL, 36 | related_name="questions_validated", 37 | to=settings.AUTH_USER_MODEL, 38 | ), 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /templates/users/administrator_list.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | {% load render_table from django_tables2 %} 3 | {% load i18n django_bootstrap5 %} 4 | 5 | {% block title %}{% translate "Administrator list" %}{{ block.super }}{% endblock %} 6 | 7 | {% block breadcrumbs %} 8 |
    9 |
    10 |
    11 | 18 |
    19 |
    20 |
    21 | {% endblock %} 22 | 23 | {% block content %} 24 |
    25 |
    26 |
    27 |

    {% translate "Administrator list" %} {{ administrators.count }}

    28 |
    29 |
    30 | 31 |
    32 |
    33 | {% render_table table %} 34 |
    35 |
    36 |
    37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /www/users/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import ListView, TemplateView 2 | from django_tables2.views import SingleTableView 3 | 4 | from core.mixins import ContributorUserRequiredMixin 5 | from users.models import User 6 | from users.tables import AdministratorTable 7 | 8 | 9 | class UserHomeView(ContributorUserRequiredMixin, TemplateView): 10 | template_name = "users/home.html" 11 | 12 | def get_context_data(self, **kwargs): 13 | context = super().get_context_data(**kwargs) 14 | context["contributor_count"] = User.objects.all_contributors().count() 15 | context["quiz_author_count"] = User.objects.has_public_quiz().count() 16 | context["administrator_count"] = User.objects.all_administrators().count() 17 | return context 18 | 19 | 20 | class AuthorCardList(ContributorUserRequiredMixin, ListView): 21 | queryset = User.objects.has_user_card().order_by("-created") 22 | template_name = "users/author_card_list.html" 23 | context_object_name = "users_with_card" 24 | 25 | 26 | class AdministratorListView(ContributorUserRequiredMixin, SingleTableView): 27 | model = User 28 | template_name = "users/administrator_list.html" 29 | context_object_name = "administrators" 30 | table_class = AdministratorTable 31 | 32 | def get_queryset(self): 33 | qs = super().get_queryset() 34 | qs = qs.all_administrators() 35 | qs = qs.order_by("created") 36 | return qs 37 | -------------------------------------------------------------------------------- /categories/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-03-30 22:31 2 | 3 | import ckeditor.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="Category", 16 | fields=[ 17 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 18 | ("name", models.CharField(help_text="Le nom de la catégorie", max_length=50)), 19 | ("name_long", models.CharField(help_text="Le nom allongé de la catégorie", max_length=150)), 20 | ( 21 | "description", 22 | ckeditor.fields.RichTextField(blank=True, help_text="Une description de la catégorie"), 23 | ), 24 | ("created", models.DateField(auto_now_add=True, help_text="La date de création de la catégorie")), 25 | ], 26 | options={ 27 | "verbose_name": "Category", 28 | "verbose_name_plural": "Categories", 29 | "ordering": ["pk"], 30 | }, 31 | ), 32 | migrations.AddConstraint( 33 | model_name="category", 34 | constraint=models.UniqueConstraint(fields=("name",), name="category_name_unique"), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /templates/auth/password_reset.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | {% load i18n django_bootstrap5 %} 3 | 4 | {% block title %}Réinitialisation du mot de passe{{ block.super }}{% endblock %} 5 | 6 | {% block content %} 7 |
    8 |
    9 |
    10 |

    Réinitialisation du mot de passe

    11 | 12 |

    Indiquez votre adresse e-mail ci-dessous, et nous vous enverrons un e-mail pour le réinitialiser.

    13 | 14 |
    15 | 16 |
    17 | {% csrf_token %} 18 | 19 | {% bootstrap_form_errors form type="all" %} 20 | 21 |
    22 | {% bootstrap_field form.email %} 23 |
    24 | 25 |
    26 | 27 |
    28 |
    29 | 32 |
    33 |
    34 |
    35 | 36 |
    37 |
    38 |
    39 |
    40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /quizs/filters.py: -------------------------------------------------------------------------------- 1 | import django_filters 2 | from django import forms 3 | from django.forms import NumberInput 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from core import constants 7 | from quizs.models import Quiz 8 | from tags.models import Tag 9 | 10 | 11 | TEXT_SEARCH_PLACEHOLDER = ( 12 | f"{_('In the following fields:')} " 13 | f"{Quiz._meta.get_field('name').verbose_name}, " 14 | f"{Quiz._meta.get_field('introduction').verbose_name}, " 15 | f"{Quiz._meta.get_field('conclusion').verbose_name}" 16 | ) 17 | 18 | 19 | class QuizFilter(django_filters.FilterSet): 20 | id = django_filters.NumberFilter(widget=NumberInput(attrs={"min": 0})) 21 | tags = django_filters.ModelMultipleChoiceFilter(queryset=Tag.objects.all()) 22 | publish = django_filters.ChoiceFilter(choices=constants.BOOLEAN_CHOICES) 23 | has_audio = django_filters.ChoiceFilter(choices=constants.BOOLEAN_CHOICES) 24 | q = django_filters.CharFilter( 25 | label=_("Text search"), 26 | method="text_search", 27 | widget=forms.TextInput(attrs={"placeholder": TEXT_SEARCH_PLACEHOLDER}), 28 | ) 29 | 30 | class Meta: 31 | model = Quiz 32 | fields = ["id", "authors", "tags", "language", "has_audio", "validation_status", "publish", "visibility", "q"] 33 | 34 | def text_search(self, queryset, name, value): 35 | if not value: 36 | return queryset 37 | return queryset.simple_search(value) 38 | -------------------------------------------------------------------------------- /categories/tables.py: -------------------------------------------------------------------------------- 1 | import django_tables2 as tables 2 | from django.db.models import Count 3 | 4 | from categories.models import Category 5 | from core.tables import DEFAULT_ATTRS, DEFAULT_TEMPLATE, RichTextColumn 6 | from questions.models import Question 7 | 8 | 9 | CATEGORY_FIELD_SEQUENCE = [field.name for field in Category._meta.fields] 10 | CATEGORY_FIELD_SEQUENCE.insert(CATEGORY_FIELD_SEQUENCE.index("created"), "question_count") 11 | 12 | 13 | class CategoryTable(tables.Table): 14 | id = tables.Column(linkify=lambda record: record.get_absolute_url()) 15 | description = RichTextColumn(attrs={"td": {"title": lambda record: record.description}}) 16 | question_count = tables.Column(verbose_name=Question._meta.verbose_name_plural) 17 | 18 | class Meta: 19 | model = Category 20 | fields = CATEGORY_FIELD_SEQUENCE 21 | sequence = CATEGORY_FIELD_SEQUENCE 22 | template_name = DEFAULT_TEMPLATE 23 | attrs = DEFAULT_ATTRS 24 | 25 | def __init__(self, *args, **kwargs): 26 | for field_name in Category.CATEGORY_TIMESTAMP_FIELDS: 27 | self.base_columns[field_name] = tables.DateTimeColumn(format="d F Y") 28 | super(CategoryTable, self).__init__(*args, **kwargs) 29 | 30 | def order_question_count(self, queryset, is_descending): 31 | queryset = queryset.annotate(question_agg=Count("questions")).order_by( 32 | ("-" if is_descending else "") + "question_agg" 33 | ) 34 | return (queryset, True) 35 | -------------------------------------------------------------------------------- /templates/profile/info_card_view.html: -------------------------------------------------------------------------------- 1 | {% extends "profile/info_base.html" %} 2 | {% load i18n django_bootstrap5 array_choices_item_display %} 3 | 4 | {% block title %}{% translate "My author card" %}{{ block.super }}{% endblock %} 5 | 6 | {% block profile_info_content %} 7 |
    8 |
    9 |

    10 | {% translate "My author card" %} 11 | {% if user.user_card %} 12 | {% translate "Edit" %} 13 | {% else %} 14 | {% translate "Add" %} 15 | {% endif %} 16 |

    17 |
    18 |
    19 |
    20 |
    21 | {% if not user.user_card %} 22 | 25 | {% else %} 26 | {% include "users/_author_card.html" with user=user %} 27 | {% endif %} 28 |
    29 | 30 |
    31 | 34 |
    35 |
    36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /quizs/migrations/0026_quiz_language_spanish_italian.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-22 09:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("quizs", "0025_quiz_model_translation"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="historicalquiz", 15 | name="language", 16 | field=models.CharField( 17 | choices=[ 18 | ("FRENCH", "French"), 19 | ("ENGLISH", "English"), 20 | ("SPANISH", "Spanish"), 21 | ("ITALIAN", "Italian"), 22 | ("GERMAN", "German"), 23 | ], 24 | default="FRENCH", 25 | max_length=50, 26 | verbose_name="Language", 27 | ), 28 | ), 29 | migrations.AlterField( 30 | model_name="quiz", 31 | name="language", 32 | field=models.CharField( 33 | choices=[ 34 | ("FRENCH", "French"), 35 | ("ENGLISH", "English"), 36 | ("SPANISH", "Spanish"), 37 | ("ITALIAN", "Italian"), 38 | ("GERMAN", "German"), 39 | ], 40 | default="FRENCH", 41 | max_length=50, 42 | verbose_name="Language", 43 | ), 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /glossary/tables.py: -------------------------------------------------------------------------------- 1 | import django_tables2 as tables 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from core.tables import DEFAULT_ATTRS, DEFAULT_TEMPLATE, ChoiceColumn, RichTextEllipsisColumn 5 | from glossary.models import GlossaryItem 6 | 7 | 8 | GLOSSARY_FIELDS = [ 9 | "name", 10 | "name_alternatives", 11 | "definition_short", 12 | "description", 13 | "has_description_accessible_url", 14 | "language", 15 | "created", 16 | "action", 17 | ] # id, description_accessible_url, updated 18 | 19 | 20 | class GlossaryTable(tables.Table): 21 | description = RichTextEllipsisColumn(attrs={"td": {"title": lambda record: record.description}}) 22 | has_description_accessible_url = tables.Column( 23 | verbose_name=GlossaryItem._meta.get_field("description_accessible_url").verbose_name, 24 | accessor="has_description_accessible_url_icon", 25 | ) 26 | action = tables.TemplateColumn( 27 | verbose_name=_("Actions"), 28 | template_name="glossary/_table_action_items.html", 29 | attrs={"th": {"style": "min-width:130px"}}, 30 | ) 31 | 32 | class Meta: 33 | model = GlossaryItem 34 | template_name = DEFAULT_TEMPLATE 35 | fields = GLOSSARY_FIELDS 36 | attrs = DEFAULT_ATTRS 37 | 38 | def __init__(self, *args, **kwargs): 39 | for field_name in GlossaryItem.GLOSSARY_ITEM_CHOICE_FIELDS: 40 | self.base_columns[field_name] = ChoiceColumn() 41 | super().__init__(*args, **kwargs) 42 | -------------------------------------------------------------------------------- /www/quizs/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from django.views.generic.base import RedirectView 3 | 4 | from www.quizs.views import ( 5 | QuizCreateView, 6 | QuizDetailCommentListView, 7 | QuizDetailEditView, 8 | QuizDetailHistoryView, 9 | QuizDetailQuestionListView, 10 | QuizDetailStatsView, 11 | QuizDetailView, 12 | QuizListView, 13 | ) 14 | 15 | 16 | app_name = "quizs" 17 | 18 | urlpatterns = [ 19 | path("", QuizListView.as_view(), name="list"), 20 | # path("/", QuizDetailView.as_view(), name="detail"), 21 | path( 22 | "/", 23 | include( 24 | [ 25 | path( 26 | "", 27 | RedirectView.as_view(pattern_name="quizs:detail_view", permanent=False), 28 | name="detail", 29 | ), 30 | path("view/", QuizDetailView.as_view(), name="detail_view"), 31 | path("edit/", QuizDetailEditView.as_view(), name="detail_edit"), 32 | path("questions/", QuizDetailQuestionListView.as_view(), name="detail_questions"), 33 | path("comments/", QuizDetailCommentListView.as_view(), name="detail_comments"), 34 | path("stats/", QuizDetailStatsView.as_view(), name="detail_stats"), 35 | path("history/", QuizDetailHistoryView.as_view(), name="detail_history"), 36 | ] 37 | ), 38 | ), 39 | path("create/", QuizCreateView.as_view(), name="create"), 40 | ] 41 | -------------------------------------------------------------------------------- /glossary/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-03-30 22:31 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="GlossaryItem", 15 | fields=[ 16 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 17 | ("name", models.CharField(help_text="Le mot ou sigle", max_length=50)), 18 | ("name_alternatives", models.TextField(blank=True, help_text="Des noms alternatifs")), 19 | ("definition_short", models.CharField(help_text="La definition succinte du mot", max_length=150)), 20 | ("description", models.TextField(blank=True, help_text="Une description longue du mot")), 21 | ( 22 | "description_accessible_url", 23 | models.URLField(blank=True, help_text="Un lien pour aller plus loin", max_length=500), 24 | ), 25 | ("added", models.DateField(blank=True, help_text="La date d'ajout du mot", null=True)), 26 | ("created", models.DateField(auto_now_add=True, help_text="La date de création du mot")), 27 | ("updated", models.DateField(auto_now=True)), 28 | ], 29 | options={ 30 | "ordering": ["name"], 31 | }, 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /glossary/migrations/0010_glossaryitem_language.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-23 13:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("glossary", "0009_glossaryitem_model_translate"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="glossaryitem", 15 | name="language", 16 | field=models.CharField( 17 | choices=[ 18 | ("FRENCH", "French"), 19 | ("ENGLISH", "English"), 20 | ("SPANISH", "Spanish"), 21 | ("ITALIAN", "Italian"), 22 | ("GERMAN", "German"), 23 | ], 24 | default="FRENCH", 25 | max_length=50, 26 | verbose_name="Language", 27 | ), 28 | ), 29 | migrations.AddField( 30 | model_name="historicalglossaryitem", 31 | name="language", 32 | field=models.CharField( 33 | choices=[ 34 | ("FRENCH", "French"), 35 | ("ENGLISH", "English"), 36 | ("SPANISH", "Spanish"), 37 | ("ITALIAN", "Italian"), 38 | ("GERMAN", "German"), 39 | ], 40 | default="FRENCH", 41 | max_length=50, 42 | verbose_name="Language", 43 | ), 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /templates/auth/password_reset_sent.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | 3 | {% block title %}Demande de réinitialisation du mot de passe envoyée{{ block.super }}{% endblock %} 4 | 5 | {% block content %} 6 |
    7 |
    8 |
    9 |

    Demande de réinitialisation du mot de passe envoyée

    10 |
    11 |

    12 | Si un compte existe, vous recevrez un e-mail de réinitialisation 13 | {% if request.GET.email %} 14 | 15 | à l'adresse email : {{ request.GET.email }} 16 | {% endif %} 17 |

    18 |
    19 |
    20 |

    Il contient les instructions pour réinitialiser votre mot de passe.

    21 |

    Si vous le ne recevez pas dans les minutes qui suivent :

    22 |
      23 |
    • vérifiez votre courrier indésirable
    • 24 |
    • vérifiez qu'il n'y a pas d'erreur dans votre e-mail
    • 25 |
    26 |
    27 |
    28 |
    29 |
    30 | 33 |
    34 |
    35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /users/filters.py: -------------------------------------------------------------------------------- 1 | import django_filters 2 | from django import forms 3 | 4 | from core import constants 5 | from users import constants as user_constants 6 | from users.models import User 7 | 8 | 9 | class ContributorFilter(django_filters.FilterSet): 10 | roles = django_filters.ChoiceFilter( 11 | label="Rôle", choices=user_constants.USER_ROLE_CHOICES, lookup_expr="icontains" 12 | ) 13 | has_question = django_filters.ChoiceFilter( 14 | label="Auteur de questions ?", choices=constants.BOOLEAN_CHOICES, method="has_question_filter" 15 | ) 16 | has_quiz = django_filters.ChoiceFilter( 17 | label="Auteur de quizs ?", choices=constants.BOOLEAN_CHOICES, method="has_quiz_filter" 18 | ) 19 | q = django_filters.CharFilter( 20 | label="Recherche", 21 | method="text_search", 22 | widget=forms.TextInput(attrs={"placeholder": "Dans les champs 'prénom', 'nom' et 'e-mail'"}), 23 | ) 24 | 25 | class Meta: 26 | model = User 27 | fields = ["roles", "has_question", "has_quiz", "q"] 28 | 29 | def has_question_filter(self, queryset, name, value): 30 | if not value: 31 | return queryset 32 | return queryset.has_question() 33 | 34 | def has_quiz_filter(self, queryset, name, value): 35 | if not value: 36 | return queryset 37 | return queryset.has_quiz() 38 | 39 | def text_search(self, queryset, name, value): 40 | if not value: 41 | return queryset 42 | return queryset.simple_search(value) 43 | -------------------------------------------------------------------------------- /templates/profile/comments_view.html: -------------------------------------------------------------------------------- 1 | {% extends "profile/comments_base.html" %} 2 | {% load render_table from django_tables2 %} 3 | {% load i18n django_bootstrap5 %} 4 | 5 | {% block profile_comments_content %} 6 |
    7 |
    8 |

    {% translate "All comments" %} {{ user_comments.count }}

    9 |
    10 |
    11 | 16 |
    17 |
    18 | 19 | {% if search_filters %} 20 |

    21 | {% include "includes/_filter_badge_list.html" with search_filters=search_filters model_name="Comment" %} 22 |

    23 | {% endif %} 24 | 25 |
    26 | {% bootstrap_form filter.form layout="horizontal" %} 27 |
    28 | {% bootstrap_button button_type="submit" content=_("Filter") %} 29 |
    30 |
    31 | 32 |
    33 |
    34 | {% render_table table %} 35 |
    36 |
    37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /questions/migrations/0026_question_language_spanish_italian.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-22 09:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("questions", "0025_alter_historicalquestion_validation_status_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="historicalquestion", 15 | name="language", 16 | field=models.CharField( 17 | choices=[ 18 | ("FRENCH", "French"), 19 | ("ENGLISH", "English"), 20 | ("SPANISH", "Spanish"), 21 | ("ITALIAN", "Italian"), 22 | ("GERMAN", "German"), 23 | ], 24 | default="FRENCH", 25 | max_length=50, 26 | verbose_name="Language", 27 | ), 28 | ), 29 | migrations.AlterField( 30 | model_name="question", 31 | name="language", 32 | field=models.CharField( 33 | choices=[ 34 | ("FRENCH", "French"), 35 | ("ENGLISH", "English"), 36 | ("SPANISH", "Spanish"), 37 | ("ITALIAN", "Italian"), 38 | ("GERMAN", "German"), 39 | ], 40 | default="FRENCH", 41 | max_length=50, 42 | verbose_name="Language", 43 | ), 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /quizs/migrations/0010_quiz_visibility.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-08 20:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("quizs", "0009_remove_quiz_author"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="historicalquiz", 15 | name="visibility", 16 | field=models.CharField( 17 | choices=[ 18 | ("PUBLIC", "Publique (dans l'export et dans l'application)"), 19 | ("HIDDEN", "Caché (dans l'export mais pas visible dans l'application)"), 20 | ("PRIVATE", "Privé (pas dans l'export ni dans l'application)"), 21 | ], 22 | default="PUBLIC", 23 | max_length=50, 24 | verbose_name="Visibilité", 25 | ), 26 | ), 27 | migrations.AddField( 28 | model_name="quiz", 29 | name="visibility", 30 | field=models.CharField( 31 | choices=[ 32 | ("PUBLIC", "Publique (dans l'export et dans l'application)"), 33 | ("HIDDEN", "Caché (dans l'export mais pas visible dans l'application)"), 34 | ("PRIVATE", "Privé (pas dans l'export ni dans l'application)"), 35 | ], 36 | default="PUBLIC", 37 | max_length=50, 38 | verbose_name="Visibilité", 39 | ), 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /templates/questions/detail_comments.html: -------------------------------------------------------------------------------- 1 | {% extends "questions/detail_base.html" %} 2 | {% load render_table from django_tables2 %} 3 | {% load i18n django_bootstrap5 %} 4 | 5 | {% block question_detail_content %} 6 |
    7 |
    8 | {% if not table.rows|length %} 9 |
    0 {% translate "Comments" %}
    10 | {% else %} 11 | {% render_table table %} 12 | {% endif %} 13 |
    14 |
    15 | 16 |
    17 | 18 | 19 |
    20 |
    21 |
    22 |
    23 | {% translate "Add a note" %} 24 |
    25 |
    26 |
    27 | {% csrf_token %} 28 | 29 |
    30 |
    31 | {% bootstrap_form form alert_error_type="all" %} 32 |
    33 |
    34 | 35 |
    36 |
    37 | {% bootstrap_button button_type="submit" button_class="btn-primary" content=_("Add") %} 38 |
    39 |
    40 |
    41 |
    42 |
    43 |
    44 |
    45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /categories/models.py: -------------------------------------------------------------------------------- 1 | from ckeditor.fields import RichTextField 2 | from django.db import models 3 | from django.urls import reverse 4 | from django.utils import timezone 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | 8 | class Category(models.Model): 9 | CATEGORY_TIMESTAMP_FIELDS = ["created", "updated"] 10 | 11 | name = models.CharField(verbose_name=_("Name"), max_length=50, blank=False) 12 | name_long = models.CharField(verbose_name=_("Name (long version)"), max_length=150, blank=False) 13 | description = RichTextField(verbose_name=_("Description"), blank=True) 14 | 15 | created = models.DateTimeField(verbose_name=_("Creation date"), default=timezone.now) 16 | updated = models.DateTimeField(verbose_name=_("Last update date"), auto_now=True) 17 | 18 | class Meta: 19 | verbose_name = _("Category") 20 | verbose_name_plural = _("Categories") 21 | ordering = ["pk"] 22 | constraints = [models.UniqueConstraint(fields=["name"], name="category_name_unique")] 23 | 24 | def __str__(self): 25 | return f"{self.name}" 26 | 27 | def get_absolute_url(self): 28 | return reverse("categories:detail", kwargs={"pk": self.id}) 29 | 30 | @property 31 | def question_count(self) -> int: 32 | return self.questions.count() 33 | 34 | @property 35 | def question_public_validated_count(self) -> int: 36 | return self.questions.public().validated().count() 37 | 38 | # Admin 39 | question_public_validated_count.fget.short_description = _("Questions (public & validated)") 40 | -------------------------------------------------------------------------------- /quizs/migrations/0024_alter_historicalquiz_validation_status_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-19 09:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("quizs", "0023_quiz_validation_status_migration"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="historicalquiz", 15 | name="validation_status", 16 | field=models.CharField( 17 | choices=[ 18 | ("DRAFT", "Draft"), 19 | ("TO_VALIDATE", "To validate"), 20 | ("VALIDATED", "Validated"), 21 | ("ASIDE", "Set aside"), 22 | ("REMOVED", "Removed"), 23 | ], 24 | default="DRAFT", 25 | max_length=150, 26 | verbose_name="Statut", 27 | ), 28 | ), 29 | migrations.AlterField( 30 | model_name="quiz", 31 | name="validation_status", 32 | field=models.CharField( 33 | choices=[ 34 | ("DRAFT", "Draft"), 35 | ("TO_VALIDATE", "To validate"), 36 | ("VALIDATED", "Validated"), 37 | ("ASIDE", "Set aside"), 38 | ("REMOVED", "Removed"), 39 | ], 40 | default="DRAFT", 41 | max_length=150, 42 | verbose_name="Statut", 43 | ), 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /quizs/migrations/0027_quiz_visibility_translation_fix.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-22 18:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("quizs", "0026_quiz_language_spanish_italian"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="historicalquiz", 15 | name="visibility", 16 | field=models.CharField( 17 | choices=[ 18 | ("PUBLIC", "Public (exported and in the application)"), 19 | ("HIDDEN", "Hidden (exported but not visible in the application)"), 20 | ("PRIVATE", "Private (not exported and not in the application)"), 21 | ], 22 | default="PUBLIC", 23 | max_length=50, 24 | verbose_name="Visibility", 25 | ), 26 | ), 27 | migrations.AlterField( 28 | model_name="quiz", 29 | name="visibility", 30 | field=models.CharField( 31 | choices=[ 32 | ("PUBLIC", "Public (exported and in the application)"), 33 | ("HIDDEN", "Hidden (exported but not visible in the application)"), 34 | ("PRIVATE", "Private (not exported and not in the application)"), 35 | ], 36 | default="PUBLIC", 37 | max_length=50, 38 | verbose_name="Visibility", 39 | ), 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /templates/auth/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | {% load i18n django_bootstrap5 %} 3 | 4 | {% block title %}Connexion{{ block.super }}{% endblock %} 5 | 6 | {% block content %} 7 |
    8 |
    9 |
    10 |

    Connexion

    11 | 12 |
    13 | 14 |
    15 | {% csrf_token %} 16 | 17 | {% if form.errors %} 18 | 19 | {% endif %} 20 | 21 |
    22 | {% bootstrap_field form.username %} 23 | {% bootstrap_field form.password %} 24 | Mot de passe oublié ? 25 |
    26 | 27 |
    28 | 29 |
    30 |
    31 | 34 |
    35 |
    36 | 37 |
    38 |
    39 |
    40 |
    41 |
    42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /questions/migrations/0025_alter_historicalquestion_validation_status_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-19 09:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("questions", "0024_question_validation_status_migration"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="historicalquestion", 15 | name="validation_status", 16 | field=models.CharField( 17 | choices=[ 18 | ("DRAFT", "Draft"), 19 | ("TO_VALIDATE", "To validate"), 20 | ("VALIDATED", "Validated"), 21 | ("ASIDE", "Set aside"), 22 | ("REMOVED", "Removed"), 23 | ], 24 | default="DRAFT", 25 | max_length=150, 26 | verbose_name="Status", 27 | ), 28 | ), 29 | migrations.AlterField( 30 | model_name="question", 31 | name="validation_status", 32 | field=models.CharField( 33 | choices=[ 34 | ("DRAFT", "Draft"), 35 | ("TO_VALIDATE", "To validate"), 36 | ("VALIDATED", "Validated"), 37 | ("ASIDE", "Set aside"), 38 | ("REMOVED", "Removed"), 39 | ], 40 | default="DRAFT", 41 | max_length=150, 42 | verbose_name="Status", 43 | ), 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /questions/migrations/0027_question_visibility_translation_fix.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-22 18:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("questions", "0026_question_language_spanish_italian"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="historicalquestion", 15 | name="visibility", 16 | field=models.CharField( 17 | choices=[ 18 | ("PUBLIC", "Public (exported and in the application)"), 19 | ("HIDDEN", "Hidden (exported but not visible in the application)"), 20 | ("PRIVATE", "Private (not exported and not in the application)"), 21 | ], 22 | default="PUBLIC", 23 | max_length=50, 24 | verbose_name="Visibility", 25 | ), 26 | ), 27 | migrations.AlterField( 28 | model_name="question", 29 | name="visibility", 30 | field=models.CharField( 31 | choices=[ 32 | ("PUBLIC", "Public (exported and in the application)"), 33 | ("HIDDEN", "Hidden (exported but not visible in the application)"), 34 | ("PRIVATE", "Private (not exported and not in the application)"), 35 | ], 36 | default="PUBLIC", 37 | max_length=50, 38 | verbose_name="Visibility", 39 | ), 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /questions/migrations/0011_question_visibility.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-08 20:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("questions", "0010_remove_historicalquestion_added_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="historicalquestion", 15 | name="visibility", 16 | field=models.CharField( 17 | choices=[ 18 | ("PUBLIC", "Publique (dans l'export et dans l'application)"), 19 | ("HIDDEN", "Caché (dans l'export mais pas visible dans l'application)"), 20 | ("PRIVATE", "Privé (pas dans l'export ni dans l'application)"), 21 | ], 22 | default="PUBLIC", 23 | max_length=50, 24 | verbose_name="Visibilité", 25 | ), 26 | ), 27 | migrations.AddField( 28 | model_name="question", 29 | name="visibility", 30 | field=models.CharField( 31 | choices=[ 32 | ("PUBLIC", "Publique (dans l'export et dans l'application)"), 33 | ("HIDDEN", "Caché (dans l'export mais pas visible dans l'application)"), 34 | ("PRIVATE", "Privé (pas dans l'export ni dans l'application)"), 35 | ], 36 | default="PUBLIC", 37 | max_length=50, 38 | verbose_name="Visibilité", 39 | ), 40 | ), 41 | ] 42 | --------------------------------------------------------------------------------