├── .circleci └── config.yml ├── .coveragerc ├── .gitattributes ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── blogging └── writeup_creation │ ├── ansible_deployment_on_gcp.yml │ └── pytorch_catch_memory_leak.py ├── compose ├── .zshrc ├── local │ └── django │ │ └── local_django_start.sh └── production │ ├── django │ ├── Dockerfile │ ├── aliases.txt │ ├── celery │ │ ├── beat │ │ │ └── start.sh │ │ └── worker │ │ │ └── start.sh │ ├── entrypoint.sh │ └── start.sh │ ├── postgres │ ├── Dockerfile │ └── maintenance │ │ ├── _sourced │ │ ├── constants.sh │ │ ├── countdown.sh │ │ ├── messages.sh │ │ └── yes_no.sh │ │ ├── backup │ │ ├── backups │ │ └── restore │ └── traefik │ ├── Dockerfile │ └── traefik.toml ├── config ├── __init__.py ├── asgi.py ├── celery_app.py ├── constants.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── local.py │ ├── production.py │ └── test.py ├── urls.py └── wsgi.py ├── local.yml ├── manage.py ├── open ├── __init__.py ├── constants.py ├── contrib │ ├── __init__.py │ └── sites │ │ ├── __init__.py │ │ └── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_alter_domain_unique.py │ │ ├── 0003_set_site_domain_and_name.py │ │ └── __init__.py ├── core │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── betterself │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── constants.py │ │ ├── factories.py │ │ ├── fixtures │ │ │ ├── __init__.py │ │ │ ├── demo_constants.py │ │ │ └── supplement_fixtures.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── activity.py │ │ │ ├── activity_log.py │ │ │ ├── daily_productivity_log.py │ │ │ ├── food.py │ │ │ ├── food_logs.py │ │ │ ├── ingredient.py │ │ │ ├── ingredient_composition.py │ │ │ ├── measurement.py │ │ │ ├── sleep_log.py │ │ │ ├── supplement.py │ │ │ ├── supplement_log.py │ │ │ ├── supplement_stack.py │ │ │ ├── supplement_stack_composition.py │ │ │ └── well_being_log.py │ │ ├── serializers │ │ │ ├── __init__.py │ │ │ ├── activity_log_serializers.py │ │ │ ├── activity_serializers.py │ │ │ ├── aggregrate_serializers.py │ │ │ ├── daily_productivity_log_serializers.py │ │ │ ├── food_log_serializers.py │ │ │ ├── food_serializers.py │ │ │ ├── ingredient_composition_serializers.py │ │ │ ├── ingredient_serializers.py │ │ │ ├── measurement_serializers.py │ │ │ ├── mixins.py │ │ │ ├── simple_generic_serializer.py │ │ │ ├── sleep_log_serializers.py │ │ │ ├── supplement_log_serializers.py │ │ │ ├── supplement_serializers.py │ │ │ ├── supplement_stack_composition_serializers.py │ │ │ ├── supplement_stack_serializers.py │ │ │ ├── validators.py │ │ │ └── well_being_log_serializers.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── mixins │ │ │ │ ├── __init__.py │ │ │ │ └── resource_mixin.py │ │ │ ├── test_demo_fixture_creation.py │ │ │ ├── test_factories.py │ │ │ ├── test_validators.py │ │ │ └── views │ │ │ │ ├── __init__.py │ │ │ │ ├── test_activity_log_views.py │ │ │ │ ├── test_activity_views.py │ │ │ │ ├── test_aggregrate_views.py │ │ │ │ ├── test_daily_productivity_log_views.py │ │ │ │ ├── test_daily_review_view.py │ │ │ │ ├── test_food_log_views.py │ │ │ │ ├── test_food_views.py │ │ │ │ ├── test_ingredient_composition_views.py │ │ │ │ ├── test_ingredient_views.py │ │ │ │ ├── test_measurement_views.py │ │ │ │ ├── test_overview_view.py │ │ │ │ ├── test_sleep_log_views.py │ │ │ │ ├── test_supplement_log_views.py │ │ │ │ ├── test_supplement_stack_composition_views.py │ │ │ │ ├── test_supplement_stack_views.py │ │ │ │ ├── test_supplement_views.py │ │ │ │ └── test_well_being_log_views.py │ │ ├── urls.py │ │ ├── utilities │ │ │ ├── __init__.py │ │ │ ├── demo_user_factory_fixtures.py │ │ │ ├── history_overview_utilities.py │ │ │ ├── serializer_utilties.py │ │ │ └── user_date_utilities.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── activity_log_views.py │ │ │ ├── activity_views.py │ │ │ ├── aggregrate_views.py │ │ │ ├── auth_views.py │ │ │ ├── daily_productivity_log_views.py │ │ │ ├── daily_view.py │ │ │ ├── food_log_views.py │ │ │ ├── food_views.py │ │ │ ├── ingredient_composition_views.py │ │ │ ├── ingredient_views.py │ │ │ ├── measurement.py │ │ │ ├── mixins.py │ │ │ ├── overview_views.py │ │ │ ├── sleep_log_views.py │ │ │ ├── supplement_log_views.py │ │ │ ├── supplement_stack_composition_views.py │ │ │ ├── supplement_stack_views.py │ │ │ ├── supplement_views.py │ │ │ └── well_being_log_views.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── show_urls_no_admin.py │ ├── migrations │ │ ├── 0001_writeup_shared_prompt.py │ │ ├── 0002_writeup_prompt_models_refactored.py │ │ ├── 0003_add_score_to_prompt_model.py │ │ ├── 0004_writeupprompt_add_content.py │ │ ├── 0005_add_betterself_models_part_1.py │ │ ├── 0006_betterself_basic_models_v2.py │ │ ├── 0007_create_betterself_models_p2.py │ │ ├── 0008_rename_supp_stack_and_compositions.py │ │ ├── 0009_add_measurement_name_uniqueness.py │ │ ├── 0010_data_migration_add_demo_user.py │ │ ├── 0011_auto_20200711_2029.py │ │ ├── 0012_data_migration_force_api_tokens.py │ │ ├── 0013_fix_demo_fixture.py │ │ ├── 0014_auto_20200713_1147.py │ │ ├── 0015_add_pomodoro_count_to_productivity_log.py │ │ ├── 0016_null_duration_minutes_activity_log.py │ │ ├── 0017_supplement_is_taken_with_food.py │ │ ├── 0018_food_foodlog.py │ │ ├── 0019_dailyproductivitylog_mistakes.py │ │ ├── 0020_supplementlog_duration_minutes.py │ │ └── __init__.py │ ├── models.py │ ├── scripts │ │ ├── __init__.py │ │ ├── betterself_create_demo_fixtures.py │ │ ├── betterself_import_historical_productivity.py │ │ ├── betterself_import_legacy_app.py │ │ ├── betterself_prototyping.py │ │ ├── betterself_reset_password.py │ │ ├── clear_redis_cache.py │ │ ├── cloudflare_extract_historical_traffic.py │ │ ├── swarm_ml_services.py │ │ ├── utilities.py │ │ ├── writeup_debug_end_of_text_not_serialized.py │ │ ├── writeup_mock_ws_listeners.py │ │ ├── writeup_profile_prompt_generate_view.py │ │ └── writeup_profile_serializers.py │ ├── tasks.py │ ├── tests │ │ ├── __init__.py │ │ └── test_urls_configured.py │ ├── utilities │ │ ├── __init__.py │ │ └── cloudflare.py │ └── writeup │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── caches.py │ │ ├── constants.py │ │ ├── consumers.py │ │ ├── factories.py │ │ ├── models.py │ │ ├── routing.py │ │ ├── serializers.py │ │ ├── tests │ │ ├── __init__.py │ │ ├── test_caches.py │ │ ├── test_factories.py │ │ ├── test_serializers.py │ │ ├── test_utilities.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── test_flagged_prompt_views.py │ │ │ ├── test_gpt2_debug_view.py │ │ │ ├── test_prompt_view.py │ │ │ └── test_prompt_vote_views.py │ │ ├── urls.py │ │ ├── utilities │ │ ├── __init__.py │ │ ├── access_permissions.py │ │ └── text_algo_serializers.py │ │ └── views.py ├── routing.py ├── static │ ├── css │ │ └── project.css │ ├── fonts │ │ └── .gitkeep │ ├── images │ │ └── favicons │ │ │ ├── favicon.ico │ │ │ └── gatsby-icon.png │ ├── js │ │ └── project.js │ └── sass │ │ ├── custom_bootstrap_vars.scss │ │ └── project.scss ├── templates │ ├── 403_csrf.html │ ├── 404.html │ ├── 500.html │ ├── base.html │ └── writeup │ │ ├── index.html │ │ └── room.html ├── tests │ └── __init__.py ├── users │ ├── __init__.py │ ├── adapters.py │ ├── admin.py │ ├── apps.py │ ├── constants.py │ ├── factories.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_user_uuid.py │ │ ├── 0003_add_user_signup_fields.py │ │ ├── 0004_adjust_user_signup_defaults.py │ │ ├── 0005_add_created_modified_to_user.py │ │ ├── 0006_auto_20200812_0936.py │ │ ├── 0007_user_timezone_string.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tasks.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_factories.py │ │ ├── test_registration.py │ │ ├── test_tasks.py │ │ ├── test_user_delete_views.py │ │ └── test_user_details_view.py │ ├── urls.py │ ├── utilities.py │ └── views.py └── utilities │ ├── __init__.py │ ├── dataframes.py │ ├── date_and_time.py │ ├── fields.py │ ├── importing_models.py │ ├── models.py │ ├── profilers.py │ ├── testing.py │ └── testing_mixins.py ├── production.yml ├── production_primary.yml ├── requirements ├── base.txt ├── local.txt └── production.txt ├── scripts ├── clean_legal_dataset.py └── restore_db_from_snapshot.sh └── setup.cfg /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | build: 4 | working_directory: ~/app 5 | docker: 6 | - image: circleci/python:3.6 7 | environment: 8 | DATABASE_URL: postgres://open_user_backend@localhost:5432/open 9 | USE_DOCKER: yes 10 | CELERY_BROKER_URL: redis://redis:6379/0 11 | REDIS_URL: redis://redis:6379/0 12 | - image: circleci/postgres:9.6.2 13 | environment: 14 | POSTGRES_USER: open_user_backend 15 | POSTGRES_DB: open 16 | steps: 17 | - checkout 18 | - restore_cache: 19 | key: requirements-{{ checksum "requirements/base.txt" }} 20 | - run: 21 | command: | 22 | sudo apt-get update 23 | virtualenv venv 24 | . venv/bin/activate 25 | pip install -U pip setuptools 26 | pip install -r requirements/production.txt 27 | pip install -r requirements/local.txt 28 | - save_cache: 29 | key: requirements-{{ checksum "requirements/base.txt" }} 30 | paths: 31 | - "venv" 32 | - run: 33 | name: Run Django Tests 34 | command: | 35 | . venv/bin/activate 36 | python manage.py test 37 | - run: 38 | name: Run Coveralls Test Coverage 39 | command: | 40 | . venv/bin/activate 41 | coverage run manage.py test 42 | coveralls 43 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | include = open/* 3 | omit = *migrations*, *tests*, *scripts*, *open/templates*, *venv* 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | static/* linguist-vendored 3 | css/* linguist-vendored 4 | templates/* linguist-vendored 5 | open/templates/* linguist-vendored 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v1.3.0 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-merge-conflict 7 | - id: check-json 8 | - id: trailing-whitespace 9 | files: \.(py|sh|yaml|txt)$ 10 | - id: requirements-txt-fixer 11 | - id: check-xml 12 | - id: debug-statements 13 | files: \.py$ 14 | exclude: core/scripts/* 15 | - id: end-of-file-fixer 16 | files: \.(py|sh)$ 17 | - id: flake8 # flake8 catch not used variables 18 | files: \.py$ 19 | args: ['--ignore=E128,W503,E501'] 20 | - id: name-tests-test 21 | files: tests/.+\.py$ 22 | args: ['--django'] # Use args: ['--django'] to match test*.py instead. 23 | exclude: mixins* 24 | - repo: https://github.com/ambv/black 25 | rev: stable 26 | hooks: 27 | - id: black 28 | args: [--line-length=88, --safe] 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | Copyright (c) 2019, Jeff Shek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | -------------------------------------------------------------------------------- /blogging/writeup_creation/ansible_deployment_on_gcp.yml: -------------------------------------------------------------------------------- 1 | - name: Deploy to Open 2 | # startup scripts does most of the hard work, but make sure 3 | # you're only deploying to things that finished from startup scripts 4 | hosts: open_django:&finished_startup 5 | gather_facts: true 6 | become: true 7 | 8 | post_tasks: 9 | - name: Run Startup Script 10 | shell: | 11 | google_metadata_script_runner --script-type startup --debug 12 | args: 13 | chdir: / 14 | become: yes 15 | -------------------------------------------------------------------------------- /blogging/writeup_creation/pytorch_catch_memory_leak.py: -------------------------------------------------------------------------------- 1 | def get_process_prompt_response(request, validated_data): 2 | try: 3 | output = generate_sequences_from_prompt(**validated_data) 4 | except RuntimeError as exc: 5 | if "out of memory" in str(exc): 6 | logger.exception( 7 | f"Ran Out of Memory When Running {validated_data}. Clearing Cache." 8 | ) 9 | torch.cuda.empty_cache() 10 | 11 | oom_response = get_oom_response(validated_data) 12 | return oom_response 13 | 14 | response = serialize_sequences_to_response( 15 | output, 16 | validated_data["prompt"], 17 | validated_data["cache_key"], 18 | WebsocketMessageTypes.COMPLETED_RESPONSE, 19 | completed=validated_data["length"], 20 | length=validated_data["length"], 21 | ) 22 | 23 | # clear cache on all responses (maybe this is overkill) 24 | torch.cuda.empty_cache() 25 | return response 26 | -------------------------------------------------------------------------------- /compose/.zshrc: -------------------------------------------------------------------------------- 1 | # Path to your oh-my-zsh installation. 2 | export ZSH="/root/.oh-my-zsh" 3 | 4 | ZSH_THEME="robbyrussell" 5 | ZSH_THEME="michelebologna" 6 | 7 | # Uncomment the following line to use case-sensitive completion. 8 | # CASE_SENSITIVE="true" 9 | 10 | # Uncomment the following line to use hyphen-insensitive completion. 11 | # Case-sensitive completion must be off. _ and - will be interchangeable. 12 | # HYPHEN_INSENSITIVE="true" 13 | 14 | # Uncomment the following line to disable bi-weekly auto-update checks. 15 | # DISABLE_AUTO_UPDATE="true" 16 | 17 | # Uncomment the following line to automatically update without prompting. 18 | # DISABLE_UPDATE_PROMPT="true" 19 | 20 | # Uncomment the following line to change how often to auto-update (in days). 21 | # export UPDATE_ZSH_DAYS=13 22 | 23 | # Uncomment the following line if pasting URLs and other text is messed up. 24 | # DISABLE_MAGIC_FUNCTIONS=true 25 | 26 | # Uncomment the following line to disable colors in ls. 27 | # DISABLE_LS_COLORS="true" 28 | 29 | # Uncomment the following line to disable auto-setting terminal title. 30 | # DISABLE_AUTO_TITLE="true" 31 | 32 | # Uncomment the following line to enable command auto-correction. 33 | # ENABLE_CORRECTION="true" 34 | 35 | # Uncomment the following line to display red dots whilst waiting for completion. 36 | # COMPLETION_WAITING_DOTS="true" 37 | 38 | # Uncomment the following line if you want to disable marking untracked files 39 | # under VCS as dirty. This makes repository status check for large repositories 40 | # much, much faster. 41 | # DISABLE_UNTRACKED_FILES_DIRTY="true" 42 | 43 | # Uncomment the following line if you want to change the command execution time 44 | # stamp shown in the history command output. 45 | # You can set one of the optional three formats: 46 | # "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd" 47 | # or set a custom format using the strftime function format specifications, 48 | # see 'man strftime' for details. 49 | # HIST_STAMPS="mm/dd/yyyy" 50 | 51 | # Would you like to use another custom folder than $ZSH/custom? 52 | # ZSH_CUSTOM=/path/to/new-custom-folder 53 | 54 | # Which plugins would you like to load? 55 | # Standard plugins can be found in ~/.oh-my-zsh/plugins/* 56 | # Custom plugins may be added to ~/.oh-my-zsh/custom/plugins/ 57 | # Example format: plugins=(rails git textmate ruby lighthouse) 58 | # Add wisely, as too many plugins slow down shell startup. 59 | plugins=(git) 60 | 61 | source $ZSH/oh-my-zsh.sh 62 | 63 | # User configuration 64 | # export MANPATH="/usr/local/man:$MANPATH" 65 | 66 | # You may need to manually set your language environment 67 | # export LANG=en_US.UTF-8 68 | 69 | # Preferred editor for local and remote sessions 70 | # if [[ -n $SSH_CONNECTION ]]; then 71 | # export EDITOR='vim' 72 | # else 73 | # export EDITOR='mvim' 74 | # fi 75 | 76 | # Compilation flags 77 | # export ARCHFLAGS="-arch x86_64" 78 | 79 | source /root/.oh-my-zsh/oh-my-zsh.sh 80 | rm ~/.zcompdump-* 81 | 82 | alias dpy="python manage.py" 83 | -------------------------------------------------------------------------------- /compose/local/django/local_django_start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | python manage.py migrate 7 | python manage.py collectstatic --noinput 8 | 9 | #/usr/local/bin/daphne -b 0.0.0.0 -p 5000 config.asgi:application 10 | # we use this now for django channels, versus runserver_plus 11 | python manage.py runserver 0.0.0.0:5000 12 | -------------------------------------------------------------------------------- /compose/production/django/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6-jessie 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | RUN apt-get update 6 | RUN apt-get install apt-utils python3-pip git zsh redis-tools nano -y 7 | 8 | RUN pip install --upgrade pip 9 | 10 | # Requirements are installed here to ensure they will be cached. 11 | COPY ./requirements /requirements 12 | RUN pip install --no-cache-dir -r /requirements/local.txt 13 | RUN pip install --no-cache-dir -r /requirements/production.txt 14 | RUN rm -rf /requirements 15 | 16 | COPY ./compose/production/django/entrypoint.sh /entrypoint.sh 17 | RUN sed -i 's/\r//' /entrypoint.sh 18 | RUN chmod +x /entrypoint.sh 19 | 20 | # create a local django start to cram everything into one dockerfile since i dislike maintaining two similar 21 | # but different dockerfiles 22 | COPY ./compose/local/django/local_django_start.sh /local_django_start.sh 23 | RUN sed -i 's/\r//' /local_django_start.sh 24 | RUN chmod +x /local_django_start.sh 25 | 26 | # this is the actual production start script 27 | COPY ./compose/production/django/start.sh /start.sh 28 | RUN sed -i 's/\r//' /start.sh 29 | RUN chmod +x /start.sh 30 | 31 | COPY ./compose/production/django/celery/worker/start.sh /start-celeryworker.sh 32 | RUN sed -i 's/\r//' /start-celeryworker.sh 33 | RUN chmod +x /start-celeryworker.sh 34 | 35 | COPY ./compose/production/django/celery/beat/start.sh /start-celerybeat.sh 36 | RUN sed -i 's/\r//' /start-celerybeat.sh 37 | RUN chmod +x /start-celerybeat.sh 38 | 39 | RUN wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | zsh || true 40 | COPY ./compose/.zshrc /root/.zshrc 41 | 42 | WORKDIR /app 43 | 44 | ENTRYPOINT ["/entrypoint.sh"] 45 | -------------------------------------------------------------------------------- /compose/production/django/aliases.txt: -------------------------------------------------------------------------------- 1 | alias dpy="python manage.py" 2 | alias testall="python manage.py test --keepdb --parallel" 3 | alias testallnoinput="python manage.py test --keepdb --parallel --no-input" 4 | -------------------------------------------------------------------------------- /compose/production/django/celery/beat/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | rm -f './celerybeat.pid' 7 | rm -f './celerybeat-schedule' 8 | 9 | celery -A config.celery_app beat -l INFO 10 | -------------------------------------------------------------------------------- /compose/production/django/celery/worker/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | celery -A config.celery_app worker -l INFO 7 | -------------------------------------------------------------------------------- /compose/production/django/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | # N.B. If only .env files supported variable expansion... 7 | export CELERY_BROKER_URL="${REDIS_URL}" 8 | 9 | 10 | if [ -z "${POSTGRES_USER}" ]; then 11 | base_postgres_image_default_user='postgres' 12 | export POSTGRES_USER="${base_postgres_image_default_user}" 13 | fi 14 | export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" 15 | 16 | postgres_ready() { 17 | python << END 18 | import sys 19 | 20 | import psycopg2 21 | 22 | try: 23 | psycopg2.connect( 24 | dbname="${POSTGRES_DB}", 25 | user="${POSTGRES_USER}", 26 | password="${POSTGRES_PASSWORD}", 27 | host="${POSTGRES_HOST}", 28 | port="${POSTGRES_PORT}", 29 | ) 30 | except psycopg2.OperationalError: 31 | sys.exit(-1) 32 | sys.exit(0) 33 | 34 | END 35 | } 36 | until postgres_ready; do 37 | >&2 echo 'Waiting for PostgreSQL to become available...' 38 | sleep 1 39 | done 40 | >&2 echo 'PostgreSQL is available' 41 | 42 | exec "$@" 43 | -------------------------------------------------------------------------------- /compose/production/django/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | 7 | python /app/manage.py collectstatic --noinput 8 | python manage.py migrate --no-input 9 | 10 | # default to 4 workers 11 | #/usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:5000 --chdir=/app --workers=4 12 | 13 | /usr/local/bin/daphne -b 0.0.0.0 -p 5000 config.asgi:application 14 | -------------------------------------------------------------------------------- /compose/production/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:11.3 2 | 3 | COPY ./compose/production/postgres/maintenance /usr/local/bin/maintenance 4 | RUN chmod +x /usr/local/bin/maintenance/* 5 | RUN mv /usr/local/bin/maintenance/* /usr/local/bin \ 6 | && rmdir /usr/local/bin/maintenance 7 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/_sourced/constants.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | BACKUP_DIR_PATH='/backups' 5 | BACKUP_FILE_PREFIX='backup' 6 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/_sourced/countdown.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | countdown() { 5 | declare desc="A simple countdown. Source: https://superuser.com/a/611582" 6 | local seconds="${1}" 7 | local d=$(($(date +%s) + "${seconds}")) 8 | while [ "$d" -ge `date +%s` ]; do 9 | echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r"; 10 | sleep 0.1 11 | done 12 | } 13 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/_sourced/messages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | message_newline() { 5 | echo 6 | } 7 | 8 | message_debug() 9 | { 10 | echo -e "DEBUG: ${@}" 11 | } 12 | 13 | message_welcome() 14 | { 15 | echo -e "\e[1m${@}\e[0m" 16 | } 17 | 18 | message_warning() 19 | { 20 | echo -e "\e[33mWARNING\e[0m: ${@}" 21 | } 22 | 23 | message_error() 24 | { 25 | echo -e "\e[31mERROR\e[0m: ${@}" 26 | } 27 | 28 | message_info() 29 | { 30 | echo -e "\e[37mINFO\e[0m: ${@}" 31 | } 32 | 33 | message_suggestion() 34 | { 35 | echo -e "\e[33mSUGGESTION\e[0m: ${@}" 36 | } 37 | 38 | message_success() 39 | { 40 | echo -e "\e[32mSUCCESS\e[0m: ${@}" 41 | } 42 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/_sourced/yes_no.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | yes_no() { 5 | declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message." 6 | local arg1="${1}" 7 | 8 | local response= 9 | read -r -p "${arg1} (y/[n])? " response 10 | if [[ "${response}" =~ ^[Yy]$ ]] 11 | then 12 | exit 0 13 | else 14 | exit 1 15 | fi 16 | } 17 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/backup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### Create a database backup. 5 | ### 6 | ### Usage: 7 | ### $ docker-compose -f .yml (exec |run --rm) postgres backup 8 | 9 | 10 | set -o errexit 11 | set -o pipefail 12 | set -o nounset 13 | 14 | 15 | working_dir="$(dirname ${0})" 16 | source "${working_dir}/_sourced/constants.sh" 17 | source "${working_dir}/_sourced/messages.sh" 18 | 19 | 20 | message_welcome "Backing up the '${POSTGRES_DB}' database..." 21 | 22 | 23 | if [[ "${POSTGRES_USER}" == "postgres" ]]; then 24 | message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." 25 | exit 1 26 | fi 27 | 28 | export PGHOST="${POSTGRES_HOST}" 29 | export PGPORT="${POSTGRES_PORT}" 30 | export PGUSER="${POSTGRES_USER}" 31 | export PGPASSWORD="${POSTGRES_PASSWORD}" 32 | export PGDATABASE="${POSTGRES_DB}" 33 | 34 | backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz" 35 | pg_dump | gzip > "${BACKUP_DIR_PATH}/${backup_filename}" 36 | 37 | 38 | message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'." 39 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/backups: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### View backups. 5 | ### 6 | ### Usage: 7 | ### $ docker-compose -f .yml (exec |run --rm) postgres backups 8 | 9 | 10 | set -o errexit 11 | set -o pipefail 12 | set -o nounset 13 | 14 | 15 | working_dir="$(dirname ${0})" 16 | source "${working_dir}/_sourced/constants.sh" 17 | source "${working_dir}/_sourced/messages.sh" 18 | 19 | 20 | message_welcome "These are the backups you have got:" 21 | 22 | ls -lht "${BACKUP_DIR_PATH}" 23 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/restore: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### Restore database from a backup. 5 | ### 6 | ### Parameters: 7 | ### <1> filename of an existing backup. 8 | ### 9 | ### Usage: 10 | ### $ docker-compose -f .yml (exec |run --rm) postgres restore <1> 11 | 12 | 13 | set -o errexit 14 | set -o pipefail 15 | set -o nounset 16 | 17 | 18 | working_dir="$(dirname ${0})" 19 | source "${working_dir}/_sourced/constants.sh" 20 | source "${working_dir}/_sourced/messages.sh" 21 | 22 | 23 | if [[ -z ${1+x} ]]; then 24 | message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again." 25 | exit 1 26 | fi 27 | backup_filename="${BACKUP_DIR_PATH}/${1}" 28 | if [[ ! -f "${backup_filename}" ]]; then 29 | message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again." 30 | exit 1 31 | fi 32 | 33 | message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..." 34 | 35 | if [[ "${POSTGRES_USER}" == "postgres" ]]; then 36 | message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." 37 | exit 1 38 | fi 39 | 40 | export PGHOST="${POSTGRES_HOST}" 41 | export PGPORT="${POSTGRES_PORT}" 42 | export PGUSER="${POSTGRES_USER}" 43 | export PGPASSWORD="${POSTGRES_PASSWORD}" 44 | export PGDATABASE="${POSTGRES_DB}" 45 | 46 | message_info "Dropping the database..." 47 | dropdb "${PGDATABASE}" 48 | 49 | message_info "Creating a new database..." 50 | createdb --owner="${POSTGRES_USER}" 51 | 52 | message_info "Applying the backup to the new database..." 53 | gunzip -c "${backup_filename}" | psql "${POSTGRES_DB}" 54 | 55 | message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup." 56 | -------------------------------------------------------------------------------- /compose/production/traefik/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM traefik:alpine 2 | COPY ./compose/production/traefik/traefik.toml /etc/traefik 3 | -------------------------------------------------------------------------------- /compose/production/traefik/traefik.toml: -------------------------------------------------------------------------------- 1 | logLevel = "INFO" 2 | # ssl termination happens on the GCP load balancer 3 | defaultEntryPoints = ["http"] 4 | 5 | [entryPoints] 6 | [entryPoints.http] 7 | address = ":80" 8 | 9 | [file] 10 | [backends] 11 | [backends.django] 12 | [backends.django.servers.server1] 13 | url = "http://django:5000" 14 | 15 | [frontends] 16 | [frontends.django] 17 | backend = "django" 18 | passHostHeader = true 19 | [frontends.django.headers] 20 | HostsProxyHeaders = ['X-CSRFToken'] 21 | [frontends.django.routes.dr1] 22 | rule = "Host:open.senrigan.io" 23 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | # This will make sure the app is always imported when 2 | # Django starts so that shared_task will use this app. 3 | from .celery_app import app as celery_app 4 | 5 | __all__ = ("celery_app",) 6 | -------------------------------------------------------------------------------- /config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI entrypoint. Configures Django and then runs the application 3 | defined in the ASGI_APPLICATION setting. 4 | """ 5 | import os 6 | import django 7 | import environ 8 | import sentry_sdk 9 | import logging 10 | 11 | from channels.routing import get_default_application 12 | 13 | from sentry_sdk.integrations.django import DjangoIntegration 14 | from sentry_sdk.integrations.aiohttp import AioHttpIntegration 15 | from sentry_sdk.integrations.asgi import SentryAsgiMiddleware 16 | from sentry_sdk.integrations.logging import LoggingIntegration 17 | 18 | env = environ.Env() 19 | SENTRY_DSN = env("SENTRY_DSN") 20 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 21 | SENTRY_LOG_LEVEL = env.int("DJANGO_SENTRY_LOG_LEVEL", logging.INFO) 22 | 23 | django.setup() 24 | 25 | sentry_logging = LoggingIntegration( 26 | level=SENTRY_LOG_LEVEL, # Capture info and above as breadcrumbs 27 | event_level=logging.ERROR, # Send no events from log messages 28 | ) 29 | 30 | sentry_sdk.init( 31 | dsn=SENTRY_DSN, 32 | integrations=[sentry_logging, DjangoIntegration(), AioHttpIntegration()], 33 | ) 34 | 35 | application = get_default_application() 36 | application = SentryAsgiMiddleware(application) 37 | -------------------------------------------------------------------------------- /config/celery_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from celery import Celery 3 | 4 | # set the default Django settings module for the 'celery' program. 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 6 | 7 | MINUTES_INTERVAL = 60 8 | 9 | app = Celery("open") 10 | 11 | app.conf.beat_schedule = { 12 | "check-all-services-running": { 13 | "task": "open.core.tasks.check_services_running", 14 | # run every 30 minutes to make sure all the ml-services are running 15 | "schedule": 30 * MINUTES_INTERVAL, 16 | }, 17 | "reset-betterself-demo-fixtures": { 18 | "task": "open.core.tasks.reset_betterself_demo_fixtures", 19 | "schedule": 30 * MINUTES_INTERVAL, 20 | }, 21 | } 22 | 23 | # Using a string here means the worker doesn't have to serialize 24 | # the configuration object to child processes. 25 | # - namespace='CELERY' means all celery-related configuration keys 26 | # should have a `CELERY_` prefix. 27 | app.config_from_object("django.conf:settings", namespace="CELERY") 28 | 29 | # Load task modules from all registered Django app configs. 30 | app.autodiscover_tasks() 31 | -------------------------------------------------------------------------------- /config/constants.py: -------------------------------------------------------------------------------- 1 | LOCAL = "local" 2 | PRODUCTION = "production" 3 | TESTING = "testing" 4 | -------------------------------------------------------------------------------- /config/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/config/settings/__init__.py -------------------------------------------------------------------------------- /config/settings/test.py: -------------------------------------------------------------------------------- 1 | from config.constants import TESTING 2 | from .base import * # noqa 3 | from .base import env 4 | 5 | ENVIRONMENT = TESTING 6 | 7 | print("TEST - SETTINGS") 8 | 9 | # GENERAL 10 | # ------------------------------------------------------------------------------ 11 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug 12 | DEBUG = False 13 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 14 | SECRET_KEY = env( 15 | "DJANGO_SECRET_KEY", 16 | default="O3jDTcehDkjazieUejh0FzLSysz2bDWGBPBZSwnMPPVSXphpTsxRqqAsB5kwdwpb", 17 | ) 18 | # https://docs.djangoproject.com/en/dev/ref/settings/#test-runner 19 | TEST_RUNNER = "django.test.runner.DiscoverRunner" 20 | 21 | # CACHES 22 | # ------------------------------------------------------------------------------ 23 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches 24 | CACHES = { 25 | "default": { 26 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 27 | "LOCATION": "", 28 | } 29 | } 30 | 31 | # PASSWORDS 32 | # ------------------------------------------------------------------------------ 33 | # https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers 34 | PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] 35 | 36 | # TEMPLATES 37 | # ------------------------------------------------------------------------------ 38 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates 39 | TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG # noqa F405 40 | TEMPLATES[0]["OPTIONS"]["loaders"] = [ # noqa F405 41 | ( 42 | "django.template.loaders.cached.Loader", 43 | [ 44 | "django.template.loaders.filesystem.Loader", 45 | "django.template.loaders.app_directories.Loader", 46 | ], 47 | ) 48 | ] 49 | 50 | # EMAIL 51 | # ------------------------------------------------------------------------------ 52 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 53 | EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 54 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-host 55 | EMAIL_HOST = "localhost" 56 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-port 57 | EMAIL_PORT = 1025 58 | 59 | CELERY_TASK_ALWAYS_EAGER = True 60 | 61 | REST_FRAMEWORK = { 62 | "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), 63 | "DEFAULT_THROTTLE_CLASSES": [ 64 | "rest_framework.throttling.AnonRateThrottle", 65 | "rest_framework.throttling.UserRateThrottle", 66 | "rest_framework.throttling.ScopedRateThrottle", 67 | ], 68 | # anything more than five a second feels quite excessive 69 | "DEFAULT_THROTTLE_RATES": { 70 | "anon": "500/second", 71 | "user": "500/second", 72 | "create_prompt_rate": "500/second", 73 | "list_prompt_rate": "500/second", 74 | }, 75 | "DEFAULT_AUTHENTICATION_CLASSES": [ 76 | "rest_framework.authentication.TokenAuthentication", 77 | "rest_framework.authentication.BasicAuthentication", 78 | "rest_framework.authentication.SessionAuthentication", 79 | ], 80 | } 81 | -------------------------------------------------------------------------------- /config/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.urls import include, path, re_path 5 | from django.views import defaults as default_views 6 | from django.views.generic import RedirectView 7 | 8 | from open.users.views import ( 9 | GitHubLogin, 10 | LoginNoCSRFAPIView, 11 | RegisterNoCSRFAPIView, 12 | ) 13 | 14 | # need a special view to make sure favicon always works 15 | favicon_view = RedirectView.as_view( 16 | url="/static/images/favicons/gatsby-icon.png", permanent=True 17 | ) 18 | 19 | 20 | def trigger_error(request): 21 | division_by_zero = 1 / 0 22 | return division_by_zero 23 | 24 | 25 | urlpatterns = [ 26 | path("sentry-debug/", trigger_error), 27 | path( 28 | "", 29 | default_views.permission_denied, 30 | kwargs={"exception": Exception("Shame On You")}, 31 | name="unnamed", 32 | ), 33 | path("rest-auth/login/", LoginNoCSRFAPIView.as_view(), name="rest_login"), 34 | # TODO - reconsider how registration should work with writeup.ai 35 | path( 36 | "rest-auth/registration/", 37 | RegisterNoCSRFAPIView.as_view(), 38 | name="rest_registration", 39 | ), 40 | re_path( 41 | r"^password-reset/confirm/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$", 42 | RedirectView.as_view( 43 | url=f"{settings.BETTERSELF_APP_URL}/password_reset/%(uidb64)s/%(token)s/" 44 | ), 45 | name="password_reset_confirm", 46 | ), 47 | path("rest-auth/", include("rest_auth.urls")), 48 | path("rest-auth/registration/", include("rest_auth.registration.urls")), 49 | path("rest-auth/github/", GitHubLogin.as_view(), name="github_login"), 50 | path("favicon.ico", favicon_view, name="favicon"), 51 | path(settings.ADMIN_URL, admin.site.urls), 52 | # prefix an api endpoint in front of everything to use a global load balancer 53 | # and route traffic based on API variants. this is to offload websocket servers 54 | # and traditional REST servers 55 | path("api/writeup/v1/", include("open.core.writeup.urls")), 56 | path("api/betterself/v2/", include("open.core.betterself.urls")), 57 | path("users/", include("open.users.urls")), 58 | ] 59 | 60 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 61 | 62 | if settings.DEBUG: 63 | if "debug_toolbar" in settings.INSTALLED_APPS: 64 | import debug_toolbar 65 | 66 | urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns 67 | -------------------------------------------------------------------------------- /config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for open project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | import sys 18 | 19 | from django.core.wsgi import get_wsgi_application 20 | 21 | # This allows easy placement of apps within the interior 22 | # open directory. 23 | app_path = os.path.abspath( 24 | os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) 25 | ) 26 | sys.path.append(os.path.join(app_path, "open")) 27 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 28 | # if running multiple sites in the same mod_wsgi process. To fix this, use 29 | # mod_wsgi daemon mode with each site in its own daemon process, or use 30 | # os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production" 31 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") 32 | 33 | # This application object is used by any WSGI server configured to use this 34 | # file. This includes Django's development server, if the WSGI_APPLICATION 35 | # setting points here. 36 | application = get_wsgi_application() 37 | 38 | # Apply WSGI middleware here. 39 | # from helloworld.wsgi import HelloWorldApplication 40 | # application = HelloWorldApplication(application) 41 | -------------------------------------------------------------------------------- /local.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | volumes: 4 | local_postgres_data: {} 5 | production_traefik: {} 6 | 7 | services: 8 | django: &django 9 | build: 10 | context: . 11 | dockerfile: ./compose/production/django/Dockerfile 12 | image: gcr.io/proven-record-242223/open:latest 13 | # watch out for sequential failures, postgres will quickly lock up 14 | # restart: on-failure 15 | # restart: no 16 | depends_on: 17 | - postgres 18 | volumes: 19 | - .:/app 20 | env_file: 21 | - ./.envs/.local/.django 22 | environment: 23 | WEB_APP: host.docker.internal 24 | ports: 25 | - "0.0.0.0:5000:5000" 26 | command: /local_django_start.sh 27 | 28 | postgres: 29 | build: 30 | context: . 31 | dockerfile: ./compose/production/postgres/Dockerfile 32 | image: open_production_postgres 33 | # always keep postgres on, it's easier that way 34 | restart: always 35 | volumes: 36 | - local_postgres_data:/var/lib/postgresql/data 37 | - .:/app 38 | env_file: 39 | - ./.envs/.local/.django 40 | ports: 41 | # 5432 is freq taken by some other instance running postgres 42 | # i have too many side projects ... so i kind of have to use a random port 43 | - "25432:5432" 44 | 45 | redis: 46 | image: redis:5.0 47 | 48 | # traefik: 49 | # build: 50 | # context: . 51 | # dockerfile: ./compose/production/traefik/Dockerfile 52 | # depends_on: 53 | # - django 54 | # ports: 55 | # - "0.0.0.0:80:80" 56 | 57 | # celeryworker: 58 | # <<: *django 59 | # image: open_local_celeryworker 60 | # depends_on: 61 | # - redis 62 | # - postgres 63 | # - mailhog 64 | # ports: [] 65 | # command: /start-celeryworker.sh 66 | 67 | # celeryworker: 68 | # image: gcr.io/proven-record-242223/open:latest 69 | # depends_on: 70 | # - redis 71 | # - postgres 72 | # env_file: 73 | # - ./.envs/.local/.django 74 | # volumes: 75 | # - .:/app 76 | # command: /start-celeryworker.sh 77 | # 78 | # celerybeat: 79 | # image: gcr.io/proven-record-242223/open:latest 80 | # depends_on: 81 | # - redis 82 | # - postgres 83 | # env_file: 84 | # - ./.envs/.local/.django 85 | # volumes: 86 | # - .:/app 87 | # command: /start-celerybeat.sh 88 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | import logging 5 | 6 | if __name__ == "__main__": 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 8 | 9 | if "test" in sys.argv: 10 | logging.disable(logging.CRITICAL) 11 | os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.test" 12 | 13 | try: 14 | from django.core.management import execute_from_command_line 15 | except ImportError: 16 | # The above import may fail for some other reason. Ensure that the 17 | # issue is really that Django is missing to avoid masking other 18 | # exceptions on Python 2. 19 | try: 20 | import django # noqa 21 | except ImportError: 22 | raise ImportError( 23 | "Couldn't import Django. Are you sure it's installed and " 24 | "available on your PYTHONPATH environment variable? Did you " 25 | "forget to activate a virtual environment?" 26 | ) 27 | 28 | raise 29 | 30 | # This allows easy placement of apps within the interior 31 | # open directory. 32 | current_path = os.path.dirname(os.path.abspath(__file__)) 33 | sys.path.append(os.path.join(current_path, "open")) 34 | 35 | execute_from_command_line(sys.argv) 36 | -------------------------------------------------------------------------------- /open/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | __version_info__ = tuple( 3 | [ 4 | int(num) if num.isdigit() else num 5 | for num in __version__.replace("-", ".", 1).split(".") 6 | ] 7 | ) 8 | -------------------------------------------------------------------------------- /open/constants.py: -------------------------------------------------------------------------------- 1 | NO_ANSWER_CHOICE_TUPLE = ("", "N/A") 2 | # Used to determine where a user signed up from 3 | WRITEUP_APPLICATION_NAME = "WriteUp" 4 | BETTERSELF_APPLICATION_NAME = "BetterSelf" 5 | 6 | WRITEUP_APPLICATION_KEY = "writeup" 7 | BETTERSELF_APPLICATION_KEY = "betterself" 8 | 9 | 10 | SIGNED_UP_FROM_DETAILS_CHOICE = ( 11 | NO_ANSWER_CHOICE_TUPLE, 12 | (WRITEUP_APPLICATION_KEY, WRITEUP_APPLICATION_NAME), 13 | (BETTERSELF_APPLICATION_KEY, BETTERSELF_APPLICATION_NAME), 14 | ) 15 | -------------------------------------------------------------------------------- /open/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /open/contrib/sites/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /open/contrib/sites/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import django.contrib.sites.models 2 | from django.contrib.sites.models import _simple_domain_name_validator 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name="Site", 13 | fields=[ 14 | ( 15 | "id", 16 | models.AutoField( 17 | verbose_name="ID", 18 | serialize=False, 19 | auto_created=True, 20 | primary_key=True, 21 | ), 22 | ), 23 | ( 24 | "domain", 25 | models.CharField( 26 | max_length=100, 27 | verbose_name="domain name", 28 | validators=[_simple_domain_name_validator], 29 | ), 30 | ), 31 | ("name", models.CharField(max_length=50, verbose_name="display name")), 32 | ], 33 | options={ 34 | "ordering": ("domain",), 35 | "db_table": "django_site", 36 | "verbose_name": "site", 37 | "verbose_name_plural": "sites", 38 | }, 39 | bases=(models.Model,), 40 | managers=[("objects", django.contrib.sites.models.SiteManager())], 41 | ) 42 | ] 43 | -------------------------------------------------------------------------------- /open/contrib/sites/migrations/0002_alter_domain_unique.py: -------------------------------------------------------------------------------- 1 | import django.contrib.sites.models 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("sites", "0001_initial")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="site", 12 | name="domain", 13 | field=models.CharField( 14 | max_length=100, 15 | unique=True, 16 | validators=[django.contrib.sites.models._simple_domain_name_validator], 17 | verbose_name="domain name", 18 | ), 19 | ) 20 | ] 21 | -------------------------------------------------------------------------------- /open/contrib/sites/migrations/0003_set_site_domain_and_name.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | from django.conf import settings 7 | from django.db import migrations 8 | 9 | 10 | def update_site_forward(apps, schema_editor): 11 | """Set site domain and name.""" 12 | Site = apps.get_model("sites", "Site") 13 | Site.objects.update_or_create( 14 | id=settings.SITE_ID, 15 | defaults={ 16 | "domain": "open.senrigan.io", 17 | "name": "open", 18 | }, 19 | ) 20 | 21 | 22 | def update_site_backward(apps, schema_editor): 23 | """Revert site domain and name to default.""" 24 | Site = apps.get_model("sites", "Site") 25 | Site.objects.update_or_create( 26 | id=settings.SITE_ID, defaults={"domain": "example.com", "name": "example.com"} 27 | ) 28 | 29 | 30 | class Migration(migrations.Migration): 31 | 32 | dependencies = [("sites", "0002_alter_domain_unique")] 33 | 34 | operations = [migrations.RunPython(update_site_forward, update_site_backward)] 35 | -------------------------------------------------------------------------------- /open/contrib/sites/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /open/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/__init__.py -------------------------------------------------------------------------------- /open/core/admin.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import open.core.writeup.admin 3 | import open.core.betterself.admin 4 | -------------------------------------------------------------------------------- /open/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = "open.core" 6 | -------------------------------------------------------------------------------- /open/core/betterself/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/betterself/__init__.py -------------------------------------------------------------------------------- /open/core/betterself/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/betterself/fixtures/__init__.py -------------------------------------------------------------------------------- /open/core/betterself/models/__init__.py: -------------------------------------------------------------------------------- 1 | from open.utilities.importing_models import import_submodules 2 | 3 | __all__ = import_submodules(__name__) 4 | -------------------------------------------------------------------------------- /open/core/betterself/models/activity.py: -------------------------------------------------------------------------------- 1 | from django.db.models import CharField, BooleanField 2 | 3 | from open.core.betterself.constants import BetterSelfResourceConstants 4 | from open.utilities.models import BaseModelWithUserGeneratedContent 5 | 6 | 7 | class Activity(BaseModelWithUserGeneratedContent): 8 | """ 9 | Users will probably put stuff like "Ate Breakfast", but ideally I want something that can 10 | support an Activity like "Morning Routine" would consists of multiple ActivityActions 11 | 12 | This is why it's set as foreign key from ActivityEvent, I don't want to overengineer and build 13 | the entire foreign key relationships, but I also don't want to build a crappy hole that I have to dig out of. 14 | """ 15 | 16 | RESOURCE_NAME = BetterSelfResourceConstants.ACTIVITIES 17 | 18 | name = CharField(max_length=300) 19 | # Was this significant? IE. Got married? Had a Kid? (Congrats!) Had surgery? New Job? Decided to quit smoking? 20 | # Mark significant events that might change all future events. 21 | # Eventually used in charts as "markers/signals" in a chart to show 22 | # IE. Once you decided to quit smoking --- > This is your heart rate. 23 | is_significant_activity = BooleanField(default=False) 24 | # Is this an user_activity you hate / want to avoid? 25 | # Are there certain patterns (sleep, diet, supplements, other activities) that lead to negative activities? 26 | # IE - Limited sleep impact decision making (probably). 27 | # Can we figure out if there are certain things you do ahead that limit sleep? 28 | # Can we figure out if there are certain behaviors you can avoid so this doesn't happen? 29 | # Are there certain foods that will likely cause a negative user_activity? 30 | # Personally - Eating foods with lots of preservatives causes depression/flu like symptoms that last for 1-2 days 31 | 32 | # i regret naming _activity at the boolean fields ... 33 | is_negative_activity = BooleanField(default=False) 34 | # I find certain events are complete days, ie. Being sick with an impacted wisdom tooth was the worst. 35 | is_all_day_activity = BooleanField(default=False) 36 | 37 | class Meta: 38 | unique_together = (("name", "user"),) 39 | ordering = ["name"] 40 | verbose_name_plural = "Activities" 41 | -------------------------------------------------------------------------------- /open/core/betterself/models/activity_log.py: -------------------------------------------------------------------------------- 1 | from django.db.models import ( 2 | CharField, 3 | ForeignKey, 4 | IntegerField, 5 | DateTimeField, 6 | CASCADE, 7 | ) 8 | 9 | from open.core.betterself.constants import ( 10 | INPUT_SOURCES_TUPLES, 11 | WEB_INPUT_SOURCE, 12 | BetterSelfResourceConstants, 13 | ) 14 | from open.core.betterself.models.activity import Activity 15 | from open.utilities.models import BaseModelWithUserGeneratedContent 16 | 17 | 18 | class ActivityLog(BaseModelWithUserGeneratedContent): 19 | """ 20 | Represents any particular type of event a user may have done 21 | - ie. Meditation, running, take dog the park, etc. 22 | 23 | This doesn't really get to the crux of how do you record a state of mind that's 24 | frustrating like depression/flu (both of which share oddly similar mental states), 25 | which if this is one thing BetterSelf cures for you, then it's a success. 26 | 27 | I just haven't figured the most appropriate way to model / store such information. 28 | """ 29 | 30 | RESOURCE_NAME = BetterSelfResourceConstants.ACTIVITY_LOGS 31 | 32 | activity = ForeignKey(Activity, on_delete=CASCADE) 33 | source = CharField( 34 | max_length=50, choices=INPUT_SOURCES_TUPLES, default=WEB_INPUT_SOURCE 35 | ) 36 | duration_minutes = IntegerField(blank=True, null=True) 37 | time = DateTimeField() 38 | 39 | class Meta: 40 | unique_together = (("time", "user", "activity"),) 41 | # ordering = ["user", "-time"] 42 | ordering = ["-time"] 43 | verbose_name = "Activity Log" 44 | verbose_name_plural = "Activity Logs" 45 | 46 | def __str__(self): 47 | return "{} {}".format(self.activity, self.time) 48 | -------------------------------------------------------------------------------- /open/core/betterself/models/daily_productivity_log.py: -------------------------------------------------------------------------------- 1 | from django.db.models import CharField, DateField, PositiveIntegerField, TextField 2 | 3 | from open.core.betterself.constants import ( 4 | INPUT_SOURCES_TUPLES, 5 | BetterSelfResourceConstants, 6 | ) 7 | from open.utilities.models import BaseModelWithUserGeneratedContent 8 | 9 | 10 | class DailyProductivityLog(BaseModelWithUserGeneratedContent): 11 | """ 12 | Represents the daily over-view of how productive a user was on that day, mimics 13 | RescueTime's concept of productive time, mildly productive, etc. 14 | """ 15 | 16 | RESOURCE_NAME = BetterSelfResourceConstants.DAILY_PRODUCTIVITY_LOGS 17 | # a list of all the fields that can be selected as key productivity drivers 18 | 19 | source = CharField(max_length=50, choices=INPUT_SOURCES_TUPLES) 20 | date = DateField() 21 | 22 | very_productive_time_minutes = PositiveIntegerField(null=True, blank=True) 23 | productive_time_minutes = PositiveIntegerField(null=True, blank=True) 24 | neutral_time_minutes = PositiveIntegerField(null=True, blank=True) 25 | distracting_time_minutes = PositiveIntegerField(null=True, blank=True) 26 | very_distracting_time_minutes = PositiveIntegerField(null=True, blank=True) 27 | pomodoro_count = PositiveIntegerField(null=True, blank=True) 28 | 29 | # i like to separate writing mistakes to a separate field for easier parsing 30 | mistakes = TextField(default="", blank=True) 31 | 32 | class Meta: 33 | verbose_name = "Daily Productivity Log" 34 | verbose_name_plural = "Daily Productivity Logs" 35 | unique_together = (("date", "user"),) 36 | ordering = ["-date"] 37 | 38 | def __str__(self): 39 | return "{} Productivity Log".format(self.date) 40 | 41 | def __repr__(self): 42 | return "{} Productivity Log".format(self.date) 43 | -------------------------------------------------------------------------------- /open/core/betterself/models/food.py: -------------------------------------------------------------------------------- 1 | from django.db.models import CharField, DecimalField, BooleanField 2 | 3 | from open.core.betterself.constants import BetterSelfResourceConstants 4 | from open.utilities.models import BaseModelWithUserGeneratedContent 5 | 6 | 7 | class Food(BaseModelWithUserGeneratedContent): 8 | """ 9 | Chips, candy and steak. 10 | """ 11 | 12 | RESOURCE_NAME = BetterSelfResourceConstants.FOODS 13 | 14 | name = CharField(max_length=300) 15 | calories = DecimalField(max_digits=10, decimal_places=2, null=True) 16 | is_liquid = BooleanField(default=False) 17 | 18 | class Meta: 19 | unique_together = ("user", "name") 20 | ordering = ["user", "name"] 21 | verbose_name = "Food" 22 | verbose_name_plural = "Foods" 23 | -------------------------------------------------------------------------------- /open/core/betterself/models/food_logs.py: -------------------------------------------------------------------------------- 1 | from django.db.models import ( 2 | ForeignKey, 3 | DecimalField, 4 | CASCADE, 5 | CharField, 6 | DateTimeField, 7 | ) 8 | 9 | from open.core.betterself.constants import ( 10 | WEB_INPUT_SOURCE, 11 | INPUT_SOURCES_TUPLES, 12 | BetterSelfResourceConstants, 13 | ) 14 | from open.core.betterself.models.food import Food 15 | from open.utilities.models import BaseModelWithUserGeneratedContent 16 | 17 | 18 | class FoodLog(BaseModelWithUserGeneratedContent): 19 | RESOURCE_NAME = BetterSelfResourceConstants.FOOD_LOGS 20 | 21 | food = ForeignKey(Food, on_delete=CASCADE) 22 | source = CharField( 23 | max_length=50, choices=INPUT_SOURCES_TUPLES, default=WEB_INPUT_SOURCE 24 | ) 25 | quantity = DecimalField(max_digits=10, decimal_places=2, default=1) 26 | time = DateTimeField() 27 | 28 | class Meta: 29 | unique_together = ("user", "time", "food") 30 | ordering = ["user", "-time"] 31 | verbose_name = "Food Log" 32 | verbose_name_plural = "Food Logs" 33 | 34 | def __str__(self): 35 | formatted_time = self.time.strftime("%Y-%m-%d %I:%M%p") 36 | return f"{self.quantity:.0f} {self.food.name} {formatted_time} from {self.source} event" 37 | -------------------------------------------------------------------------------- /open/core/betterself/models/ingredient.py: -------------------------------------------------------------------------------- 1 | from django.db.models import PositiveIntegerField, CharField 2 | 3 | from open.core.betterself.constants import BetterSelfResourceConstants 4 | from open.utilities.models import BaseModelWithUserGeneratedContent 5 | 6 | 7 | class Ingredient(BaseModelWithUserGeneratedContent): 8 | RESOURCE_NAME = BetterSelfResourceConstants.INGREDIENTS 9 | 10 | # if some ingredient is longer than 300 characters, prob shouldn't take it. 11 | # if anyone ever reads up reading this, 1,3 dimethylamylamine is probably a great 12 | # example of if you can't pronounce it, don't take it. 13 | name = CharField(max_length=255, default="", blank=False, null=False) 14 | # name = TextField(default="", blank=False, null=False) 15 | # this is going to be a hard thing to source / scrap, but you do care about this, leave blank 16 | # but don't let default be zero. 17 | half_life_minutes = PositiveIntegerField(null=True, blank=True) 18 | 19 | def __str__(self): 20 | return f"Ingredient | {self.name} - {self.user}" 21 | 22 | class Meta: 23 | unique_together = ("name", "user") 24 | -------------------------------------------------------------------------------- /open/core/betterself/models/ingredient_composition.py: -------------------------------------------------------------------------------- 1 | from django.db.models import ForeignKey, DecimalField, CASCADE 2 | 3 | from open.core.betterself.constants import BetterSelfResourceConstants 4 | from open.core.betterself.models.ingredient import Ingredient 5 | from open.core.betterself.models.measurement import Measurement 6 | from open.utilities.models import BaseModelWithUserGeneratedContent 7 | 8 | 9 | class IngredientComposition(BaseModelWithUserGeneratedContent): 10 | """ Creatine, 5, grams """ 11 | 12 | RESOURCE_NAME = BetterSelfResourceConstants.INGREDIENT_COMPOSITIONS 13 | 14 | ingredient = ForeignKey(Ingredient, on_delete=CASCADE) 15 | # users should fill this in if they want to use a composition ... otherwise, they could 16 | # just leave it at supplement 17 | measurement = ForeignKey(Measurement, null=False, blank=False, on_delete=CASCADE) 18 | quantity = DecimalField(max_digits=10, decimal_places=4, null=False, blank=True) 19 | 20 | class Meta: 21 | unique_together = ("user", "ingredient", "measurement", "quantity") 22 | ordering = ["user", "ingredient__name"] 23 | verbose_name = "Ingredient Composition" 24 | verbose_name_plural = "Ingredient Compositions" 25 | 26 | def __str__(self): 27 | return f"IngredientComposition | {self.ingredient.name} {self.quantity} {self.measurement.name}" 28 | -------------------------------------------------------------------------------- /open/core/betterself/models/measurement.py: -------------------------------------------------------------------------------- 1 | from django.db.models import BooleanField, CharField 2 | 3 | from open.utilities.models import BaseModel 4 | 5 | 6 | class Measurement(BaseModel): 7 | name = CharField(max_length=100, unique=True) # 'milligram' 8 | short_name = CharField(max_length=100, default="", blank=True) # 'ml' 9 | is_liquid = BooleanField(default=False) 10 | 11 | def __str__(self): 12 | return f"{self.name}" 13 | -------------------------------------------------------------------------------- /open/core/betterself/models/sleep_log.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.db.models import CharField, DateTimeField 3 | 4 | from open.core.betterself.constants import ( 5 | INPUT_SOURCES_TUPLES, 6 | BetterSelfResourceConstants, 7 | ) 8 | from open.utilities.date_and_time import ( 9 | format_datetime_to_human_readable, 10 | convert_timedelta_to_minutes, 11 | ) 12 | from open.utilities.models import BaseModelWithUserGeneratedContent 13 | 14 | 15 | class SleepLog(BaseModelWithUserGeneratedContent): 16 | """ 17 | Records per each time a person falls asleep that combined across 24 hours is a way to see how much sleep 18 | a person gets. 19 | """ 20 | 21 | RESOURCE_NAME = BetterSelfResourceConstants.SLEEP_LOGS 22 | 23 | source = CharField(max_length=50, choices=INPUT_SOURCES_TUPLES) 24 | start_time = DateTimeField() 25 | end_time = DateTimeField() 26 | 27 | class Meta: 28 | verbose_name = "Sleep Log" 29 | verbose_name_plural = "Sleep Logs" 30 | ordering = ["user", "-end_time"] 31 | 32 | def __str__(self): 33 | return f"{self.user_id} {format_datetime_to_human_readable(self.start_time)} {format_datetime_to_human_readable(self.end_time)}" 34 | 35 | def save(self, *args, **kwargs): 36 | # now thinking about this a little bit more ... not sure if this really matters. if the user puts wrong 37 | # information in why should one try to fix it? 38 | if self.end_time <= self.start_time: 39 | raise ValidationError("End Time must be greater than Start Time") 40 | 41 | # make sure that there are no overlaps for activities 42 | # https://stackoverflow.com/questions/325933/determine-whether-two-date-ranges-overlap 43 | # thinking about this a little - sort of wonder, shouldn't i just allow this to let people try out multiple devices 44 | # like a fitbit watch and an apple sleep? 45 | # queryset = SleepLog.objects.filter( 46 | # user=self.user, end_time__gte=self.start_time, start_time__lte=self.end_time 47 | # ) 48 | # if queryset.exists(): 49 | # duplicated = queryset.first() 50 | # 51 | # raise ValidationError( 52 | # f"Overlapping Periods found when saving Sleep Activity! Found {duplicated.start_time} {duplicated.end_time}" 53 | # ) 54 | 55 | super().save(*args, **kwargs) 56 | 57 | @property 58 | def duration(self): 59 | return self.end_time - self.start_time 60 | 61 | @property 62 | def duration_minutes(self): 63 | minutes = convert_timedelta_to_minutes(self.duration) 64 | return minutes 65 | 66 | @property 67 | def duration_hours(self): 68 | minutes = convert_timedelta_to_minutes(self.duration) 69 | hours = minutes / 60 70 | return hours 71 | -------------------------------------------------------------------------------- /open/core/betterself/models/supplement.py: -------------------------------------------------------------------------------- 1 | from django.db.models import CharField, ManyToManyField, BooleanField 2 | 3 | from open.core.betterself.constants import BetterSelfResourceConstants 4 | from open.core.betterself.models.ingredient_composition import IngredientComposition 5 | from open.utilities.models import BaseModelWithUserGeneratedContent 6 | 7 | 8 | class Supplement(BaseModelWithUserGeneratedContent): 9 | """ 10 | Could be a stack like BCAA (which would have 4 ingredient comps) 11 | Or could just be something simple like Caffeine. 12 | """ 13 | 14 | RESOURCE_NAME = BetterSelfResourceConstants.SUPPLEMENTS 15 | 16 | name = CharField(max_length=300) 17 | ingredient_compositions = ManyToManyField(IngredientComposition, blank=True) 18 | is_taken_with_food = BooleanField(default=None, blank=True, null=True) 19 | 20 | class Meta: 21 | unique_together = ("user", "name") 22 | ordering = ["user", "name"] 23 | verbose_name = "Supplement" 24 | verbose_name_plural = "Supplements" 25 | -------------------------------------------------------------------------------- /open/core/betterself/models/supplement_log.py: -------------------------------------------------------------------------------- 1 | from django.db.models import ( 2 | ForeignKey, 3 | DecimalField, 4 | CASCADE, 5 | CharField, 6 | DateTimeField, 7 | PositiveIntegerField, 8 | ) 9 | 10 | from open.core.betterself.constants import ( 11 | WEB_INPUT_SOURCE, 12 | INPUT_SOURCES_TUPLES, 13 | BetterSelfResourceConstants, 14 | ) 15 | from open.core.betterself.models.supplement import Supplement 16 | from open.utilities.models import BaseModelWithUserGeneratedContent 17 | 18 | 19 | class SupplementLog(BaseModelWithUserGeneratedContent): 20 | RESOURCE_NAME = BetterSelfResourceConstants.SUPPLEMENT_LOGS 21 | 22 | supplement = ForeignKey(Supplement, on_delete=CASCADE) 23 | source = CharField( 24 | max_length=50, choices=INPUT_SOURCES_TUPLES, default=WEB_INPUT_SOURCE 25 | ) 26 | quantity = DecimalField(max_digits=10, decimal_places=2) 27 | # what time did the user take the five hour energy? use the time model 28 | # so eventually (maybe never) can do half-life analysis 29 | time = DateTimeField() 30 | duration_minutes = PositiveIntegerField(null=True, blank=True) 31 | 32 | class Meta: 33 | unique_together = ("user", "time", "supplement") 34 | # ordering = ["user", "-time"] 35 | ordering = ["-time"] 36 | verbose_name = "Supplement Log" 37 | verbose_name_plural = "Supplement Logs" 38 | 39 | def __str__(self): 40 | formatted_time = self.time.strftime("%Y-%m-%d %I:%M%p") 41 | return f"{self.quantity:.0f} {self.supplement.name} {formatted_time} from {self.source} event" 42 | -------------------------------------------------------------------------------- /open/core/betterself/models/supplement_stack.py: -------------------------------------------------------------------------------- 1 | from open.core.betterself.constants import BetterSelfResourceConstants 2 | from open.utilities.fields import DEFAULT_MODELS_CHAR_FIELD 3 | from open.utilities.models import BaseModelWithUserGeneratedContent 4 | 5 | 6 | class SupplementStack(BaseModelWithUserGeneratedContent): 7 | RESOURCE_NAME = BetterSelfResourceConstants.SUPPLEMENT_STACKS 8 | 9 | name = DEFAULT_MODELS_CHAR_FIELD 10 | 11 | class Meta: 12 | unique_together = ("user", "name") 13 | ordering = ["user", "name"] 14 | verbose_name = "Supplements Stack" 15 | verbose_name_plural = "Supplements Stacks" 16 | 17 | def __str__(self): 18 | return "{} Stack".format(self.name) 19 | 20 | @property 21 | def description(self): 22 | compositions = self.compositions.all() 23 | descriptions = [composition.description for composition in compositions] 24 | 25 | if descriptions: 26 | return ", ".join(descriptions) 27 | else: 28 | return "" 29 | -------------------------------------------------------------------------------- /open/core/betterself/models/supplement_stack_composition.py: -------------------------------------------------------------------------------- 1 | from django.db.models import DecimalField, ForeignKey, CASCADE 2 | 3 | from open.core.betterself.constants import BetterSelfResourceConstants 4 | from open.core.betterself.models.supplement import Supplement 5 | from open.core.betterself.models.supplement_stack import SupplementStack 6 | from open.utilities.models import BaseModelWithUserGeneratedContent 7 | 8 | 9 | class SupplementStackComposition(BaseModelWithUserGeneratedContent): 10 | RESOURCE_NAME = BetterSelfResourceConstants.SUPPLEMENT_STACK_COMPOSITIONS 11 | 12 | supplement = ForeignKey(Supplement, on_delete=CASCADE) 13 | stack = ForeignKey(SupplementStack, related_name="compositions", on_delete=CASCADE) 14 | # by default, don't allow this to be blank, it doesn't make sense for a supplement stack 15 | quantity = DecimalField( 16 | max_digits=10, decimal_places=4, null=False, blank=False, default=1 17 | ) 18 | 19 | class Meta: 20 | unique_together = ("user", "supplement", "stack") 21 | verbose_name = "Supplement Stack Composition" 22 | 23 | def __str__(self): 24 | return "{}-{}".format(self.stack_id, self.supplement_id) 25 | 26 | @property 27 | def description(self): 28 | return "{quantity} {supplement}".format( 29 | quantity=self.quantity, supplement=self.supplement 30 | ) 31 | -------------------------------------------------------------------------------- /open/core/betterself/models/well_being_log.py: -------------------------------------------------------------------------------- 1 | from django.db.models import ( 2 | CharField, 3 | DateTimeField, 4 | PositiveSmallIntegerField, 5 | ) 6 | 7 | from open.core.betterself.constants import ( 8 | INPUT_SOURCES_TUPLES, 9 | WEB_INPUT_SOURCE, 10 | BetterSelfResourceConstants, 11 | ) 12 | from open.utilities.date_and_time import get_utc_now 13 | from open.utilities.models import BaseModelWithUserGeneratedContent 14 | 15 | 16 | class WellBeingLog(BaseModelWithUserGeneratedContent): 17 | """ 18 | Meant to capture both physical and mental well-being 19 | 20 | IE. Someone could be happy and tired, or sad and strong(?) less-likely 21 | 22 | Capture enough data to be helpful to diagnose chronic 23 | """ 24 | 25 | RESOURCE_NAME = BetterSelfResourceConstants.WELL_BEING_LOGS 26 | 27 | time = DateTimeField(default=get_utc_now) 28 | 29 | # differentiate between feeling how a person may feel mentally versus physically 30 | # do as a score of 1-10 31 | mental_value = PositiveSmallIntegerField(null=True, blank=True) 32 | physical_value = PositiveSmallIntegerField(null=True, blank=True) 33 | 34 | source = CharField( 35 | max_length=50, choices=INPUT_SOURCES_TUPLES, default=WEB_INPUT_SOURCE 36 | ) 37 | 38 | class Meta: 39 | unique_together = ("user", "time") 40 | ordering = ["user", "-time"] 41 | verbose_name = "Well Being Log" 42 | verbose_name_plural = "Well Being Logs" 43 | 44 | def __str__(self): 45 | return "User - {}, Mood - {} at {}".format(self.user, self.value, self.time) 46 | -------------------------------------------------------------------------------- /open/core/betterself/serializers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/betterself/serializers/__init__.py -------------------------------------------------------------------------------- /open/core/betterself/serializers/activity_log_serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.exceptions import ValidationError 2 | from rest_framework.fields import UUIDField 3 | 4 | from open.core.betterself.models.activity import Activity 5 | from open.core.betterself.models.activity_log import ActivityLog 6 | from open.core.betterself.serializers.mixins import ( 7 | BaseCreateUpdateSerializer, 8 | BaseModelReadSerializer, 9 | ) 10 | from open.core.betterself.serializers.simple_generic_serializer import ( 11 | create_name_uuid_serializer, 12 | ) 13 | from open.core.betterself.serializers.validators import ModelValidatorsMixin 14 | 15 | 16 | class ActivityLogReadSerializer(BaseModelReadSerializer): 17 | activity = create_name_uuid_serializer(Activity) 18 | 19 | class Meta: 20 | model = ActivityLog 21 | fields = ( 22 | "uuid", 23 | "activity", 24 | "source", 25 | "duration_minutes", 26 | "time", 27 | "notes", 28 | # "created", 29 | # "modified", 30 | "display_name", 31 | ) 32 | 33 | 34 | class ActivityLogCreateUpdateSerializer( 35 | BaseCreateUpdateSerializer, ModelValidatorsMixin 36 | ): 37 | activity_uuid = UUIDField(source="activity.uuid") 38 | 39 | class Meta: 40 | model = ActivityLog 41 | fields = ( 42 | "activity_uuid", 43 | "source", 44 | "duration_minutes", 45 | "time", 46 | "notes", 47 | "user", 48 | ) 49 | 50 | def validate(self, validated_data): 51 | user = self.context["request"].user 52 | is_creating_instance = not self.instance 53 | 54 | if validated_data.get("activity"): 55 | activity_uuid = validated_data.pop("activity")["uuid"] 56 | activity = Activity.objects.get(uuid=activity_uuid, user=user) 57 | validated_data["activity"] = activity 58 | 59 | if is_creating_instance: 60 | if self.Meta.model.objects.filter( 61 | user=user, activity=activity, time=validated_data["time"], 62 | ).exists(): 63 | raise ValidationError( 64 | f"Fields user, ingredient, measurement, and quantity are not unique!" 65 | ) 66 | 67 | return validated_data 68 | -------------------------------------------------------------------------------- /open/core/betterself/serializers/activity_serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.exceptions import ValidationError 2 | 3 | from open.core.betterself.models.activity import Activity 4 | from open.core.betterself.serializers.mixins import ( 5 | BaseCreateUpdateSerializer, 6 | BaseModelReadSerializer, 7 | ) 8 | 9 | 10 | class ActivityReadSerializer(BaseModelReadSerializer): 11 | class Meta: 12 | model = Activity 13 | fields = ( 14 | "uuid", 15 | "name", 16 | "is_significant_activity", 17 | "is_negative_activity", 18 | "is_all_day_activity", 19 | "notes", 20 | "created", 21 | "modified", 22 | "display_name", 23 | ) 24 | 25 | 26 | class ActivityCreateUpdateSerializer(BaseCreateUpdateSerializer): 27 | class Meta: 28 | model = Activity 29 | fields = ( 30 | "name", 31 | "is_significant_activity", 32 | "is_negative_activity", 33 | "is_all_day_activity", 34 | "notes", 35 | "user", 36 | ) 37 | 38 | def validate(self, validated_data): 39 | user = self.context["request"].user 40 | is_creating_instance = not self.instance 41 | 42 | if is_creating_instance: 43 | if self.Meta.model.objects.filter( 44 | user=user, name=validated_data["name"], 45 | ).exists(): 46 | raise ValidationError(f"Fields user and activity name are not unique!") 47 | 48 | return validated_data 49 | -------------------------------------------------------------------------------- /open/core/betterself/serializers/aggregrate_serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.fields import DateField, ListField, UUIDField 2 | from rest_framework.serializers import Serializer 3 | 4 | from open.core.betterself.models.activity import Activity 5 | from open.core.betterself.models.food import Food 6 | from open.core.betterself.models.supplement import Supplement 7 | from open.core.betterself.serializers.validators import ( 8 | generic_model_uuid_validator, 9 | ) 10 | 11 | 12 | class AggregrateViewParamsSerializer(Serializer): 13 | start_date = DateField() 14 | end_date = DateField() 15 | supplement_uuids = ListField( 16 | child=UUIDField(validators=[generic_model_uuid_validator(Supplement)]), 17 | required=False, 18 | ) 19 | activity_uuids = ListField( 20 | child=UUIDField(validators=[generic_model_uuid_validator(Activity)]), 21 | required=False, 22 | ) 23 | food_uuids = ListField( 24 | child=UUIDField(validators=[generic_model_uuid_validator(Food)]), required=False 25 | ) 26 | 27 | class Meta: 28 | fields = ( 29 | "start_date", 30 | "end_date", 31 | "supplement_uuids", 32 | "activity_uuids", 33 | "food_uuids", 34 | ) 35 | -------------------------------------------------------------------------------- /open/core/betterself/serializers/daily_productivity_log_serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.exceptions import ValidationError 2 | from rest_framework.fields import DateField, ChoiceField, CharField 3 | 4 | from open.core.betterself.constants import ( 5 | BETTERSELF_LOG_INPUT_SOURCES, 6 | WEB_INPUT_SOURCE, 7 | ) 8 | from open.core.betterself.models.daily_productivity_log import DailyProductivityLog 9 | from open.core.betterself.serializers.mixins import ( 10 | BaseCreateUpdateSerializer, 11 | BaseModelReadSerializer, 12 | ) 13 | from open.core.betterself.serializers.validators import ModelValidatorsMixin 14 | from open.utilities.date_and_time import ( 15 | format_datetime_to_human_readable, 16 | yyyy_mm_dd_format_1, 17 | ) 18 | 19 | 20 | class DailyProductivityLogReadSerializer(BaseModelReadSerializer): 21 | class Meta: 22 | model = DailyProductivityLog 23 | fields = ( 24 | "uuid", 25 | "source", 26 | "date", 27 | "very_productive_time_minutes", 28 | "productive_time_minutes", 29 | "neutral_time_minutes", 30 | "distracting_time_minutes", 31 | "very_distracting_time_minutes", 32 | "notes", 33 | "mistakes", 34 | "created", 35 | "modified", 36 | "display_name", 37 | "pomodoro_count", 38 | ) 39 | 40 | def get_display_name(self, instance): 41 | model = self.Meta.model 42 | model_name = model._meta.verbose_name 43 | 44 | time_label = instance.date 45 | serialized_time = format_datetime_to_human_readable( 46 | time_label, yyyy_mm_dd_format_1 47 | ) 48 | 49 | display_name = f"{model_name} | Date: {serialized_time}" 50 | return display_name 51 | 52 | 53 | class DailyProductivityLogCreateUpdateSerializer( 54 | BaseCreateUpdateSerializer, ModelValidatorsMixin 55 | ): 56 | # allow an regular isoformat of milliseconds also be passed 57 | date = DateField(input_formats=["iso-8601"]) 58 | source = ChoiceField(choices=BETTERSELF_LOG_INPUT_SOURCES, default=WEB_INPUT_SOURCE) 59 | mistakes = CharField(trim_whitespace=True, default="", allow_blank=True) 60 | 61 | class Meta: 62 | model = DailyProductivityLog 63 | fields = ( 64 | "source", 65 | "date", 66 | "very_productive_time_minutes", 67 | "productive_time_minutes", 68 | "neutral_time_minutes", 69 | "distracting_time_minutes", 70 | "very_distracting_time_minutes", 71 | "pomodoro_count", 72 | "notes", 73 | "mistakes", 74 | "user", 75 | ) 76 | 77 | def validate(self, validated_data): 78 | user = self.context["request"].user 79 | is_creating_instance = not self.instance 80 | 81 | if is_creating_instance: 82 | if self.Meta.model.objects.filter( 83 | user=user, date=validated_data["date"], 84 | ).exists(): 85 | raise ValidationError(f"Fields user and date need to be unique!") 86 | 87 | return validated_data 88 | -------------------------------------------------------------------------------- /open/core/betterself/serializers/food_log_serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.exceptions import ValidationError 2 | from rest_framework.fields import UUIDField 3 | 4 | from open.core.betterself.models.food import Food 5 | from open.core.betterself.models.food_logs import FoodLog 6 | from open.core.betterself.serializers.mixins import ( 7 | BaseCreateUpdateSerializer, 8 | BaseModelReadSerializer, 9 | ) 10 | from open.core.betterself.serializers.simple_generic_serializer import ( 11 | create_name_uuid_serializer, 12 | ) 13 | from open.core.betterself.serializers.validators import ModelValidatorsMixin 14 | 15 | 16 | class FoodLogReadSerializer(BaseModelReadSerializer): 17 | food = create_name_uuid_serializer(Food) 18 | 19 | class Meta: 20 | model = FoodLog 21 | fields = ( 22 | "uuid", 23 | "food", 24 | "time", 25 | "notes", 26 | "created", 27 | "modified", 28 | "display_name", 29 | "quantity", 30 | ) 31 | 32 | 33 | class FoodLogCreateUpdateSerializer(BaseCreateUpdateSerializer, ModelValidatorsMixin): 34 | food_uuid = UUIDField(source="food.uuid") 35 | 36 | class Meta: 37 | model = FoodLog 38 | fields = ( 39 | "food_uuid", 40 | "quantity", 41 | "time", 42 | "notes", 43 | "user", 44 | ) 45 | 46 | def validate(self, validated_data): 47 | user = self.context["request"].user 48 | is_creating_instance = not self.instance 49 | 50 | if validated_data.get("food"): 51 | food_uuid = validated_data.pop("food")["uuid"] 52 | food = Food.objects.get(uuid=food_uuid, user=user) 53 | validated_data["food"] = food 54 | 55 | if is_creating_instance: 56 | if self.Meta.model.objects.filter( 57 | user=user, food=food, time=validated_data["time"], 58 | ).exists(): 59 | raise ValidationError(f"Fields user, food, and time are not unique!") 60 | 61 | return validated_data 62 | -------------------------------------------------------------------------------- /open/core/betterself/serializers/food_serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.exceptions import ValidationError 2 | 3 | from open.core.betterself.models.food import Food 4 | from open.core.betterself.serializers.mixins import ( 5 | BaseCreateUpdateSerializer, 6 | BaseModelReadSerializer, 7 | ) 8 | 9 | 10 | class FoodReadSerializer(BaseModelReadSerializer): 11 | class Meta: 12 | model = Food 13 | fields = ( 14 | "uuid", 15 | "name", 16 | "notes", 17 | "created", 18 | "modified", 19 | "display_name", 20 | "calories", 21 | "is_liquid", 22 | ) 23 | 24 | 25 | class FoodCreateUpdateSerializer(BaseCreateUpdateSerializer): 26 | class Meta: 27 | model = Food 28 | fields = ("name", "notes", "user", "calories", "is_liquid") 29 | 30 | def validate(self, validated_data): 31 | user = self.context["request"].user 32 | is_creating_instance = not self.instance 33 | 34 | if is_creating_instance: 35 | if self.Meta.model.objects.filter( 36 | user=user, name=validated_data["name"], 37 | ).exists(): 38 | raise ValidationError(f"Fields user and name are not unique!") 39 | 40 | return validated_data 41 | -------------------------------------------------------------------------------- /open/core/betterself/serializers/ingredient_serializers.py: -------------------------------------------------------------------------------- 1 | from open.core.betterself.models.ingredient import Ingredient 2 | from open.core.betterself.serializers.mixins import ( 3 | BaseCreateUpdateSerializer, 4 | BaseModelReadSerializer, 5 | ) 6 | from open.users.serializers import SimpleUserReadSerializer 7 | 8 | 9 | class IngredientReadSerializer(BaseModelReadSerializer): 10 | user = SimpleUserReadSerializer(read_only=True) 11 | 12 | class Meta: 13 | model = Ingredient 14 | fields = [ 15 | "half_life_minutes", 16 | "name", 17 | "notes", 18 | "user", 19 | "uuid", 20 | "created", 21 | "modified", 22 | "display_name", 23 | ] 24 | 25 | 26 | class IngredientCreateUpdateSerializer(BaseCreateUpdateSerializer): 27 | class Meta: 28 | model = Ingredient 29 | fields = ["half_life_minutes", "name", "notes", "user", "uuid"] 30 | -------------------------------------------------------------------------------- /open/core/betterself/serializers/measurement_serializers.py: -------------------------------------------------------------------------------- 1 | from open.core.betterself.models.measurement import Measurement 2 | from open.core.betterself.serializers.mixins import BaseModelReadSerializer 3 | 4 | 5 | class MeasurementReadSerializer(BaseModelReadSerializer): 6 | class Meta: 7 | model = Measurement 8 | fields = ("uuid", "name", "short_name", "is_liquid", "display_name") 9 | -------------------------------------------------------------------------------- /open/core/betterself/serializers/mixins.py: -------------------------------------------------------------------------------- 1 | from rest_framework.fields import ( 2 | UUIDField, 3 | HiddenField, 4 | CurrentUserDefault, 5 | SerializerMethodField, 6 | CharField, 7 | ) 8 | from rest_framework.serializers import ModelSerializer 9 | 10 | from open.utilities.date_and_time import format_datetime_to_human_readable 11 | 12 | 13 | class BaseModelReadSerializer(ModelSerializer): 14 | display_name = SerializerMethodField() 15 | 16 | def get_display_name(self, instance): 17 | if hasattr(instance, "name"): 18 | return instance.name 19 | else: 20 | model = self.Meta.model 21 | model_name = model._meta.verbose_name 22 | 23 | created = instance.created 24 | created_serialized = format_datetime_to_human_readable(created) 25 | 26 | display_name = f"{model_name} | Create Time: {created_serialized} UTC" 27 | return display_name 28 | 29 | 30 | class BaseCreateUpdateSerializer(ModelSerializer): 31 | uuid = UUIDField(required=False, read_only=True) 32 | user = HiddenField(default=CurrentUserDefault()) 33 | # if you don't have this when notes is sent with "null/none", database integrity has an issue 34 | # since serializers will try to input that into the db 35 | notes = CharField( 36 | default="", trim_whitespace=True, required=False, allow_blank=True, 37 | ) 38 | 39 | def create(self, validated_data): 40 | create_model = self.Meta.model 41 | obj = create_model.objects.create(**validated_data) 42 | return obj 43 | 44 | def update(self, instance, validated_data): 45 | for key, value in validated_data.items(): 46 | setattr(instance, key, value) 47 | 48 | instance.save() 49 | return instance 50 | -------------------------------------------------------------------------------- /open/core/betterself/serializers/simple_generic_serializer.py: -------------------------------------------------------------------------------- 1 | from rest_framework.serializers import ModelSerializer 2 | 3 | 4 | def create_name_uuid_serializer(model): 5 | """ Dynamically creates a serializer with a minimum set of required 6 | fields, easier to do this can create lots of tiny serializers that 7 | are just the same but different Meta.models 8 | """ 9 | 10 | class NameUUIDSerializer(ModelSerializer): 11 | class Meta: 12 | fields = ("uuid", "created", "modified", "name") 13 | 14 | NameUUIDSerializer.Meta.model = model 15 | return NameUUIDSerializer() 16 | -------------------------------------------------------------------------------- /open/core/betterself/serializers/supplement_stack_serializers.py: -------------------------------------------------------------------------------- 1 | from open.core.betterself.models.supplement_stack import SupplementStack 2 | from open.core.betterself.serializers.mixins import ( 3 | BaseCreateUpdateSerializer, 4 | BaseModelReadSerializer, 5 | ) 6 | from open.core.betterself.serializers.supplement_stack_composition_serializers import ( 7 | SupplementStackCompositionReadSerializer, 8 | ) 9 | 10 | 11 | class SupplementStackReadSerializer(BaseModelReadSerializer): 12 | compositions = SupplementStackCompositionReadSerializer(many=True) 13 | 14 | class Meta: 15 | model = SupplementStack 16 | fields = ( 17 | "uuid", 18 | "name", 19 | "notes", 20 | "compositions", 21 | "display_name", 22 | ) 23 | 24 | 25 | class SupplementStackCreateUpdateSerializer(BaseCreateUpdateSerializer): 26 | class Meta: 27 | model = SupplementStack 28 | fields = ( 29 | "uuid", 30 | "name", 31 | "user", 32 | "notes", 33 | ) 34 | -------------------------------------------------------------------------------- /open/core/betterself/serializers/validators.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ObjectDoesNotExist 2 | from rest_framework.exceptions import ValidationError 3 | 4 | from open.core.betterself.models.activity import Activity 5 | from open.core.betterself.models.ingredient_composition import IngredientComposition 6 | from open.core.betterself.models.supplement import Supplement 7 | from functools import partial 8 | 9 | 10 | def ingredient_composition_uuid_validator(uuid): 11 | validate_model_uuid(uuid, IngredientComposition) 12 | return uuid 13 | 14 | 15 | def supplement_uuid_validator(uuid): 16 | validate_model_uuid(uuid, Supplement) 17 | return uuid 18 | 19 | 20 | def generic_model_uuid_validator(model): 21 | return partial(validate_model_uuid, model=model) 22 | 23 | 24 | def validate_model_uuid(uuid, model, user=None): 25 | try: 26 | if user: 27 | model.objects.get(uuid=uuid, user=user) 28 | else: 29 | model.objects.get(uuid=uuid) 30 | 31 | except ObjectDoesNotExist: 32 | raise ValidationError(f"Cannot Find {model._meta.verbose_name.title()} UUID") 33 | 34 | 35 | class ModelValidatorsMixin: 36 | """ a mixin holding all the commonly used validators in the validate_X step""" 37 | 38 | def validate_activity_uuid(self, value): 39 | user = None 40 | if self.context["request"]: 41 | user = self.context["request"].user 42 | 43 | validate_model_uuid(uuid=value, model=Activity, user=user) 44 | return value 45 | -------------------------------------------------------------------------------- /open/core/betterself/serializers/well_being_log_serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.exceptions import ValidationError 2 | from rest_framework.fields import ChoiceField 3 | 4 | from open.core.betterself.constants import INPUT_SOURCES_TUPLES, WEB_INPUT_SOURCE 5 | from open.core.betterself.models.well_being_log import WellBeingLog 6 | from open.core.betterself.serializers.mixins import ( 7 | BaseCreateUpdateSerializer, 8 | BaseModelReadSerializer, 9 | ) 10 | from open.core.betterself.serializers.validators import ModelValidatorsMixin 11 | 12 | 13 | class WellBeingLogReadSerializer(BaseModelReadSerializer): 14 | class Meta: 15 | model = WellBeingLog 16 | fields = ( 17 | "mental_value", 18 | "physical_value", 19 | "time", 20 | "source", 21 | "notes", 22 | # "created", 23 | # "modified", 24 | "uuid", 25 | "display_name", 26 | ) 27 | 28 | 29 | class WellBeingLogCreateUpdateSerializer( 30 | BaseCreateUpdateSerializer, ModelValidatorsMixin 31 | ): 32 | source = ChoiceField(INPUT_SOURCES_TUPLES, default=WEB_INPUT_SOURCE) 33 | 34 | class Meta: 35 | model = WellBeingLog 36 | fields = ( 37 | "mental_value", 38 | "physical_value", 39 | "time", 40 | "source", 41 | "notes", 42 | "user", 43 | ) 44 | 45 | def validate(self, validated_data): 46 | user = self.context["request"].user 47 | is_creating_instance = not self.instance 48 | 49 | if is_creating_instance: 50 | if self.Meta.model.objects.filter( 51 | user=user, time=validated_data["time"], 52 | ).exists(): 53 | raise ValidationError(f"Fields user and time are not unique!") 54 | 55 | return validated_data 56 | -------------------------------------------------------------------------------- /open/core/betterself/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/betterself/tests/__init__.py -------------------------------------------------------------------------------- /open/core/betterself/tests/mixins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/betterself/tests/mixins/__init__.py -------------------------------------------------------------------------------- /open/core/betterself/tests/test_demo_fixture_creation.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from test_plus import TestCase 3 | 4 | from open.core.betterself.constants import DEMO_TESTING_ACCOUNT 5 | from open.core.betterself.utilities.demo_user_factory_fixtures import ( 6 | create_demo_fixtures_for_user, 7 | ) 8 | from open.users.factories import UserFactory 9 | 10 | User = get_user_model() 11 | 12 | """ 13 | python manage.py test --pattern="*test_demo_fixture_creation.py" --keepdb 14 | """ 15 | 16 | 17 | class TestDemoFixtureUtility(TestCase): 18 | def setUp(self) -> None: 19 | self.user = UserFactory(username=DEMO_TESTING_ACCOUNT) 20 | 21 | @classmethod 22 | def setUpTestData(cls): 23 | user = UserFactory(username=DEMO_TESTING_ACCOUNT, email=DEMO_TESTING_ACCOUNT) 24 | create_demo_fixtures_for_user(user) 25 | 26 | # turned this off until i know what i want to do with it ... 27 | # def test_demo_fixture_with_supplement_and_compositions(self): 28 | # user_supplements = Supplement.objects.filter(user=self.user) 29 | # instance = user_supplements[0] 30 | # 31 | # self.assertGreater(instance.ingredient_compositions.all().count(), 0) 32 | 33 | def test_demo_fixture_wont_make_for_valid_user(self): 34 | user = UserFactory(username="real@gmail.com", email="real@gmail.com") 35 | 36 | with self.assertRaises(ValueError): 37 | create_demo_fixtures_for_user(user) 38 | -------------------------------------------------------------------------------- /open/core/betterself/tests/test_factories.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from test_plus import TestCase 3 | 4 | from open.core.betterself.factories import ( 5 | SupplementLogFactory, 6 | SupplementFactory, 7 | SupplementStackCompositionFactory, 8 | SupplementStackFactory, 9 | IngredientCompositionFactory, 10 | MeasurementFactory, 11 | IngredientFactory, 12 | ActivityLogFactory, 13 | ActivityFactory, 14 | DailyProductivityLogFactory, 15 | SleepLogFactory, 16 | WellBeingLogFactory, 17 | FoodFactory, 18 | FoodLogFactory, 19 | ) 20 | 21 | User = get_user_model() 22 | 23 | """ 24 | python manage.py test --pattern="*test_factories.py" --keepdb 25 | """ 26 | 27 | 28 | class TestBetterSelfFactories(TestCase): 29 | def test_all_betterself_factories(self): 30 | """ 31 | A quick and lazy way to make sure all factories fire correctly 32 | """ 33 | factories_to_test = [ 34 | ActivityFactory, 35 | ActivityLogFactory, 36 | DailyProductivityLogFactory, 37 | IngredientFactory, 38 | IngredientCompositionFactory, 39 | MeasurementFactory, 40 | SleepLogFactory, 41 | SupplementFactory, 42 | SupplementLogFactory, 43 | SupplementStackFactory, 44 | SupplementStackCompositionFactory, 45 | WellBeingLogFactory, 46 | FoodFactory, 47 | FoodLogFactory, 48 | ] 49 | 50 | for factory in factories_to_test: 51 | created_instance = factory() 52 | self.assertIsNotNone(created_instance) 53 | 54 | def test_sleep_log_factory(self): 55 | # this one is so unique i need to test it separately 56 | sleep_logs = SleepLogFactory.create_batch(10) 57 | instance = sleep_logs[0] 58 | 59 | self.assertTrue(instance.end_time > instance.start_time) 60 | 61 | sleep_time = instance.end_time - instance.start_time 62 | sleep_time_seconds = sleep_time.seconds 63 | sleep_time_hours = sleep_time_seconds // 3600 64 | 65 | # should always range between at least 1 and 14 for test fixtures 66 | self.assertTrue(14 > sleep_time_hours > 1) 67 | -------------------------------------------------------------------------------- /open/core/betterself/tests/test_validators.py: -------------------------------------------------------------------------------- 1 | from rest_framework.exceptions import ValidationError 2 | from test_plus import TestCase 3 | 4 | from open.core.betterself.factories import SupplementFactory 5 | from open.core.betterself.models.supplement import Supplement 6 | from open.core.betterself.serializers.validators import generic_model_uuid_validator 7 | import uuid 8 | 9 | """ 10 | dpy test open.core.betterself.tests.test_validators --keepdb 11 | """ 12 | 13 | 14 | class TestValidators(TestCase): 15 | def test_partial_validator_with_supplement_uuid(self): 16 | supplement = SupplementFactory() 17 | supplement_uuid = str(supplement.uuid) 18 | 19 | validator = generic_model_uuid_validator(Supplement) 20 | 21 | result = validator(supplement_uuid) 22 | # if it's valid, it doesn't return anything 23 | self.assertIsNone(result) 24 | 25 | # bad uuid, it should trip 26 | random_uuid = str(uuid.uuid4()) 27 | with self.assertRaises(ValidationError): 28 | validator(random_uuid) 29 | -------------------------------------------------------------------------------- /open/core/betterself/tests/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/betterself/tests/views/__init__.py -------------------------------------------------------------------------------- /open/core/betterself/tests/views/test_food_log_views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from test_plus import TestCase 3 | 4 | from open.core.betterself.constants import BetterSelfResourceConstants 5 | from open.core.betterself.factories import FoodLogFactory 6 | from open.core.betterself.models.food_logs import FoodLog 7 | from open.core.betterself.tests.mixins.resource_mixin import ( 8 | BetterSelfResourceViewTestCaseMixin, 9 | GetTestsMixin, 10 | DeleteTestsMixin, 11 | ) 12 | 13 | User = get_user_model() 14 | 15 | """ 16 | python manage.py test --pattern="*test_food_log_views.py" --keepdb 17 | """ 18 | 19 | 20 | class FoodLogTestView(BetterSelfResourceViewTestCaseMixin, TestCase): 21 | url_name = BetterSelfResourceConstants.FOODS 22 | model_class_factory = FoodLogFactory 23 | model_class = FoodLog 24 | 25 | 26 | class FoodLogTestGetUpdateView( 27 | BetterSelfResourceViewTestCaseMixin, GetTestsMixin, DeleteTestsMixin, TestCase 28 | ): 29 | url_name = BetterSelfResourceConstants.FOODS 30 | model_class_factory = FoodLogFactory 31 | model_class = FoodLog 32 | -------------------------------------------------------------------------------- /open/core/betterself/tests/views/test_food_views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from test_plus import TestCase 3 | 4 | from open.core.betterself.constants import BetterSelfResourceConstants 5 | from open.core.betterself.factories import FoodFactory 6 | from open.core.betterself.models.food import Food 7 | from open.core.betterself.tests.mixins.resource_mixin import ( 8 | BetterSelfResourceViewTestCaseMixin, 9 | GetTestsMixin, 10 | DeleteTestsMixin, 11 | ) 12 | 13 | User = get_user_model() 14 | 15 | """ 16 | python manage.py test --pattern="*test_food_views.py" --keepdb 17 | """ 18 | 19 | 20 | class FoodTestView(BetterSelfResourceViewTestCaseMixin, TestCase): 21 | url_name = BetterSelfResourceConstants.FOODS 22 | model_class_factory = FoodFactory 23 | model_class = Food 24 | 25 | 26 | class FoodTestGetUpdateView( 27 | BetterSelfResourceViewTestCaseMixin, GetTestsMixin, DeleteTestsMixin, TestCase 28 | ): 29 | url_name = BetterSelfResourceConstants.FOODS 30 | model_class_factory = FoodFactory 31 | model_class = Food 32 | -------------------------------------------------------------------------------- /open/core/betterself/tests/views/test_measurement_views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from test_plus import TestCase 3 | 4 | from open.core.betterself.constants import BetterSelfResourceConstants 5 | from open.core.betterself.factories import MeasurementFactory 6 | from open.core.betterself.models.measurement import Measurement 7 | from open.core.betterself.tests.mixins.resource_mixin import ( 8 | BetterSelfResourceViewTestCaseMixin, 9 | ) 10 | 11 | User = get_user_model() 12 | 13 | """ 14 | python manage.py test --pattern="*test_measurement_views.py" --keepdb 15 | """ 16 | 17 | 18 | class TestMeasurementView(BetterSelfResourceViewTestCaseMixin, TestCase): 19 | url_name = BetterSelfResourceConstants.MEASUREMENTS 20 | model_class_factory = MeasurementFactory 21 | model_class = Measurement 22 | 23 | def test_view(self): 24 | data = self.client_1.get(self.url).data 25 | self.assertEqual(len(data), 5) 26 | 27 | def test_no_access_view(self): 28 | """ 29 | Doesn't apply here, measurements are available for all. 30 | """ 31 | return 32 | -------------------------------------------------------------------------------- /open/core/betterself/tests/views/test_supplement_views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from test_plus import TestCase 3 | 4 | from open.core.betterself.constants import ( 5 | BetterSelfResourceConstants, 6 | TEST_CONSTANTS as CONSTANTS, 7 | ) 8 | from open.core.betterself.factories import ( 9 | SupplementFactory, 10 | IngredientCompositionFactory, 11 | ) 12 | from open.core.betterself.models.supplement import Supplement 13 | from open.core.betterself.tests.mixins.resource_mixin import ( 14 | BetterSelfResourceViewTestCaseMixin, 15 | DeleteTestsMixin, 16 | GetTestsMixin, 17 | ) 18 | from open.core.betterself.utilities.serializer_utilties import iterable_to_uuids_list 19 | 20 | User = get_user_model() 21 | 22 | """ 23 | python manage.py test --pattern="*test_supplement_views.py" --keepdb 24 | """ 25 | 26 | 27 | class TestSupplementsView(BetterSelfResourceViewTestCaseMixin, TestCase): 28 | url_name = BetterSelfResourceConstants.SUPPLEMENTS 29 | model_class_factory = SupplementFactory 30 | model_class = Supplement 31 | 32 | def test_create_view(self): 33 | post_data = {"name": CONSTANTS.NAME_1, "notes": CONSTANTS.NOTES_1} 34 | 35 | response = self.client_1.post(self.url, data=post_data) 36 | self.assertEqual(response.status_code, 200, response.data) 37 | 38 | data = response.data 39 | 40 | self.assertEqual(data["name"], CONSTANTS.NAME_1) 41 | self.assertEqual(data["notes"], CONSTANTS.NOTES_1) 42 | 43 | def test_create_view_with_ingredient_compositions(self): 44 | ind_comps = IngredientCompositionFactory.create_batch(3, user=self.user_1) 45 | ingredient_composition_uuids = iterable_to_uuids_list(ind_comps) 46 | 47 | post_data = { 48 | "name": CONSTANTS.NOTES_1, 49 | "notes": CONSTANTS.NOTES_1, 50 | "ingredient_composition_uuids": ingredient_composition_uuids, 51 | } 52 | 53 | response = self.client_1.post(self.url, data=post_data) 54 | self.assertEqual(response.status_code, 200, response.data) 55 | 56 | def test_create_view_with_conflicting_ingredient_compositions(self): 57 | post_data = { 58 | "name": CONSTANTS.NOTES_1, 59 | "notes": CONSTANTS.NOTES_1, 60 | "ingredient_composition_uuids": [CONSTANTS.INVALID_UUID], 61 | } 62 | 63 | response = self.client_1.post(self.url, data=post_data) 64 | 65 | # error out and say the reason is 66 | self.assertEqual(response.status_code, 400, response.data) 67 | self.assertIn("ingredient_composition_uuids", response.data) 68 | 69 | 70 | class TestSupplementGetUpdateDelete( 71 | BetterSelfResourceViewTestCaseMixin, GetTestsMixin, DeleteTestsMixin, TestCase 72 | ): 73 | url_name = BetterSelfResourceConstants.SUPPLEMENTS 74 | model_class_factory = SupplementFactory 75 | model_class = Supplement 76 | 77 | def test_avoid_extra_sql_queries(self): 78 | # TODO - need to add the prefetch_related to this when it gets slow 79 | return 80 | -------------------------------------------------------------------------------- /open/core/betterself/tests/views/test_well_being_log_views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from test_plus import TestCase 3 | 4 | from open.core.betterself.constants import BetterSelfResourceConstants 5 | from open.core.betterself.factories import WellBeingLogFactory 6 | from open.core.betterself.models.well_being_log import WellBeingLog 7 | from open.core.betterself.tests.mixins.resource_mixin import ( 8 | BetterSelfResourceViewTestCaseMixin, 9 | GetTestsMixin, 10 | DeleteTestsMixin, 11 | ) 12 | 13 | User = get_user_model() 14 | 15 | """ 16 | python manage.py test --pattern="*test_well_being_log_views.py" --keepdb 17 | """ 18 | 19 | 20 | class WellBeingLogCreateTestView(BetterSelfResourceViewTestCaseMixin, TestCase): 21 | url_name = BetterSelfResourceConstants.WELL_BEING_LOGS 22 | model_class_factory = WellBeingLogFactory 23 | model_class = WellBeingLog 24 | 25 | def test_create_view_with_conflicting_unique(self): 26 | post_data = {"time": self.current_time_isoformat, "mental_value": 5} 27 | 28 | response = self.client_1.post(self.url, data=post_data) 29 | self.assertEqual(response.status_code, 200) 30 | 31 | # post again, don't let you create something already made 32 | response = self.client_1.post(self.url, data=post_data) 33 | self.assertEqual(response.status_code, 400, response.data) 34 | 35 | data = response.data 36 | """ 37 | error_message should be 38 | {'non_field_errors': [ErrorDetail(string='The fields user, name must make a unique set.', code='unique')]} 39 | """ 40 | expected_error_found = "non_field_errors" in data 41 | self.assertTrue(expected_error_found) 42 | 43 | 44 | class WellBeingLogTestGetUpdateView( 45 | BetterSelfResourceViewTestCaseMixin, GetTestsMixin, DeleteTestsMixin, TestCase 46 | ): 47 | url_name = BetterSelfResourceConstants.WELL_BEING_LOGS 48 | model_class_factory = WellBeingLogFactory 49 | model_class = WellBeingLog 50 | 51 | def test_update_view_with_updated_params(self): 52 | instance = self.model_class_factory(user=self.user_1) 53 | previous_mental_value = instance.mental_value 54 | url = instance.get_update_url() 55 | 56 | params = {"mental_value": previous_mental_value + 1} 57 | 58 | response = self.client_1.post(url, data=params) 59 | data = response.data 60 | 61 | self.assertEqual(response.status_code, 200, data) 62 | self.assertEqual(data["mental_value"], previous_mental_value + 1) 63 | 64 | instance.refresh_from_db() 65 | self.assertEqual(instance.mental_value, previous_mental_value + 1) 66 | -------------------------------------------------------------------------------- /open/core/betterself/utilities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/betterself/utilities/__init__.py -------------------------------------------------------------------------------- /open/core/betterself/utilities/serializer_utilties.py: -------------------------------------------------------------------------------- 1 | def iterable_to_uuids_list(iterable): 2 | """ 3 | takes an iterable of django objects and gets the str uuid into a list 4 | """ 5 | result = [] 6 | for item in iterable: 7 | uuid_label = str(item.uuid) 8 | result.append(uuid_label) 9 | return result 10 | -------------------------------------------------------------------------------- /open/core/betterself/utilities/user_date_utilities.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django.http import Http404 4 | 5 | 6 | def serialize_date_to_user_localized_datetime(date, user): 7 | if isinstance(date, str): 8 | try: 9 | date = datetime.strptime(date, "%Y-%m-%d").date() 10 | except ValueError: 11 | raise Http404 12 | 13 | start_period = date 14 | start_period = datetime( 15 | year=start_period.year, 16 | month=start_period.month, 17 | day=start_period.day, 18 | tzinfo=user.timezone, 19 | ) 20 | return start_period 21 | 22 | 23 | def serialize_end_date_to_user_localized_datetime(date, user): 24 | if isinstance(date, str): 25 | try: 26 | date = datetime.strptime(date, "%Y-%m-%d").date() 27 | except ValueError: 28 | raise Http404 29 | 30 | end_period = date 31 | end_period = datetime( 32 | year=end_period.year, 33 | month=end_period.month, 34 | day=end_period.day, 35 | hour=23, 36 | minute=59, 37 | second=59, 38 | tzinfo=user.timezone, 39 | ) 40 | return end_period 41 | -------------------------------------------------------------------------------- /open/core/betterself/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/betterself/views/__init__.py -------------------------------------------------------------------------------- /open/core/betterself/views/activity_log_views.py: -------------------------------------------------------------------------------- 1 | from open.core.betterself.models.activity_log import ActivityLog 2 | from open.core.betterself.serializers.activity_log_serializers import ( 3 | ActivityLogReadSerializer, 4 | ActivityLogCreateUpdateSerializer, 5 | ) 6 | from open.core.betterself.views.mixins import ( 7 | BaseGetUpdateDeleteView, 8 | BaseCreateListView, 9 | ) 10 | 11 | 12 | class ActivityLogCreateListView(BaseCreateListView): 13 | model_class = ActivityLog 14 | read_serializer_class = ActivityLogReadSerializer 15 | create_serializer_class = ActivityLogCreateUpdateSerializer 16 | 17 | 18 | class ActivityLogGetUpdateView(BaseGetUpdateDeleteView): 19 | model_class = ActivityLog 20 | read_serializer_class = ActivityLogReadSerializer 21 | update_serializer_class = ActivityLogCreateUpdateSerializer 22 | -------------------------------------------------------------------------------- /open/core/betterself/views/activity_views.py: -------------------------------------------------------------------------------- 1 | from open.core.betterself.models.activity import Activity 2 | from open.core.betterself.serializers.activity_serializers import ( 3 | ActivityReadSerializer, 4 | ActivityCreateUpdateSerializer, 5 | ) 6 | from open.core.betterself.views.mixins import ( 7 | BaseGetUpdateDeleteView, 8 | BaseCreateListView, 9 | ) 10 | 11 | 12 | class ActivityCreateListView(BaseCreateListView): 13 | model_class = Activity 14 | read_serializer_class = ActivityReadSerializer 15 | create_serializer_class = ActivityCreateUpdateSerializer 16 | 17 | 18 | class ActivityGetUpdateView(BaseGetUpdateDeleteView): 19 | model_class = Activity 20 | read_serializer_class = ActivityReadSerializer 21 | update_serializer_class = ActivityCreateUpdateSerializer 22 | -------------------------------------------------------------------------------- /open/core/betterself/views/aggregrate_views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | from rest_framework.views import APIView 3 | 4 | from open.core.betterself.models.food import Food 5 | from open.core.betterself.models.supplement import Supplement 6 | from open.core.betterself.serializers.aggregrate_serializers import ( 7 | AggregrateViewParamsSerializer, 8 | ) 9 | from open.core.betterself.utilities.history_overview_utilities import ( 10 | get_overview_supplements_data, 11 | get_overview_food_data, 12 | ) 13 | from open.core.betterself.utilities.user_date_utilities import ( 14 | serialize_date_to_user_localized_datetime, 15 | serialize_end_date_to_user_localized_datetime, 16 | ) 17 | 18 | 19 | class AggregateView(APIView): 20 | """ 21 | Only working for Supplements for now, I'll add more when it makes sense to 22 | - This should eventually support food / activities too 23 | """ 24 | 25 | def post(self, request): 26 | user = request.user 27 | 28 | serializer = AggregrateViewParamsSerializer(data=request.data) 29 | serializer.is_valid(raise_exception=True) 30 | 31 | data = serializer.validated_data 32 | 33 | # convert to start (00:00) and end of day (23:59) 34 | start_period = serialize_date_to_user_localized_datetime( 35 | data["start_date"], user 36 | ) 37 | end_period = serialize_end_date_to_user_localized_datetime( 38 | data["end_date"], user 39 | ) 40 | 41 | response = { 42 | # change it back to date, so it doesn't look super confusing on api response ... 43 | "start_period": start_period.date().isoformat(), 44 | "end_period": end_period.date().isoformat(), 45 | } 46 | 47 | supplement_uuids = data.get("supplement_uuids") 48 | if supplement_uuids: 49 | supplements = Supplement.objects.filter(uuid__in=supplement_uuids) 50 | supplements_data = get_overview_supplements_data( 51 | user=user, 52 | start_period=start_period, 53 | end_period=end_period, 54 | filter_supplements=supplements, 55 | ) 56 | response["supplements"] = supplements_data 57 | 58 | food_uuids = data.get("food_uuids") 59 | if food_uuids: 60 | filter_foods = Food.objects.filter(uuid__in=food_uuids) 61 | foods_data = get_overview_food_data( 62 | user=user, 63 | start_period=start_period, 64 | end_period=end_period, 65 | filter_foods=filter_foods, 66 | ) 67 | response["foods"] = foods_data 68 | 69 | return Response(response) 70 | -------------------------------------------------------------------------------- /open/core/betterself/views/auth_views.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/betterself/views/auth_views.py -------------------------------------------------------------------------------- /open/core/betterself/views/daily_productivity_log_views.py: -------------------------------------------------------------------------------- 1 | from open.core.betterself.models.daily_productivity_log import DailyProductivityLog 2 | from open.core.betterself.serializers.daily_productivity_log_serializers import ( 3 | DailyProductivityLogReadSerializer, 4 | DailyProductivityLogCreateUpdateSerializer, 5 | ) 6 | from open.core.betterself.views.mixins import ( 7 | BaseGetUpdateDeleteView, 8 | BaseCreateListView, 9 | ) 10 | 11 | 12 | class DailyProductivityLogCreateListView(BaseCreateListView): 13 | model_class = DailyProductivityLog 14 | read_serializer_class = DailyProductivityLogReadSerializer 15 | create_serializer_class = DailyProductivityLogCreateUpdateSerializer 16 | 17 | 18 | class DailyProductivityLogGetUpdateView(BaseGetUpdateDeleteView): 19 | model_class = DailyProductivityLog 20 | read_serializer_class = DailyProductivityLogReadSerializer 21 | update_serializer_class = DailyProductivityLogCreateUpdateSerializer 22 | -------------------------------------------------------------------------------- /open/core/betterself/views/daily_view.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from rest_framework.response import Response 4 | from rest_framework.views import APIView 5 | 6 | from open.core.betterself.utilities.user_date_utilities import ( 7 | serialize_date_to_user_localized_datetime, 8 | ) 9 | from open.core.betterself.utilities.history_overview_utilities import ( 10 | get_overview_supplements_data, 11 | get_overview_productivity_data, 12 | get_overview_sleep_data, 13 | get_overview_well_being_data, 14 | get_overview_activity_data, 15 | get_overview_food_data, 16 | get_timeline_data, 17 | ) 18 | 19 | 20 | class DailyReviewView(APIView): 21 | def get(self, request, date): 22 | user = request.user 23 | start_period = serialize_date_to_user_localized_datetime(date, user) 24 | 25 | # use the start_period, but now get the end of the day 26 | end_period = datetime( 27 | year=start_period.year, 28 | month=start_period.month, 29 | day=start_period.day, 30 | hour=23, 31 | minute=59, 32 | second=59, 33 | tzinfo=user.timezone, 34 | ) 35 | 36 | sleep_data = get_overview_sleep_data( 37 | user, start_period=start_period, end_period=end_period 38 | ) 39 | supplements_data = get_overview_supplements_data( 40 | user=user, start_period=start_period, end_period=end_period 41 | ) 42 | 43 | productivity_data = get_overview_productivity_data( 44 | user=user, start_period=start_period, end_period=end_period 45 | ) 46 | 47 | well_being_data = get_overview_well_being_data( 48 | user=user, start_period=start_period, end_period=end_period 49 | ) 50 | activities_data = get_overview_activity_data( 51 | user=user, start_period=start_period, end_period=end_period 52 | ) 53 | foods_data = get_overview_food_data( 54 | user=user, start_period=start_period, end_period=end_period 55 | ) 56 | timeline_data = get_timeline_data( 57 | sleep_data=sleep_data, 58 | supplements_data=supplements_data, 59 | productivity_data=productivity_data, 60 | well_being_data=well_being_data, 61 | activities_data=activities_data, 62 | foods_data=foods_data, 63 | ) 64 | 65 | response = { 66 | # change it back to a date, so it doesn't look super confusing on api response ... 67 | "date": start_period.date().isoformat(), 68 | "activities": activities_data, 69 | "foods": foods_data, 70 | "productivity": productivity_data, 71 | "sleep": sleep_data, 72 | "supplements": supplements_data, 73 | "well_being": well_being_data, 74 | "timeline": timeline_data, 75 | } 76 | 77 | return Response(response) 78 | -------------------------------------------------------------------------------- /open/core/betterself/views/food_log_views.py: -------------------------------------------------------------------------------- 1 | from open.core.betterself.models.food_logs import FoodLog 2 | from open.core.betterself.serializers.food_log_serializers import ( 3 | FoodLogReadSerializer, 4 | FoodLogCreateUpdateSerializer, 5 | ) 6 | from open.core.betterself.views.mixins import ( 7 | BaseGetUpdateDeleteView, 8 | BaseCreateListView, 9 | ) 10 | 11 | 12 | class FoodLogCreateListView(BaseCreateListView): 13 | model_class = FoodLog 14 | read_serializer_class = FoodLogReadSerializer 15 | create_serializer_class = FoodLogCreateUpdateSerializer 16 | 17 | 18 | class FoodLogGetUpdateView(BaseGetUpdateDeleteView): 19 | model_class = FoodLog 20 | read_serializer_class = FoodLogReadSerializer 21 | update_serializer_class = FoodLogCreateUpdateSerializer 22 | -------------------------------------------------------------------------------- /open/core/betterself/views/food_views.py: -------------------------------------------------------------------------------- 1 | from open.core.betterself.models.food import Food 2 | from open.core.betterself.serializers.food_serializers import ( 3 | FoodReadSerializer, 4 | FoodCreateUpdateSerializer, 5 | ) 6 | from open.core.betterself.views.mixins import ( 7 | BaseGetUpdateDeleteView, 8 | BaseCreateListView, 9 | ) 10 | 11 | 12 | class FoodCreateListView(BaseCreateListView): 13 | model_class = Food 14 | read_serializer_class = FoodReadSerializer 15 | create_serializer_class = FoodCreateUpdateSerializer 16 | 17 | 18 | class FoodGetUpdateView(BaseGetUpdateDeleteView): 19 | model_class = Food 20 | read_serializer_class = FoodReadSerializer 21 | update_serializer_class = FoodCreateUpdateSerializer 22 | -------------------------------------------------------------------------------- /open/core/betterself/views/ingredient_composition_views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from open.core.betterself.models.ingredient_composition import IngredientComposition 4 | from open.core.betterself.serializers.ingredient_composition_serializers import ( 5 | IngredientCompositionReadSerializer, 6 | IngredientCompositionCreateUpdateSerializer, 7 | ) 8 | from open.core.betterself.views.mixins import ( 9 | BaseGetUpdateDeleteView, 10 | BaseCreateListView, 11 | ) 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class IngredientCompositionCreateListView(BaseCreateListView): 17 | model_class = IngredientComposition 18 | read_serializer_class = IngredientCompositionReadSerializer 19 | create_serializer_class = IngredientCompositionCreateUpdateSerializer 20 | 21 | 22 | class IngredientCompositionGetUpdateView(BaseGetUpdateDeleteView): 23 | model_class = IngredientComposition 24 | read_serializer_class = IngredientCompositionReadSerializer 25 | update_serializer_class = IngredientCompositionCreateUpdateSerializer 26 | -------------------------------------------------------------------------------- /open/core/betterself/views/ingredient_views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from open.core.betterself.models.ingredient import Ingredient 4 | from open.core.betterself.serializers.ingredient_serializers import ( 5 | IngredientReadSerializer, 6 | IngredientCreateUpdateSerializer, 7 | ) 8 | from open.core.betterself.views.mixins import ( 9 | BaseGetUpdateDeleteView, 10 | BaseCreateListView, 11 | ) 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class IngredientCreateListView(BaseCreateListView): 17 | model_class = Ingredient 18 | read_serializer_class = IngredientReadSerializer 19 | create_serializer_class = IngredientCreateUpdateSerializer 20 | 21 | 22 | class IngredientGetUpdateView(BaseGetUpdateDeleteView): 23 | model_class = Ingredient 24 | read_serializer_class = IngredientReadSerializer 25 | update_serializer_class = IngredientCreateUpdateSerializer 26 | -------------------------------------------------------------------------------- /open/core/betterself/views/measurement.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | from rest_framework.views import APIView 3 | 4 | from open.core.betterself.models.measurement import Measurement 5 | from open.core.betterself.serializers.measurement_serializers import ( 6 | MeasurementReadSerializer, 7 | ) 8 | 9 | 10 | class MeasurementListView(APIView): 11 | model_class = Measurement 12 | read_serializer_class = MeasurementReadSerializer 13 | create_serializer_class = None 14 | update_serializer_class = None 15 | 16 | def get(self, request): 17 | # slightly different from other views because this doesn't filter on user 18 | instances = self.model_class.objects.all() 19 | serializer = self.read_serializer_class(instances, many=True) 20 | data = serializer.data 21 | return Response(data=data) 22 | -------------------------------------------------------------------------------- /open/core/betterself/views/overview_views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | from rest_framework.views import APIView 3 | 4 | from open.core.betterself.utilities.history_overview_utilities import ( 5 | get_overview_supplements_data, 6 | get_overview_productivity_data, 7 | get_overview_sleep_data, 8 | ) 9 | from open.core.betterself.utilities.user_date_utilities import ( 10 | serialize_date_to_user_localized_datetime, 11 | serialize_end_date_to_user_localized_datetime, 12 | ) 13 | 14 | 15 | class OverviewView(APIView): 16 | def get(self, request, start_date, end_date): 17 | user = request.user 18 | start_period = serialize_date_to_user_localized_datetime(start_date, user) 19 | end_period = serialize_end_date_to_user_localized_datetime(end_date, user) 20 | 21 | sleep_data = get_overview_sleep_data( 22 | user, start_period=start_period, end_period=end_period 23 | ) 24 | supplements_data = get_overview_supplements_data( 25 | user=user, start_period=start_period, end_period=end_period 26 | ) 27 | 28 | productivity_data = get_overview_productivity_data( 29 | user=user, start_period=start_period, end_period=end_period 30 | ) 31 | 32 | response = { 33 | # change it back to date, so it doesn't look super confusing on api response ... 34 | "start_period": start_period.date().isoformat(), 35 | "end_period": end_period.date().isoformat(), 36 | "sleep": sleep_data, 37 | "supplements": supplements_data, 38 | "productivity": productivity_data, 39 | } 40 | 41 | return Response(response) 42 | -------------------------------------------------------------------------------- /open/core/betterself/views/sleep_log_views.py: -------------------------------------------------------------------------------- 1 | from open.core.betterself.models.sleep_log import SleepLog 2 | from open.core.betterself.serializers.sleep_log_serializers import ( 3 | SleepLogReadSerializer, 4 | SleepLogCreateUpdateSerializer, 5 | ) 6 | from open.core.betterself.views.mixins import ( 7 | BaseGetUpdateDeleteView, 8 | BaseCreateListView, 9 | ) 10 | 11 | 12 | class SleepLogCreateListView(BaseCreateListView): 13 | model_class = SleepLog 14 | read_serializer_class = SleepLogReadSerializer 15 | create_serializer_class = SleepLogCreateUpdateSerializer 16 | 17 | 18 | class SleepLogGetUpdateView(BaseGetUpdateDeleteView): 19 | model_class = SleepLog 20 | read_serializer_class = SleepLogReadSerializer 21 | update_serializer_class = SleepLogCreateUpdateSerializer 22 | -------------------------------------------------------------------------------- /open/core/betterself/views/supplement_log_views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from rest_framework.response import Response 4 | 5 | from open.core.betterself.models.supplement_log import SupplementLog 6 | from open.core.betterself.serializers.supplement_log_serializers import ( 7 | SupplementLogReadSerializer, 8 | SupplementLogCreateUpdateSerializer, 9 | ) 10 | from open.core.betterself.views.mixins import ( 11 | BaseCreateListView, 12 | BaseGetUpdateDeleteView, 13 | ) 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class SupplementLogCreateListView(BaseCreateListView): 19 | model_class = SupplementLog 20 | read_serializer_class = SupplementLogReadSerializer 21 | create_serializer_class = SupplementLogCreateUpdateSerializer 22 | 23 | def post(self, request): 24 | context = {"request": request} 25 | serializer = self.create_serializer_class(data=request.data, context=context) 26 | 27 | serializer.is_valid(raise_exception=True) 28 | 29 | # normal operation, proceed as normal 30 | if "supplement" in serializer.validated_data: 31 | instance = serializer.save() 32 | serialized_data = self.read_serializer_class(instance).data 33 | else: 34 | # supplement_stack - do some non-elegant stuff 35 | instances = serializer.save() 36 | serialized_data = self.read_serializer_class(instances, many=True).data 37 | 38 | return Response(serialized_data) 39 | 40 | 41 | class SupplementLogGetUpdateView(BaseGetUpdateDeleteView): 42 | model_class = SupplementLog 43 | read_serializer_class = SupplementLogReadSerializer 44 | update_serializer_class = SupplementLogCreateUpdateSerializer 45 | -------------------------------------------------------------------------------- /open/core/betterself/views/supplement_stack_composition_views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from open.core.betterself.models.supplement_stack_composition import ( 4 | SupplementStackComposition, 5 | ) 6 | from open.core.betterself.serializers.supplement_stack_composition_serializers import ( 7 | SupplementStackCompositionReadSerializer, 8 | SupplementStackCompositionCreateUpdateSerializer, 9 | ) 10 | from open.core.betterself.views.mixins import ( 11 | BaseGetUpdateDeleteView, 12 | BaseCreateListView, 13 | ) 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class SupplementStackCompositionCreateListView(BaseCreateListView): 19 | model_class = SupplementStackComposition 20 | read_serializer_class = SupplementStackCompositionReadSerializer 21 | create_serializer_class = SupplementStackCompositionCreateUpdateSerializer 22 | 23 | 24 | class SupplementStackCompositionGetUpdateView(BaseGetUpdateDeleteView): 25 | model_class = SupplementStackComposition 26 | read_serializer_class = SupplementStackCompositionReadSerializer 27 | update_serializer_class = SupplementStackCompositionCreateUpdateSerializer 28 | -------------------------------------------------------------------------------- /open/core/betterself/views/supplement_stack_views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from open.core.betterself.models.supplement_stack import SupplementStack 4 | from open.core.betterself.serializers.supplement_stack_serializers import ( 5 | SupplementStackReadSerializer, 6 | SupplementStackCreateUpdateSerializer, 7 | ) 8 | from open.core.betterself.views.mixins import ( 9 | BaseGetUpdateDeleteView, 10 | BaseCreateListView, 11 | ) 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class SupplementStackCreateListView(BaseCreateListView): 17 | model_class = SupplementStack 18 | read_serializer_class = SupplementStackReadSerializer 19 | create_serializer_class = SupplementStackCreateUpdateSerializer 20 | 21 | 22 | class SupplementStackGetUpdateView(BaseGetUpdateDeleteView): 23 | model_class = SupplementStack 24 | read_serializer_class = SupplementStackReadSerializer 25 | update_serializer_class = SupplementStackCreateUpdateSerializer 26 | -------------------------------------------------------------------------------- /open/core/betterself/views/supplement_views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from open.core.betterself.models.supplement import Supplement 4 | from open.core.betterself.serializers.supplement_serializers import ( 5 | SupplementReadSerializer, 6 | SupplementCreateUpdateSerializer, 7 | ) 8 | from open.core.betterself.views.mixins import ( 9 | BaseGetUpdateDeleteView, 10 | BaseCreateListView, 11 | ) 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class SupplementCreateListView(BaseCreateListView): 17 | model_class = Supplement 18 | read_serializer_class = SupplementReadSerializer 19 | create_serializer_class = SupplementCreateUpdateSerializer 20 | 21 | 22 | class SupplementGetUpdateView(BaseGetUpdateDeleteView): 23 | model_class = Supplement 24 | read_serializer_class = SupplementReadSerializer 25 | update_serializer_class = SupplementCreateUpdateSerializer 26 | -------------------------------------------------------------------------------- /open/core/betterself/views/well_being_log_views.py: -------------------------------------------------------------------------------- 1 | from open.core.betterself.models.well_being_log import WellBeingLog 2 | from open.core.betterself.serializers.well_being_log_serializers import ( 3 | WellBeingLogReadSerializer, 4 | WellBeingLogCreateUpdateSerializer, 5 | ) 6 | from open.core.betterself.views.mixins import ( 7 | BaseGetUpdateDeleteView, 8 | BaseCreateListView, 9 | ) 10 | 11 | 12 | class WellBeingLogCreateListView(BaseCreateListView): 13 | model_class = WellBeingLog 14 | read_serializer_class = WellBeingLogReadSerializer 15 | create_serializer_class = WellBeingLogCreateUpdateSerializer 16 | 17 | 18 | class WellBeingLogGetUpdateView(BaseGetUpdateDeleteView): 19 | model_class = WellBeingLog 20 | read_serializer_class = WellBeingLogReadSerializer 21 | update_serializer_class = WellBeingLogCreateUpdateSerializer 22 | -------------------------------------------------------------------------------- /open/core/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/management/__init__.py -------------------------------------------------------------------------------- /open/core/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/management/commands/__init__.py -------------------------------------------------------------------------------- /open/core/migrations/0001_writeup_shared_prompt.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.2 on 2019-08-03 14:08 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | import model_utils.fields 6 | import uuid 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="WriteUpSharedPrompt", 18 | fields=[ 19 | ( 20 | "id", 21 | models.AutoField( 22 | auto_created=True, 23 | primary_key=True, 24 | serialize=False, 25 | verbose_name="ID", 26 | ), 27 | ), 28 | ( 29 | "modified", 30 | model_utils.fields.AutoLastModifiedField( 31 | default=django.utils.timezone.now, 32 | editable=False, 33 | verbose_name="modified", 34 | ), 35 | ), 36 | ( 37 | "created", 38 | models.DateTimeField( 39 | blank=True, default=django.utils.timezone.now, editable=False 40 | ), 41 | ), 42 | ( 43 | "uuid", 44 | models.UUIDField(default=uuid.uuid4, editable=False, unique=True), 45 | ), 46 | ("text", models.TextField(blank=True, default="")), 47 | ("email", models.TextField(blank=True, default="")), 48 | ("title", models.TextField(blank=True, default="")), 49 | ], 50 | options={"abstract": False}, 51 | ) 52 | ] 53 | -------------------------------------------------------------------------------- /open/core/migrations/0003_add_score_to_prompt_model.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-08-10 06:24 2 | 3 | from django.db import migrations, models 4 | import django_fsm 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("core", "0002_writeup_prompt_models_refactored")] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name="writeupflaggedprompt", 14 | options={"verbose_name": "Write Up Flagged Prompt"}, 15 | ), 16 | migrations.AlterModelOptions( 17 | name="writeupprompt", options={"verbose_name": "Write Up Prompt"} 18 | ), 19 | migrations.AlterModelOptions( 20 | name="writeuppromptvote", options={"verbose_name": "Write Up Prompt Vote"} 21 | ), 22 | migrations.AddField( 23 | model_name="writeupprompt", 24 | name="score", 25 | field=models.IntegerField(default=0), 26 | ), 27 | migrations.AlterField( 28 | model_name="writeupprompt", 29 | name="share_state", 30 | field=django_fsm.FSMField( 31 | choices=[ 32 | ("unshared", "Unshared"), 33 | ("published_link_access_only", "Link Access Only"), 34 | ("published", "Published"), 35 | ], 36 | default="unshared", 37 | max_length=50, 38 | ), 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /open/core/migrations/0004_writeupprompt_add_content.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-08-20 06:34 2 | 3 | from django.db import migrations 4 | import django_extensions.db.fields.json 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("core", "0003_add_score_to_prompt_model")] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="writeupprompt", 14 | name="content", 15 | field=django_extensions.db.fields.json.JSONField(blank=True, default=dict), 16 | ) 17 | ] 18 | -------------------------------------------------------------------------------- /open/core/migrations/0006_betterself_basic_models_v2.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-07-06 00:58 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("core", "0005_add_betterself_models_part_1"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="ingredient", 16 | name="name", 17 | field=models.CharField(default="", max_length=255), 18 | ), 19 | migrations.AlterField( 20 | model_name="ingredientcomposition", 21 | name="measurement", 22 | field=models.ForeignKey( 23 | on_delete=django.db.models.deletion.CASCADE, to="core.Measurement" 24 | ), 25 | ), 26 | migrations.AlterField( 27 | model_name="ingredientcomposition", 28 | name="quantity", 29 | field=models.DecimalField(blank=True, decimal_places=4, max_digits=10), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /open/core/migrations/0008_rename_supp_stack_and_compositions.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-07-06 12:32 2 | 3 | from django.conf import settings 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 11 | ("core", "0007_create_betterself_models_p2"), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameModel( 16 | old_name="UserSupplementStack", new_name="SupplementStack", 17 | ), 18 | migrations.RenameModel( 19 | old_name="UserSupplementStackComposition", 20 | new_name="SupplementStackComposition", 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /open/core/migrations/0009_add_measurement_name_uniqueness.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-07-08 00:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("core", "0008_rename_supp_stack_and_compositions"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="measurement", 15 | name="name", 16 | field=models.CharField(max_length=100, unique=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /open/core/migrations/0010_data_migration_add_demo_user.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | import logging 4 | 5 | from django.contrib.auth.hashers import make_password 6 | from django.db import migrations 7 | 8 | from open.core.betterself.constants import DEMO_TESTING_ACCOUNT 9 | 10 | logger = logging.getLogger() 11 | 12 | """ 13 | This whole data migration was a fail. 14 | """ 15 | 16 | 17 | def load_data(apps, schema_editor): 18 | User = apps.get_model("users", "User") 19 | 20 | # don't worry, i changed the password after the datamigration! 21 | adjusted_password = make_password(DEMO_TESTING_ACCOUNT) 22 | 23 | user, _ = User.objects.get_or_create( 24 | username=DEMO_TESTING_ACCOUNT, 25 | password=adjusted_password, 26 | email=DEMO_TESTING_ACCOUNT, 27 | ) 28 | # label = f"Created {user}" 29 | 30 | 31 | class Migration(migrations.Migration): 32 | dependencies = [ 33 | ("core", "0009_add_measurement_name_uniqueness"), 34 | ] 35 | 36 | operations = [migrations.RunPython(load_data)] 37 | -------------------------------------------------------------------------------- /open/core/migrations/0011_auto_20200711_2029.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-07-12 00:29 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("core", "0010_data_migration_add_demo_user"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="wellbeinglog", 15 | name="mental_value", 16 | field=models.PositiveSmallIntegerField(blank=True, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name="wellbeinglog", 20 | name="physical_value", 21 | field=models.PositiveSmallIntegerField(blank=True, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /open/core/migrations/0012_data_migration_force_api_tokens.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import binascii 3 | import logging 4 | import os 5 | 6 | from django.db import migrations 7 | 8 | from open.users.utilities import create_user_api_tokens 9 | 10 | logger = logging.getLogger() 11 | 12 | 13 | def load_data(apps, schema_editor): 14 | User = apps.get_model("users", "User") 15 | users = User.objects.all() 16 | 17 | Token = apps.get_model("authtoken", "Token") 18 | Token.objects.all().delete() 19 | 20 | for user in users: 21 | key = binascii.hexlify(os.urandom(20)).decode() 22 | Token.objects.get_or_create(user=user, key=key) 23 | 24 | 25 | class Migration(migrations.Migration): 26 | dependencies = [ 27 | ("core", "0011_auto_20200711_2029"), 28 | ] 29 | 30 | operations = [migrations.RunPython(load_data)] 31 | -------------------------------------------------------------------------------- /open/core/migrations/0013_fix_demo_fixture.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | import logging 4 | 5 | from django.contrib.auth.hashers import make_password 6 | from django.db import migrations 7 | 8 | from open.core.betterself.constants import DEMO_TESTING_ACCOUNT 9 | 10 | logger = logging.getLogger() 11 | 12 | 13 | def load_data(apps, schema_editor): 14 | User = apps.get_model("users", "User") 15 | adjusted_password = make_password(DEMO_TESTING_ACCOUNT) 16 | 17 | demo_user, _ = User.objects.get_or_create(username=DEMO_TESTING_ACCOUNT) 18 | demo_user.password = adjusted_password 19 | demo_user.save() 20 | 21 | 22 | class Migration(migrations.Migration): 23 | dependencies = [ 24 | ("core", "0012_data_migration_force_api_tokens"), 25 | ] 26 | 27 | operations = [migrations.RunPython(load_data)] 28 | -------------------------------------------------------------------------------- /open/core/migrations/0014_auto_20200713_1147.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-07-13 15:47 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("core", "0013_fix_demo_fixture"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="activity", 15 | options={"ordering": ["name"], "verbose_name_plural": "Activities"}, 16 | ), 17 | migrations.AlterModelOptions( 18 | name="activitylog", 19 | options={ 20 | "ordering": ["user", "-time"], 21 | "verbose_name": "Activity Log", 22 | "verbose_name_plural": "Activity Logs", 23 | }, 24 | ), 25 | migrations.AlterModelOptions( 26 | name="supplementstackcomposition", 27 | options={"verbose_name": "Supplement Stack Composition"}, 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /open/core/migrations/0015_add_pomodoro_count_to_productivity_log.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-07-28 20:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("core", "0014_auto_20200713_1147"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="dailyproductivitylog", 15 | name="pomodoro_count", 16 | field=models.PositiveIntegerField(blank=True, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name="activity", 20 | name="notes", 21 | field=models.TextField(blank=True, default=""), 22 | ), 23 | migrations.AlterField( 24 | model_name="activitylog", 25 | name="notes", 26 | field=models.TextField(blank=True, default=""), 27 | ), 28 | migrations.AlterField( 29 | model_name="dailyproductivitylog", 30 | name="notes", 31 | field=models.TextField(blank=True, default=""), 32 | ), 33 | migrations.AlterField( 34 | model_name="ingredient", 35 | name="notes", 36 | field=models.TextField(blank=True, default=""), 37 | ), 38 | migrations.AlterField( 39 | model_name="ingredientcomposition", 40 | name="notes", 41 | field=models.TextField(blank=True, default=""), 42 | ), 43 | migrations.AlterField( 44 | model_name="sleeplog", 45 | name="notes", 46 | field=models.TextField(blank=True, default=""), 47 | ), 48 | migrations.AlterField( 49 | model_name="supplement", 50 | name="notes", 51 | field=models.TextField(blank=True, default=""), 52 | ), 53 | migrations.AlterField( 54 | model_name="supplementlog", 55 | name="notes", 56 | field=models.TextField(blank=True, default=""), 57 | ), 58 | migrations.AlterField( 59 | model_name="supplementstack", 60 | name="notes", 61 | field=models.TextField(blank=True, default=""), 62 | ), 63 | migrations.AlterField( 64 | model_name="supplementstackcomposition", 65 | name="notes", 66 | field=models.TextField(blank=True, default=""), 67 | ), 68 | migrations.AlterField( 69 | model_name="wellbeinglog", 70 | name="notes", 71 | field=models.TextField(blank=True, default=""), 72 | ), 73 | ] 74 | -------------------------------------------------------------------------------- /open/core/migrations/0016_null_duration_minutes_activity_log.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-08-01 20:52 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("core", "0015_add_pomodoro_count_to_productivity_log"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="activitylog", 15 | name="duration_minutes", 16 | field=models.IntegerField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /open/core/migrations/0017_supplement_is_taken_with_food.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-08-02 01:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("core", "0016_null_duration_minutes_activity_log"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="supplement", 15 | name="is_taken_with_food", 16 | field=models.BooleanField(blank=True, default=None, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /open/core/migrations/0019_dailyproductivitylog_mistakes.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-08-12 13:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("core", "0018_food_foodlog"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="dailyproductivitylog", 15 | name="mistakes", 16 | field=models.TextField(blank=True, default=""), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /open/core/migrations/0020_supplementlog_duration_minutes.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-08-19 15:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("core", "0019_dailyproductivitylog_mistakes"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="supplementlog", 15 | name="duration_minutes", 16 | field=models.PositiveIntegerField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /open/core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/migrations/__init__.py -------------------------------------------------------------------------------- /open/core/models.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import open.core.writeup.models 3 | import open.core.betterself.models 4 | -------------------------------------------------------------------------------- /open/core/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/scripts/__init__.py -------------------------------------------------------------------------------- /open/core/scripts/betterself_create_demo_fixtures.py: -------------------------------------------------------------------------------- 1 | """ 2 | dpy runscript betterself_create_demo_fixtures 3 | """ 4 | from open.core.betterself.constants import DEMO_TESTING_ACCOUNT 5 | from open.core.betterself.utilities.demo_user_factory_fixtures import ( 6 | create_demo_fixtures_for_user, 7 | ) 8 | from open.users.models import User 9 | 10 | 11 | def run(): 12 | user = User.objects.get(username=DEMO_TESTING_ACCOUNT) 13 | create_demo_fixtures_for_user(user) 14 | -------------------------------------------------------------------------------- /open/core/scripts/betterself_import_historical_productivity.py: -------------------------------------------------------------------------------- 1 | # a file for you to import your legacy productivity files 2 | from datetime import date as datetime_date 3 | 4 | import requests 5 | from django.conf import settings 6 | import pandas as pd 7 | 8 | """ 9 | dpy runscript betterself_import_historical_productivity 10 | """ 11 | 12 | 13 | def run(): 14 | url = "https://open.senrigan.io/api/betterself/v2/productivity_logs/" 15 | 16 | df = pd.read_excel( 17 | "snapshots/personal_historical_pomodoros.xlsx", keep_default_na="" 18 | ) 19 | token_key = f"Token {settings.BETTERSELF_PERSONAL_API_KEY}" 20 | headers = {"Authorization": token_key} 21 | 22 | df.index = df["Date"] 23 | df = df.sort_index() 24 | 25 | start_import_period = datetime_date(2020, 7, 9) 26 | end_import_period = datetime_date(2020, 7, 28) 27 | 28 | for index, row in df.iterrows(): 29 | date = row["Date"].date().isoformat() 30 | 31 | should_import = start_import_period <= index.date() <= end_import_period 32 | if not should_import: 33 | continue 34 | 35 | notes = row["Description Of Day"] 36 | mistakes = row["Mistakes"] 37 | pomodoro_count = row["Pomodoros"] 38 | 39 | if not pomodoro_count: 40 | continue 41 | 42 | data = { 43 | "pomodoro_count": pomodoro_count, 44 | "date": date, 45 | "notes": notes, 46 | "mistakes": mistakes, 47 | } 48 | 49 | response = requests.post(url, json=data, headers=headers) 50 | if response.status_code != 200: 51 | print(response.content) 52 | assert response.status_code == 200 53 | -------------------------------------------------------------------------------- /open/core/scripts/betterself_prototyping.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | """ 3 | dpy runscript betterself_prototyping 4 | """ 5 | 6 | import pkgutil 7 | from django.conf import settings 8 | 9 | import logging 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def run(): 15 | logger.info("Prototyping") 16 | # used for prototyping 17 | return 18 | -------------------------------------------------------------------------------- /open/core/scripts/betterself_reset_password.py: -------------------------------------------------------------------------------- 1 | from rest_framework.reverse import reverse 2 | from rest_framework.test import APIClient 3 | 4 | from open.users.models import User 5 | 6 | """ 7 | dpy runscript betterself_reset_password 8 | """ 9 | 10 | 11 | def run(): 12 | user = User.objects.get(email="jeff@senrigan.io") 13 | url = reverse("rest_password_reset") 14 | 15 | client = APIClient() 16 | client.force_login(user) 17 | 18 | data = {"email": user.email} 19 | 20 | response = client.post(url, data=data) 21 | -------------------------------------------------------------------------------- /open/core/scripts/clear_redis_cache.py: -------------------------------------------------------------------------------- 1 | from django.core.cache import cache 2 | 3 | """ 4 | dpy runscript clear_redis_cache 5 | """ 6 | 7 | 8 | def run(): 9 | # because native django clear cache does not work with django-redis :( 10 | cache.delete_pattern("*") 11 | -------------------------------------------------------------------------------- /open/core/scripts/cloudflare_extract_historical_traffic.py: -------------------------------------------------------------------------------- 1 | from open.core.utilities.cloudflare import get_dashboard_history 2 | 3 | 4 | def run(): 5 | """ 6 | To run 7 | python manage.py runscript cloudflare_extract_historical_traffic 8 | """ 9 | get_dashboard_history() 10 | -------------------------------------------------------------------------------- /open/core/scripts/writeup_debug_end_of_text_not_serialized.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import requests 3 | from django.conf import settings 4 | 5 | from open.core.writeup.utilities.text_algo_serializers import ( 6 | serialize_text_algo_api_response 7 | ) 8 | 9 | """ 10 | dpy runscript writeup_debug_end_of_text_not_serialized 11 | """ 12 | 13 | 14 | def run(): 15 | data = { 16 | "prompt": "We know that this is an important moment in the history of our country he said. We are proud to support the work of the American Civil Liberties Union and we will continue to fight for the rights of all Americans. The ACLU is currently suing the government over its surveillance programs. The ACLU said it had been monitoring the surveillance program since September 2011.", 17 | "temperature": 1, 18 | "api_key": settings.ML_SERVICE_ENDPOINT_API_KEY, 19 | "length": 20, 20 | "top_k": 30, 21 | } 22 | 23 | response = requests.post(settings.GPT2_MEDIUM_API_ENDPOINT, data=data) 24 | returned_data = response.json() 25 | serialized_text_responses = serialize_text_algo_api_response(returned_data) 26 | -------------------------------------------------------------------------------- /open/core/scripts/writeup_mock_ws_listeners.py: -------------------------------------------------------------------------------- 1 | import json 2 | import ssl 3 | 4 | from websocket import create_connection 5 | 6 | """ 7 | dpy runscript writeup_mock_ws_listeners 8 | """ 9 | 10 | 11 | def run(): 12 | ws = create_connection( 13 | "wss://open.senrigan.io/ws/async/writeup/text_generation/session/test/", 14 | sslopt={"cert_reqs": ssl.CERT_NONE}, 15 | timeout=15, 16 | ) 17 | 18 | valid_data = {"text0": "Yep", "prompt": "Yep", "message_type": "new_request"} 19 | to_send = json.dumps(valid_data) 20 | ws.send(to_send) 21 | 22 | result2 = ws.recv() 23 | print(result2) 24 | ws.close() 25 | -------------------------------------------------------------------------------- /open/core/scripts/writeup_profile_prompt_generate_view.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import json 3 | import time 4 | 5 | import requests 6 | from django.conf import settings 7 | from websocket import create_connection 8 | 9 | from open.core.scripts.swarm_ml_services import get_random_prompt 10 | from open.core.writeup.constants import TEXT_GENERATION_URL 11 | 12 | """ 13 | this script's design was to compare performance behind django channels 14 | and how much overhead it added versus directly hitting the microservice 15 | 16 | output: 17 | 18 | 1.8620352506637574 was the average time in seconds to run. 19 | 1.8132854890823364 was the average time in seconds to run directly. 20 | 21 | amazingly enough, django channels ... has almost zero overhead wow. 22 | """ 23 | 24 | 25 | def run(): 26 | # dpy runscript writeup_profile_prompt_generate_view 27 | url = f"wss://open.senrigan.io/ws/async/writeup/{TEXT_GENERATION_URL}/session/a-cool-test-session/" 28 | ws = create_connection(url) 29 | 30 | start = time.time() 31 | 32 | intervals = 50 33 | for _ in range(intervals): 34 | data = get_random_prompt() 35 | ws_msg = json.dumps(data) 36 | ws.send(ws_msg) 37 | result = ws.recv() 38 | 39 | end = time.time() 40 | 41 | websocket_difference = end - start 42 | print(f"{websocket_difference/intervals} was the average time in seconds to run.") 43 | 44 | url = settings.GPT2_MEDUM_API_ENDPOINT 45 | token_key = f"Token {settings.ML_SERVICE_ENDPOINT_API_KEY}" 46 | headers = {"Authorization": token_key} 47 | 48 | api_start = time.time() 49 | for _ in range(intervals): 50 | data = get_random_prompt() 51 | response = requests.post(url, json=data, headers=headers) 52 | assert response.status_code == 200 53 | 54 | api_end = time.time() 55 | 56 | api_difference = api_end - api_start 57 | print( 58 | f"{api_difference / intervals} was the average time in seconds to run directly." 59 | ) 60 | -------------------------------------------------------------------------------- /open/core/scripts/writeup_profile_serializers.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from django.utils.lorem_ipsum import COMMON_P 4 | 5 | from open.core.writeup.serializers import TextAlgorithmPromptSerializer 6 | 7 | """ 8 | dpy runscript writeup_time_serializers 9 | 10 | ended up taking 2.59 milliseconds, so not worth the refactoring 11 | """ 12 | 13 | 14 | def run(): 15 | data = { 16 | "text": "Today I Saw A Village, what if i had a lot of text" + COMMON_P, 17 | "temperature": 1, 18 | "top_k": 20, 19 | } 20 | 21 | start = time.time() 22 | serializer = TextAlgorithmPromptSerializer(data=data) 23 | serializer.is_valid() 24 | end = time.time() 25 | 26 | difference = end - start 27 | 28 | print(difference) 29 | -------------------------------------------------------------------------------- /open/core/tasks.py: -------------------------------------------------------------------------------- 1 | from config.celery_app import app 2 | from django.conf import settings 3 | import logging 4 | import requests 5 | 6 | from config.constants import PRODUCTION 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | @app.task(serializer="json") 12 | def check_services_running(): 13 | if settings.ENVIRONMENT != PRODUCTION: 14 | return 15 | 16 | post_data = { 17 | "prompt": "We know that this is an important moment in the history of our country he said. We are proud to support the work of the American Civil Liberties Union and we will continue to fight for the rights of all Americans. The ACLU is currently suing the government over its surveillance programs. The ACLU said it had been monitoring the surveillance program since September 2011.", 18 | "temperature": 1, 19 | "api_key": settings.ML_SERVICE_ENDPOINT_API_KEY, 20 | "length": 20, 21 | "top_k": 30, 22 | } 23 | 24 | endpoints = [ 25 | settings.GPT2_MEDIUM_API_ENDPOINT, 26 | # settings.GPT2_LARGE_API_ENDPOINT, 27 | settings.GPT2_MEDIUM_HP_API_ENDPOINT, 28 | # settings.GPT2_MEDIUM_LEGAL_API_ENDPOINT, 29 | settings.GPT2_MEDIUM_RESEARCH_API_ENDPOINT, 30 | settings.GPT2_MEDIUM_COMPANIES_API_ENDPOINT, 31 | ] 32 | 33 | for url in endpoints: 34 | response = requests.post(url, json=post_data) 35 | data = response.json() 36 | 37 | assert response.status_code == 200 38 | # make sure it returns text_4 39 | assert len(data["text_4"]) > 30 40 | 41 | 42 | @app.task(serializer="json") 43 | def reset_betterself_demo_fixtures(): 44 | from open.users.models import User 45 | 46 | from open.core.betterself.constants import DEMO_TESTING_ACCOUNT 47 | from open.core.betterself.utilities.demo_user_factory_fixtures import ( 48 | create_demo_fixtures_for_user, 49 | ) 50 | 51 | user = User.objects.get(username=DEMO_TESTING_ACCOUNT) 52 | create_demo_fixtures_for_user(user) 53 | -------------------------------------------------------------------------------- /open/core/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/tests/__init__.py -------------------------------------------------------------------------------- /open/core/tests/test_urls_configured.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth import get_user_model 3 | from django.test import TestCase 4 | from django.urls import URLPattern, URLResolver 5 | 6 | User = get_user_model() 7 | 8 | """ 9 | python manage.py test --pattern="*test_urls_configured.py" 10 | """ 11 | 12 | urlconf = __import__(settings.ROOT_URLCONF, {}, {}, [""]) 13 | 14 | 15 | def list_urls(lis, acc=None): 16 | # ripped from stackoverflow 17 | if acc is None: 18 | acc = [] 19 | if not lis: 20 | return 21 | data = lis[0] 22 | if isinstance(data, URLPattern): 23 | yield acc + [str(data.pattern)] 24 | elif isinstance(data, URLResolver): 25 | yield from list_urls(data.url_patterns, acc + [str(data.pattern)]) 26 | yield from list_urls(lis[1:], acc) 27 | 28 | 29 | class TestAPIPostfixSlash(TestCase): 30 | """ 31 | django will automatically postfix requests with a trailing slash from API requests 32 | ie. if someone makes a request on api/resource, django will search the urls for api/resource/ 33 | 34 | to support this, always add a trailing slash at urls 35 | 36 | otherwise, you can have some weird browser issues with safari refusing to make requests 37 | """ 38 | 39 | def test_api_urls_ends_with_slash(self): 40 | 41 | for url_pattern in list_urls(urlconf.urlpatterns): 42 | """ 43 | returns similar to below 44 | 45 | ['admin/', 'core/writeupflaggedprompt/', '/'] 46 | ['api/writeup/v1/', 'generated_sentence/'] 47 | """ 48 | url_pattern_serialized = "".join(url_pattern) 49 | 50 | if not url_pattern_serialized: 51 | continue 52 | 53 | is_api_endpoint = url_pattern_serialized[:3] == "api" 54 | ends_with_forward_slash = url_pattern_serialized[-1] == "/" 55 | 56 | if is_api_endpoint: 57 | if not ends_with_forward_slash: 58 | raise ValueError(f"{url_pattern_serialized} does not end with a /!") 59 | -------------------------------------------------------------------------------- /open/core/utilities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/utilities/__init__.py -------------------------------------------------------------------------------- /open/core/utilities/cloudflare.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import requests 3 | from django.conf import settings 4 | 5 | start_date_default = "2019-05-04T00:00:00Z" 6 | end_date_default = "2019-05-11T00:00:00Z" 7 | 8 | CLOUDFLARE_HEADERS = { 9 | "X-Auth-Email": settings.CLOUDFLARE_EMAIL, 10 | "X-Auth-Key": settings.CLOUDFLARE_API_KEY, 11 | } 12 | 13 | CLOUDFLARE_PREFIX = "https://api.cloudflare.com/client/v4/zones" 14 | 15 | 16 | def get_dashboard_history(start_date=start_date_default, end_date=end_date_default): 17 | cf_url = ( 18 | f"{CLOUDFLARE_PREFIX}/{settings.CLOUDFLARE_SENRIGAN_ZONE_ID}/analytics/dashboard?since={start_date}&until={end_date}&" # noqa: E501 19 | ) 20 | response = requests.get(cf_url, headers=CLOUDFLARE_HEADERS) 21 | 22 | data = response.json() 23 | 24 | unique_values = [] 25 | unique_dates = [] 26 | 27 | timeseries = data["result"]["timeseries"] 28 | for daily_snapshot in timeseries: 29 | 30 | date_start = daily_snapshot["since"] 31 | uniques = daily_snapshot["uniques"] 32 | 33 | uniques_all = uniques["all"] 34 | 35 | # create two lists holding the dates and values to load into a pd.Series 36 | unique_dates.append(date_start) 37 | unique_values.append(uniques_all) 38 | 39 | series = pd.Series(data=unique_values, index=unique_dates) 40 | series.to_excel("exports/cloudflare_export_may.xlsx") 41 | 42 | return series 43 | -------------------------------------------------------------------------------- /open/core/writeup/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/writeup/__init__.py -------------------------------------------------------------------------------- /open/core/writeup/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from open.core.writeup.models import ( 4 | WriteUpPrompt, 5 | WriteUpPromptVote, 6 | WriteUpFlaggedPrompt, 7 | ) 8 | 9 | 10 | @admin.register(WriteUpPrompt) 11 | class WriteUpPromptAdmin(admin.ModelAdmin): 12 | list_display = ( 13 | "uuid", 14 | # "text", 15 | "title", 16 | "user", 17 | "email", 18 | "instagram", 19 | "twitter", 20 | "website", 21 | "share_state", 22 | "staff_verified_share_state", 23 | "score", 24 | ) 25 | list_filter = ("user",) 26 | 27 | 28 | @admin.register(WriteUpPromptVote) 29 | class WriteUpPromptVoteAdmin(admin.ModelAdmin): 30 | list_display = ("uuid", "prompt", "user", "value") 31 | list_filter = ("prompt", "user") 32 | 33 | 34 | @admin.register(WriteUpFlaggedPrompt) 35 | class WriteUpFlaggedPromptAdmin(admin.ModelAdmin): 36 | list_display = ("id", "modified", "uuid", "prompt", "user") 37 | list_filter = ("prompt", "user") 38 | -------------------------------------------------------------------------------- /open/core/writeup/caches.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from open.core.writeup.constants import MLModelNames, ENGLISH_CONSTANT 4 | 5 | 6 | def get_cache_key_for_text_algo_parameter( 7 | prompt, 8 | batch_size, 9 | length, 10 | temperature, 11 | top_k, 12 | top_p, 13 | language=ENGLISH_CONSTANT, 14 | model_name=MLModelNames.GPT2_MEDIUM, 15 | ): 16 | app = "writeup" 17 | 18 | # have to encode into bytes first 19 | # just do this for redis's memory not to take up too much space 20 | # there's no speed difference in using shorter keys though, redis is too good 21 | prompt_encoded = prompt.strip().encode("utf-8") 22 | prompt_hash = hashlib.md5(prompt_encoded).hexdigest() 23 | 24 | cache_key = f"{app}_{prompt_hash}_{batch_size}_{length}_{temperature}_{top_k}_{top_p}_{language}_{model_name}" 25 | return cache_key 26 | 27 | 28 | def get_cache_key_for_processing_algo_parameter(cache_key): 29 | # need a prefix, otherwise that's be cache collusion with data 30 | return f"processing_{cache_key}" 31 | -------------------------------------------------------------------------------- /open/core/writeup/factories.py: -------------------------------------------------------------------------------- 1 | from factory import SubFactory 2 | from factory.django import DjangoModelFactory 3 | from factory import Faker 4 | from factory.fuzzy import FuzzyInteger 5 | 6 | from open.core.writeup.models import ( 7 | WriteUpPrompt, 8 | WriteUpPromptVote, 9 | WriteUpFlaggedPrompt, 10 | ) 11 | 12 | 13 | class WriteUpPromptFactory(DjangoModelFactory): 14 | email = Faker("email") 15 | text = Faker("text") 16 | instagram = Faker("first_name") 17 | twitter = Faker("first_name") 18 | score = FuzzyInteger(1, 1000) 19 | 20 | class Meta: 21 | model = WriteUpPrompt 22 | 23 | 24 | class WriteUpPromptVoteFactory(DjangoModelFactory): 25 | prompt = SubFactory(WriteUpPromptFactory) 26 | 27 | class Meta: 28 | model = WriteUpPromptVote 29 | 30 | 31 | class WriteUpFlaggedPromptFactory(DjangoModelFactory): 32 | prompt = SubFactory(WriteUpPromptFactory) 33 | 34 | class Meta: 35 | model = WriteUpFlaggedPrompt 36 | -------------------------------------------------------------------------------- /open/core/writeup/models.py: -------------------------------------------------------------------------------- 1 | from django.db.models import TextField, ForeignKey, SET_NULL, CASCADE, IntegerField 2 | from django_extensions.db.fields.json import JSONField 3 | from django_fsm import FSMField 4 | 5 | from open.core.writeup.constants import ( 6 | STAFF_VERIFIED_SHARE_STATE_CHOICES, 7 | StaffVerifiedShareStates, 8 | PROMPT_SHARE_STATES_CHOICES, 9 | PromptShareStates, 10 | ) 11 | from open.users.models import User 12 | from open.utilities.models import BaseModel 13 | 14 | 15 | class WriteUpPrompt(BaseModel): 16 | text = TextField(default="", blank=True) 17 | # store the content in a jsonfield that will allow the frontend to preserve 18 | # formatting from slatejs 19 | content = JSONField(blank=True) 20 | title = TextField(default="", blank=True) 21 | user = ForeignKey(User, null=True, blank=True, on_delete=SET_NULL) 22 | # published/sharing options 23 | # let the writers/composers be famous, if they want other people 24 | # to contact them for good writing, allow them to put in their details 25 | email = TextField(default="", blank=True) 26 | instagram = TextField(default="", blank=True) 27 | twitter = TextField(default="", blank=True) 28 | website = TextField(default="", blank=True) 29 | # default to unshared, but allow people to share otherwise 30 | share_state = FSMField( 31 | choices=PROMPT_SHARE_STATES_CHOICES, default=PromptShareStates.UNSHARED 32 | ) 33 | # catch bad/mean prompts that shouldn't be shown for things on the public list 34 | staff_verified_share_state = FSMField( 35 | choices=STAFF_VERIFIED_SHARE_STATE_CHOICES, 36 | default=StaffVerifiedShareStates.UNVERIFIED, 37 | ) 38 | # this is a denormalized field that is the sum of all the upvotes 39 | # this serves as a quick queryable field, where it's also easy to easy 40 | # to recompute for all prompts when cpu is idle 41 | score = IntegerField(default=0) 42 | 43 | class Meta: 44 | verbose_name = "Write Up Prompt" 45 | 46 | def __str__(self): 47 | return f"{self.title} - {self.id}" 48 | 49 | 50 | class WriteUpPromptVote(BaseModel): 51 | prompt = ForeignKey(WriteUpPrompt, on_delete=CASCADE) 52 | user = ForeignKey(User, null=True, blank=True, on_delete=CASCADE) 53 | # values can be negative (up to -1), which means a downvote 54 | value = IntegerField(default=1) 55 | 56 | class Meta: 57 | unique_together = ("prompt", "user") 58 | verbose_name = "Write Up Prompt Vote" 59 | 60 | 61 | class WriteUpFlaggedPrompt(BaseModel): 62 | # allow prompts that are public to be flagged 63 | # save who flagged it to prevent abuse 64 | prompt = ForeignKey(WriteUpPrompt, null=False, on_delete=CASCADE) 65 | user = ForeignKey(User, null=True, blank=True, on_delete=CASCADE) 66 | 67 | class Meta: 68 | unique_together = ("prompt", "user") 69 | verbose_name = "Write Up Flagged Prompt" 70 | -------------------------------------------------------------------------------- /open/core/writeup/routing.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from open.core.writeup import consumers 3 | 4 | # can't actually test this yet without pytest 5 | 6 | # these url patterns are prefixed with a /ws/ for load balancers to redirect traffic 7 | websocket_urlpatterns = [ 8 | url( 9 | r"^ws/async/writeup/text_generation/session/(?P[^/]+)/$", 10 | consumers.AsyncWriteUpGPT2MediumConsumer, 11 | ), 12 | # this is a a quick test url when you need to restart the ml backend servers 13 | url( 14 | r"^ws/test/writeup/text_generation/session/(?P[^/]+)/$", 15 | consumers.WriteUpGPT2MediumConsumerMock, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /open/core/writeup/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from rest_framework.fields import IntegerField, UUIDField 3 | 4 | from open.core.writeup.constants import ML_MODEL_NAME_CHOICES, MLModelNames 5 | from open.core.writeup.models import ( 6 | WriteUpPrompt, 7 | WriteUpFlaggedPrompt, 8 | WriteUpPromptVote, 9 | ) 10 | 11 | 12 | class TextAlgorithmPromptSerializer(serializers.Serializer): 13 | # warning! 14 | # copy and pasted from service repo, the only modifications to this should be one way 15 | # synced from other repo 16 | prompt = serializers.CharField(required=True) 17 | batch_size = serializers.IntegerField(default=5, max_value=10, min_value=1) 18 | # cap off at 256 for now ... the frontend doesn't let someone put more than 50, 19 | # but it wouldn't be rocket science to reverse this endpoint considering the repo is oss lol 20 | length = serializers.IntegerField(default=40, max_value=256, min_value=1) 21 | temperature = serializers.FloatField(default=.7, min_value=0.1, max_value=1) 22 | top_k = serializers.IntegerField(default=10, max_value=40, min_value=0) 23 | # keep off for the time being ... 24 | top_p = serializers.FloatField(default=0, max_value=1, min_value=0) 25 | model_name = serializers.ChoiceField( 26 | choices=ML_MODEL_NAME_CHOICES, default=MLModelNames.GPT2_MEDIUM 27 | ) 28 | 29 | 30 | class WriteUpPromptCreateReadSerializer(serializers.ModelSerializer): 31 | """ 32 | PROMPTS 33 | """ 34 | 35 | score = IntegerField(read_only=True) 36 | 37 | class Meta: 38 | model = WriteUpPrompt 39 | fields = ( 40 | "text", 41 | "content", 42 | "email", 43 | "title", 44 | "uuid", 45 | "instagram", 46 | "twitter", 47 | "website", 48 | "share_state", 49 | "score", 50 | "created", 51 | ) 52 | 53 | 54 | class WriteUpPromptVoteModifySerializer(serializers.ModelSerializer): 55 | """ 56 | VOTES 57 | """ 58 | 59 | value = IntegerField(min_value=-1, max_value=3, default=1) 60 | 61 | class Meta: 62 | model = WriteUpPromptVote 63 | fields = ("value",) 64 | 65 | 66 | class WriteUpFlaggedPromptModifySerializer(serializers.ModelSerializer): 67 | """ 68 | FLAGGING 69 | """ 70 | 71 | prompt_uuid = UUIDField(required=True) 72 | 73 | class Meta: 74 | model = WriteUpFlaggedPrompt 75 | fields = ("prompt_uuid",) 76 | -------------------------------------------------------------------------------- /open/core/writeup/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/writeup/tests/__init__.py -------------------------------------------------------------------------------- /open/core/writeup/tests/test_caches.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from open.core.writeup.caches import get_cache_key_for_text_algo_parameter 4 | from open.core.writeup.serializers import TextAlgorithmPromptSerializer 5 | 6 | 7 | class TestWriteUpCaches(TestCase): 8 | def test_cache_key(self): 9 | prompt = "Testing A Cache Key\n" 10 | batch_size = 5 11 | length = 10 12 | temperature = 0.7 13 | top_k = 40 14 | top_p = 0.5 15 | 16 | cache_key = get_cache_key_for_text_algo_parameter( 17 | prompt=prompt, 18 | batch_size=batch_size, 19 | length=length, 20 | top_k=top_k, 21 | top_p=top_p, 22 | temperature=temperature, 23 | ) 24 | expected_result = f"writeup_6a8d5bebad2295c438c3c26c28813f80_5_10_0.7_40_0.5_english_gpt2-medium" 25 | 26 | self.assertEqual(cache_key, expected_result) 27 | 28 | def test_cache_key_with_new_line(self): 29 | prompt = "Testing A Cache Key\n" 30 | batch_size = 5 31 | length = 10 32 | temperature = 0.7 33 | top_k = 40 34 | top_p = 0.5 35 | 36 | cache_key = get_cache_key_for_text_algo_parameter( 37 | prompt, batch_size, length, temperature, top_k, top_p 38 | ) 39 | expected_result = f"writeup_6a8d5bebad2295c438c3c26c28813f80_5_10_0.7_40_0.5_english_gpt2-medium" 40 | 41 | self.assertEqual(cache_key, expected_result) 42 | 43 | def test_cache_key_with_space(self): 44 | prompt = "Testing A Cache Key\n " 45 | batch_size = 5 46 | length = 10 47 | temperature = 0.7 48 | top_k = 40 49 | top_p = 0.5 50 | 51 | cache_key = get_cache_key_for_text_algo_parameter( 52 | prompt, batch_size, length, temperature, top_k, top_p 53 | ) 54 | expected_result = f"writeup_6a8d5bebad2295c438c3c26c28813f80_5_10_0.7_40_0.5_english_gpt2-medium" 55 | 56 | self.assertEqual(cache_key, expected_result) 57 | 58 | def test_cache_key_with_serializer(self): 59 | post_message = {"prompt": "Hello"} 60 | 61 | serializer = TextAlgorithmPromptSerializer(data=post_message) 62 | serializer.is_valid(raise_exception=False) 63 | 64 | cache_key = get_cache_key_for_text_algo_parameter(**serializer.validated_data) 65 | 66 | expected_cache_key = f"writeup_8b1a9953c4611296a827abf8c47804d7_5_40_0.7_10_0_english_gpt2-medium" 67 | self.assertEqual(cache_key, expected_cache_key) 68 | -------------------------------------------------------------------------------- /open/core/writeup/tests/test_factories.py: -------------------------------------------------------------------------------- 1 | from test_plus import TestCase 2 | 3 | from open.core.writeup.factories import ( 4 | WriteUpPromptFactory, 5 | WriteUpPromptVoteFactory, 6 | WriteUpFlaggedPromptFactory, 7 | ) 8 | 9 | 10 | class TestFactoryMixin: 11 | def test_factory(self): 12 | instance = self.FACTORY() 13 | self.assertIsNotNone(instance) 14 | 15 | 16 | class TestWriteUpPromptFactories(TestCase, TestFactoryMixin): 17 | FACTORY = WriteUpPromptFactory 18 | 19 | def test_score_calculated(self): 20 | instance = self.FACTORY() 21 | self.assertTrue(instance.score != 0) 22 | 23 | def test_writeup_prompt_factory_loaded(self): 24 | text = "Jeff Was Here" 25 | email = "Jeff Was Here" 26 | title = "Jeff Was Here" 27 | 28 | writeup_prompt = WriteUpPromptFactory(text=text, email=email, title=title) 29 | 30 | self.assertEqual(writeup_prompt.text, text) 31 | self.assertEqual(writeup_prompt.email, text) 32 | self.assertEqual(writeup_prompt.title, text) 33 | 34 | 35 | class TestWriteUpPromptVoteFactory(TestCase, TestFactoryMixin): 36 | FACTORY = WriteUpPromptVoteFactory 37 | 38 | 39 | class TestWriteUpFlaggedPromptFactory(TestCase): 40 | FACTORY = WriteUpFlaggedPromptFactory 41 | -------------------------------------------------------------------------------- /open/core/writeup/tests/test_serializers.py: -------------------------------------------------------------------------------- 1 | from test_plus import TestCase 2 | 3 | from open.core.writeup.serializers import ( 4 | TextAlgorithmPromptSerializer, 5 | WriteUpPromptCreateReadSerializer, 6 | ) 7 | 8 | 9 | class WriteupPromptSerializerTests(TestCase): 10 | def test_serializer_returns_invalid_on_empty_prompts(self): 11 | data = {"prompt": "", "temperature": 1} 12 | 13 | serializer = TextAlgorithmPromptSerializer(data=data) 14 | valid = serializer.is_valid() 15 | 16 | self.assertFalse(valid) 17 | 18 | 19 | class WriteUpPromptSerializerTests(TestCase): 20 | def test_serializer_works(self): 21 | text = "I am so hungry for Shake Shack right now" 22 | data = {"text": text, "email": text, "title": text} 23 | 24 | serializer = WriteUpPromptCreateReadSerializer(data=data) 25 | valid = serializer.is_valid() 26 | 27 | self.assertTrue(valid) 28 | -------------------------------------------------------------------------------- /open/core/writeup/tests/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/writeup/tests/views/__init__.py -------------------------------------------------------------------------------- /open/core/writeup/tests/views/test_flagged_prompt_views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.reverse import reverse 2 | from rest_framework.test import APIClient 3 | from test_plus import TestCase 4 | 5 | from open.core.writeup.constants import WriteUpResourceEndpoints 6 | from open.core.writeup.factories import ( 7 | WriteUpPromptFactory, 8 | WriteUpFlaggedPromptFactory, 9 | ) 10 | from open.core.writeup.models import WriteUpFlaggedPrompt 11 | from open.users.factories import UserFactory 12 | from open.users.models import User 13 | from open.utilities.testing import generate_random_uuid_as_string 14 | 15 | 16 | class WriteUpFlaggedPromptViewTests(TestCase): 17 | VIEW_NAME = WriteUpResourceEndpoints.PROMPT_FLAGS 18 | 19 | @classmethod 20 | def setUpTestData(cls): 21 | registered_user = UserFactory(is_staff=False) 22 | cls.registered_user_id = registered_user.id 23 | 24 | staff_user = UserFactory(is_staff=True) 25 | cls.staff_user_id = staff_user.id 26 | 27 | def setUp(self): 28 | self.unregistered_user_client = APIClient() 29 | 30 | self.registered_user = User.objects.get(id=self.registered_user_id) 31 | self.registered_user_client = APIClient() 32 | self.registered_user_client.force_login(self.registered_user) 33 | 34 | self.staff_user = UserFactory(is_staff=True) 35 | self.staff_user_client = APIClient() 36 | self.staff_user_client.force_login(self.staff_user) 37 | 38 | def test_view(self): 39 | prompt = WriteUpPromptFactory() 40 | data_kwargs = {"prompt_uuid": prompt.uuid_str} 41 | url = reverse(self.VIEW_NAME, kwargs=data_kwargs) 42 | 43 | response = self.registered_user_client.post(url) 44 | self.assertEqual(response.status_code, 200) 45 | 46 | def test_post_view_multiple_times_only_results_in_one(self): 47 | prompt = WriteUpPromptFactory() 48 | data_kwargs = {"prompt_uuid": prompt.uuid_str} 49 | url = reverse(self.VIEW_NAME, kwargs=data_kwargs) 50 | 51 | for _ in range(3): 52 | self.registered_user_client.post(url) 53 | 54 | instance_count = WriteUpFlaggedPrompt.objects.filter( 55 | user=self.registered_user, prompt=prompt 56 | ).count() 57 | self.assertEqual(instance_count, 1) 58 | 59 | def test_view_delete(self): 60 | prompt = WriteUpPromptFactory() 61 | WriteUpFlaggedPromptFactory(user=self.registered_user, prompt=prompt) 62 | 63 | data_kwargs = {"prompt_uuid": prompt.uuid_str} 64 | url = reverse(self.VIEW_NAME, kwargs=data_kwargs) 65 | 66 | response = self.registered_user_client.delete(url) 67 | self.assertEqual(response.status_code, 204) 68 | 69 | def test_view_delete_doesnt_exist(self): 70 | data_kwargs = {"prompt_uuid": generate_random_uuid_as_string()} 71 | url = reverse(self.VIEW_NAME, kwargs=data_kwargs) 72 | 73 | response = self.registered_user_client.delete(url) 74 | self.assertEqual(response.status_code, 404) 75 | -------------------------------------------------------------------------------- /open/core/writeup/tests/views/test_gpt2_debug_view.py: -------------------------------------------------------------------------------- 1 | from open.core.writeup.constants import WriteUpResourceEndpoints 2 | from open.utilities.testing_mixins import OpenDefaultTest 3 | 4 | 5 | class GPT2MediumPromptDebugViewTests(OpenDefaultTest): 6 | VIEW_NAME = WriteUpResourceEndpoints.GENERATED_SENTENCE 7 | VIEW_NEEDS_LOGIN = False 8 | 9 | def test_get_view(self): 10 | response = self._get_response_data() 11 | self.assertTrue("prompt" in response) 12 | -------------------------------------------------------------------------------- /open/core/writeup/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from open.core.writeup.constants import WriteUpResourceEndpoints 4 | from open.core.writeup.views import ( 5 | GPT2MediumPromptDebugView, 6 | WriteUpPromptView, 7 | WriteUpPromptVoteView, 8 | WriteUpFlaggedPromptView, 9 | WriteUpPromptListCreateView, 10 | ) 11 | 12 | urlpatterns = [ 13 | path( 14 | f"{WriteUpResourceEndpoints.GENERATED_SENTENCE}/", 15 | view=GPT2MediumPromptDebugView.as_view(), 16 | name=WriteUpResourceEndpoints.GENERATED_SENTENCE, 17 | ), 18 | path( 19 | r"prompts/", 20 | WriteUpPromptListCreateView.as_view(), 21 | name=WriteUpResourceEndpoints.PROMPTS, 22 | ), 23 | path( 24 | # frontend urls are like 25 | # writeup.ai/prompts/:uuid/ 26 | r"prompts//", 27 | WriteUpPromptView.as_view(), 28 | name=WriteUpResourceEndpoints.PROMPTS, 29 | ), 30 | path( 31 | r"prompts//votes/", 32 | WriteUpPromptVoteView.as_view(), 33 | name=WriteUpResourceEndpoints.PROMPT_VOTES, 34 | ), 35 | path( 36 | r"prompts//flags/", 37 | WriteUpFlaggedPromptView.as_view(), 38 | name=WriteUpResourceEndpoints.PROMPT_FLAGS, 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /open/core/writeup/utilities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/core/writeup/utilities/__init__.py -------------------------------------------------------------------------------- /open/core/writeup/utilities/access_permissions.py: -------------------------------------------------------------------------------- 1 | from open.core.writeup.constants import PromptShareStates, StaffVerifiedShareStates 2 | 3 | 4 | def user_can_read_prompt_instance(user, prompt): 5 | if prompt.user == user: 6 | return True 7 | 8 | # if this is marked as some type of bad content, no one should see 9 | if prompt.staff_verified_share_state in [ 10 | StaffVerifiedShareStates.VERIFIED_FAIL, 11 | StaffVerifiedShareStates.UNVERIFIED_ISSUE_MULTIPLE, 12 | ]: 13 | return False 14 | 15 | if prompt.share_state in [ 16 | PromptShareStates.PUBLISHED, 17 | PromptShareStates.PUBLISHED_LINK_ACCESS_ONLY, 18 | ]: 19 | return True 20 | 21 | return False 22 | -------------------------------------------------------------------------------- /open/core/writeup/utilities/text_algo_serializers.py: -------------------------------------------------------------------------------- 1 | from asgiref.sync import sync_to_async 2 | 3 | from open.core.writeup.constants import GPT2_END_TEXT_STRING, TransformerXLNetTokenTypes 4 | 5 | 6 | def serialize_text_algo_individual_values(text): 7 | """ Remove end of line and any other particular oddities you discover """ 8 | text_serialized = text.strip() 9 | 10 | if GPT2_END_TEXT_STRING in text_serialized: 11 | end_location = text_serialized.index(GPT2_END_TEXT_STRING) 12 | text_serialized = text_serialized[:end_location] 13 | if TransformerXLNetTokenTypes.BEGINNING_OF_PROMPT in text_serialized: 14 | location = text_serialized.index(TransformerXLNetTokenTypes.BEGINNING_OF_PROMPT) 15 | text_serialized = text_serialized[:location] 16 | if TransformerXLNetTokenTypes.ENDING_OF_PROMPT in text_serialized: 17 | location = text_serialized.index(TransformerXLNetTokenTypes.ENDING_OF_PROMPT) 18 | text_serialized = text_serialized[:location] 19 | 20 | if TransformerXLNetTokenTypes.ENDING_OF_PARAGRAPH in text_serialized: 21 | text_serialized = text_serialized.replace( 22 | TransformerXLNetTokenTypes.ENDING_OF_PARAGRAPH, "\n\n" 23 | ) 24 | 25 | if TransformerXLNetTokenTypes.UNKNOWN_TOKEN in text_serialized: 26 | text_serialized = text_serialized.replace( 27 | TransformerXLNetTokenTypes.UNKNOWN_TOKEN, "" 28 | ) 29 | 30 | return text_serialized 31 | 32 | 33 | def serialize_text_algo_api_response_sync(returned_data): 34 | """ Was A Hack To Debug Async """ 35 | # TODO - I think you should be able to merge this back into one function 36 | text_responses = returned_data.copy() 37 | for key, value in returned_data.items(): 38 | if "text_" not in key: 39 | continue 40 | 41 | value_serialized = serialize_text_algo_individual_values(value) 42 | text_responses[key] = value_serialized 43 | 44 | return text_responses 45 | 46 | 47 | @sync_to_async 48 | def serialize_text_algo_api_response(returned_data): 49 | return serialize_text_algo_api_response_sync(returned_data) 50 | -------------------------------------------------------------------------------- /open/routing.py: -------------------------------------------------------------------------------- 1 | from channels.auth import AuthMiddlewareStack 2 | from channels.routing import ProtocolTypeRouter, URLRouter 3 | 4 | from open.core.writeup.routing import websocket_urlpatterns 5 | 6 | application = ProtocolTypeRouter( 7 | {"websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns))} 8 | ) 9 | -------------------------------------------------------------------------------- /open/static/css/project.css: -------------------------------------------------------------------------------- 1 | /* These styles are generated from project.scss. */ 2 | 3 | .alert-debug { 4 | color: black; 5 | background-color: white; 6 | border-color: #d6e9c6; 7 | } 8 | 9 | .alert-error { 10 | color: #b94a48; 11 | background-color: #f2dede; 12 | border-color: #eed3d7; 13 | } 14 | -------------------------------------------------------------------------------- /open/static/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/static/fonts/.gitkeep -------------------------------------------------------------------------------- /open/static/images/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/static/images/favicons/favicon.ico -------------------------------------------------------------------------------- /open/static/images/favicons/gatsby-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/static/images/favicons/gatsby-icon.png -------------------------------------------------------------------------------- /open/static/js/project.js: -------------------------------------------------------------------------------- 1 | /* Project specific Javascript goes here. */ 2 | -------------------------------------------------------------------------------- /open/static/sass/custom_bootstrap_vars.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/static/sass/custom_bootstrap_vars.scss -------------------------------------------------------------------------------- /open/static/sass/project.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | // project specific CSS goes here 6 | 7 | //////////////////////////////// 8 | //Variables// 9 | //////////////////////////////// 10 | 11 | // Alert colors 12 | 13 | $white: #fff; 14 | $mint-green: #d6e9c6; 15 | $black: #000; 16 | $pink: #f2dede; 17 | $dark-pink: #eed3d7; 18 | $red: #b94a48; 19 | 20 | //////////////////////////////// 21 | //Alerts// 22 | //////////////////////////////// 23 | 24 | // bootstrap alert CSS, translated to the django-standard levels of 25 | // debug, info, success, warning, error 26 | 27 | .alert-debug { 28 | background-color: $white; 29 | border-color: $mint-green; 30 | color: $black; 31 | } 32 | 33 | .alert-error { 34 | background-color: $pink; 35 | border-color: $dark-pink; 36 | color: $red; 37 | } 38 | -------------------------------------------------------------------------------- /open/templates/403_csrf.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Forbidden (403){% endblock %} 4 | 5 | {% block content %} 6 |

Forbidden (403)

7 | 8 |

CSRF verification failed. Request aborted.

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /open/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Page not found{% endblock %} 4 | 5 | {% block content %} 6 |

Page not found

7 | 8 |

This is not the page you were looking for.

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /open/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Server Error{% endblock %} 4 | 5 | {% block content %} 6 |

Ooops!!! 500

7 | 8 |

Looks like something went wrong!

9 | 10 |

We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing.

11 | {% endblock content %} 12 | 13 | 14 | -------------------------------------------------------------------------------- /open/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static i18n %} 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /open/templates/writeup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chat Rooms! 6 | 7 | 8 | What chat room would you like to enter?
9 |
10 | 11 | 12 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /open/templates/writeup/room.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chat Room 6 | 7 | 8 |
9 |
10 | 11 | 12 | 49 | 50 | -------------------------------------------------------------------------------- /open/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/tests/__init__.py -------------------------------------------------------------------------------- /open/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/users/__init__.py -------------------------------------------------------------------------------- /open/users/adapters.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from allauth.account.adapter import DefaultAccountAdapter 4 | from allauth.socialaccount.adapter import DefaultSocialAccountAdapter 5 | from django.conf import settings 6 | from django.http import HttpRequest 7 | 8 | 9 | class AccountAdapter(DefaultAccountAdapter): 10 | def is_open_for_signup(self, request: HttpRequest): 11 | return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) 12 | 13 | 14 | class SocialAccountAdapter(DefaultSocialAccountAdapter): 15 | def is_open_for_signup(self, request: HttpRequest, sociallogin: Any): 16 | return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) 17 | -------------------------------------------------------------------------------- /open/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth import admin as auth_admin 3 | from django.contrib.auth import get_user_model 4 | from rest_framework.authtoken.models import Token 5 | 6 | from open.core.betterself.utilities.demo_user_factory_fixtures import ( 7 | create_demo_fixtures_for_user, 8 | ) 9 | from open.users.forms import UserChangeForm, UserCreationForm 10 | 11 | User = get_user_model() 12 | 13 | 14 | admin.site.site_header = "Open Control Panel" 15 | 16 | 17 | def create_demo_fixtures(modeladmin, request, queryset): 18 | for instance in queryset: 19 | create_demo_fixtures_for_user(instance) 20 | 21 | 22 | create_demo_fixtures.short_description = "Create Demo Fixtures" 23 | 24 | 25 | class TokenInline(admin.TabularInline): 26 | model = Token 27 | 28 | 29 | @admin.register(User) 30 | class UserAdmin(auth_admin.UserAdmin): 31 | form = UserChangeForm 32 | add_form = UserCreationForm 33 | fieldsets = ( 34 | ("User", {"fields": ("name", "signed_up_from")}), 35 | ) + auth_admin.UserAdmin.fieldsets 36 | list_display = [ 37 | "username", 38 | "email", 39 | "signed_up_from", 40 | "created", 41 | "modified", 42 | ] 43 | search_fields = ["name", "email"] 44 | inlines = [TokenInline] 45 | actions = [create_demo_fixtures] 46 | ordering = ["-id"] 47 | -------------------------------------------------------------------------------- /open/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class UsersConfig(AppConfig): 6 | name = "open.users" 7 | verbose_name = _("Users") 8 | 9 | def ready(self): 10 | try: 11 | import open.users.signals # noqa F401 12 | except ImportError: 13 | pass 14 | -------------------------------------------------------------------------------- /open/users/constants.py: -------------------------------------------------------------------------------- 1 | class UserResourceNames: 2 | USER_REDIRECT = "redirect" 3 | DETAILS = "details" 4 | DELETE_USER_CONFIRMED = "delete_user_confirmed" 5 | -------------------------------------------------------------------------------- /open/users/factories.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Sequence 2 | 3 | from django.contrib.auth import get_user_model 4 | from factory import DjangoModelFactory, Faker, post_generation 5 | 6 | 7 | class UserFactory(DjangoModelFactory): 8 | 9 | username = Faker("user_name") 10 | email = Faker("email") 11 | name = Faker("name") 12 | timezone_string = "US/Eastern" 13 | 14 | @post_generation 15 | def password(self, create: bool, extracted: Sequence[Any], **kwargs): 16 | password = Faker( 17 | "password", 18 | length=42, 19 | special_chars=True, 20 | digits=True, 21 | upper_case=True, 22 | lower_case=True, 23 | ).generate(extra_kwargs={}) 24 | self.set_password(password) 25 | 26 | class Meta: 27 | model = get_user_model() 28 | django_get_or_create = ["username"] 29 | -------------------------------------------------------------------------------- /open/users/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model, forms 2 | from django.core.exceptions import ValidationError 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | User = get_user_model() 6 | 7 | 8 | class UserChangeForm(forms.UserChangeForm): 9 | class Meta(forms.UserChangeForm.Meta): 10 | model = User 11 | 12 | 13 | class UserCreationForm(forms.UserCreationForm): 14 | 15 | error_message = forms.UserCreationForm.error_messages.update( 16 | {"duplicate_username": _("This username has already been taken.")} 17 | ) 18 | 19 | class Meta(forms.UserCreationForm.Meta): 20 | model = User 21 | 22 | def clean_username(self): 23 | username = self.cleaned_data["username"] 24 | 25 | try: 26 | User.objects.get(username=username) 27 | except User.DoesNotExist: 28 | return username 29 | 30 | raise ValidationError(self.error_messages["duplicate_username"]) 31 | -------------------------------------------------------------------------------- /open/users/migrations/0002_user_uuid.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.2 on 2019-07-12 23:05 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("users", "0001_initial")] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="user", 14 | name="uuid", 15 | field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True), 16 | ) 17 | ] 18 | -------------------------------------------------------------------------------- /open/users/migrations/0003_add_user_signup_fields.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2020-07-05 01:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("users", "0002_user_uuid"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="user", 15 | name="is_betterself_user", 16 | field=models.BooleanField(default=False), 17 | ), 18 | migrations.AddField( 19 | model_name="user", 20 | name="is_writeup_user", 21 | field=models.BooleanField(default=True), 22 | ), 23 | migrations.AddField( 24 | model_name="user", 25 | name="signed_up_from", 26 | field=models.CharField( 27 | blank=True, 28 | choices=[ 29 | ("", "N/A"), 30 | ("writeup", "WriteUp"), 31 | ("betterself", "BetterSelf"), 32 | ], 33 | default="writeup", 34 | max_length=255, 35 | ), 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /open/users/migrations/0004_adjust_user_signup_defaults.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2020-07-05 02:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("users", "0003_add_user_signup_fields"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="user", 15 | name="is_writeup_user", 16 | field=models.BooleanField(default=False), 17 | ), 18 | migrations.AlterField( 19 | model_name="user", 20 | name="signed_up_from", 21 | field=models.CharField( 22 | blank=True, 23 | choices=[ 24 | ("", "N/A"), 25 | ("writeup", "WriteUp"), 26 | ("betterself", "BetterSelf"), 27 | ], 28 | default="", 29 | max_length=255, 30 | ), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /open/users/migrations/0005_add_created_modified_to_user.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-07-13 15:47 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | import model_utils.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("users", "0004_adjust_user_signup_defaults"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name="user", 17 | name="created", 18 | field=models.DateTimeField( 19 | blank=True, default=django.utils.timezone.now, editable=False 20 | ), 21 | ), 22 | migrations.AddField( 23 | model_name="user", 24 | name="modified", 25 | field=model_utils.fields.AutoLastModifiedField( 26 | default=django.utils.timezone.now, 27 | editable=False, 28 | verbose_name="modified", 29 | ), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /open/users/migrations/0006_auto_20200812_0936.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-08-12 13:36 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("users", "0005_add_created_modified_to_user"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions(name="user", options={"ordering": ["-id"]},), 14 | ] 15 | -------------------------------------------------------------------------------- /open/users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/users/migrations/__init__.py -------------------------------------------------------------------------------- /open/users/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.contrib.auth.models import AbstractUser 4 | from django.db.models import CharField, UUIDField, BooleanField, DateTimeField 5 | from django.utils import timezone 6 | from django.utils.translation import ugettext_lazy as _ 7 | from model_utils.fields import AutoLastModifiedField 8 | from rest_framework.authtoken.models import Token 9 | 10 | from open.constants import SIGNED_UP_FROM_DETAILS_CHOICE 11 | 12 | import pytz 13 | 14 | TIMEZONES = tuple(zip(pytz.all_timezones, pytz.all_timezones)) 15 | 16 | 17 | class User(AbstractUser): 18 | # First Name and Last Name do not cover name patterns around the globe. 19 | name = CharField(_("Name of User"), blank=True, max_length=255) 20 | uuid = UUIDField(primary_key=False, default=uuid.uuid4, editable=False, unique=True) 21 | 22 | # used to determine where a user signed up from 23 | signed_up_from = CharField( 24 | choices=SIGNED_UP_FROM_DETAILS_CHOICE, default="", blank=True, max_length=255 25 | ) 26 | 27 | # for specific features that are specific to an app 28 | is_writeup_user = BooleanField(default=False) 29 | is_betterself_user = BooleanField(default=False) 30 | 31 | # doing this lets you insert records and modify the timestamps of created 32 | created = DateTimeField(default=timezone.now, editable=False, blank=True) 33 | modified = AutoLastModifiedField(_("modified")) 34 | 35 | # have it stored as a string here, then use a property to grab the timezones 36 | timezone_string = CharField(max_length=32, choices=TIMEZONES, default="US/Eastern") 37 | 38 | class Meta: 39 | ordering = ["-id"] 40 | 41 | @property 42 | def timezone(self): 43 | return pytz.timezone(self.timezone_string) 44 | 45 | def save(self, *args, **kwargs): 46 | needs_api_key = False 47 | if not self.pk: 48 | needs_api_key = True 49 | 50 | super().save(*args, **kwargs) 51 | 52 | if needs_api_key: 53 | Token.objects.get_or_create(user=self) 54 | -------------------------------------------------------------------------------- /open/users/tasks.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | 3 | from config import celery_app 4 | 5 | User = get_user_model() 6 | 7 | 8 | @celery_app.task() 9 | def get_users_count(): 10 | """A pointless Celery task to demonstrate usage.""" 11 | return User.objects.count() 12 | -------------------------------------------------------------------------------- /open/users/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/users/tests/__init__.py -------------------------------------------------------------------------------- /open/users/tests/test_factories.py: -------------------------------------------------------------------------------- 1 | from test_plus import TestCase 2 | 3 | from open.users.factories import UserFactory 4 | 5 | 6 | class TestUserFactories(TestCase): 7 | def test_user_factory(self): 8 | email = "test@gmail.com" 9 | user = UserFactory(email=email) 10 | self.assertEqual(user.email, email) 11 | -------------------------------------------------------------------------------- /open/users/tests/test_tasks.py: -------------------------------------------------------------------------------- 1 | from celery.result import EagerResult 2 | from django.test import TestCase 3 | 4 | from open.users.factories import UserFactory 5 | from open.users.models import User 6 | 7 | from open.users.tasks import get_users_count 8 | 9 | 10 | class TestUserCeleryTasks(TestCase): 11 | def test_user_count(self): 12 | User.objects.all().delete() 13 | """A basic test to execute the get_users_count Celery task.""" 14 | UserFactory.create_batch(3) 15 | 16 | task_result = get_users_count.delay() 17 | 18 | self.assertIsInstance(task_result, EagerResult) 19 | self.assertEqual(task_result.result, 3) 20 | -------------------------------------------------------------------------------- /open/users/tests/test_user_details_view.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from test_plus import TestCase 3 | from django.urls import reverse 4 | from rest_framework.test import APIClient 5 | 6 | from open.users.factories import UserFactory 7 | 8 | User = get_user_model() 9 | 10 | """ 11 | python manage.py test --pattern="*test_user_details_view.py" --keepdb 12 | """ 13 | 14 | 15 | class TestUserDetailsView(TestCase): 16 | @classmethod 17 | def setUpTestData(cls): 18 | user_1 = UserFactory() 19 | user_2 = UserFactory() 20 | 21 | cls.user_1_id = user_1.id 22 | cls.user_2_id = user_2.id 23 | 24 | super().setUpTestData() 25 | 26 | def setUp(self): 27 | self.user_1 = User.objects.get(id=self.user_1_id) 28 | self.user_2 = User.objects.get(id=self.user_2_id) 29 | 30 | # a user that owns the instance 31 | self.client_1 = APIClient() 32 | self.client_1.force_login(self.user_1) 33 | 34 | # a user that shouldn't have access to the instance 35 | self.client_2 = APIClient() 36 | self.client_2.force_login(self.user_2) 37 | 38 | super().setUp() 39 | 40 | def test_view(self): 41 | url = reverse("users:details") 42 | 43 | response = self.client_1.get(url) 44 | data = response.data 45 | 46 | self.assertEqual(data["username"], self.user_1.username) 47 | -------------------------------------------------------------------------------- /open/users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from open.users.constants import UserResourceNames 4 | from open.users.views import EmptyView, UserDetailsView, UserDeleteView 5 | 6 | # i hate by default there's an app name here 7 | app_name = "users" 8 | 9 | urlpatterns = [ 10 | # used by django for some random redirects, they always need this populate, so just put a blank view 11 | path("~redirect/", view=EmptyView.as_view(), name=UserResourceNames.USER_REDIRECT), 12 | path("details/", view=UserDetailsView.as_view(), name=UserResourceNames.DETAILS), 13 | # welp, this makes me sad, but i guess it's final then, so long and thanks for the fishes 14 | # not technically rest, but paranoia makes me create a separate endpoint 15 | path( 16 | "delete_user_confirmed//", 17 | view=UserDeleteView.as_view(), 18 | name=UserResourceNames.DELETE_USER_CONFIRMED, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /open/users/utilities.py: -------------------------------------------------------------------------------- 1 | from open.users.models import User 2 | 3 | from rest_framework.authtoken.models import Token 4 | 5 | 6 | def create_user_api_tokens(): 7 | users = User.objects.all() 8 | for user in users: 9 | Token.objects.get_or_create(user=user) 10 | -------------------------------------------------------------------------------- /open/utilities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/utilities/__init__.py -------------------------------------------------------------------------------- /open/utilities/dataframes.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | 3 | 4 | def change_dataframe_nans_to_none(df): 5 | df = df.where(pd.notnull(df), None) 6 | return df 7 | -------------------------------------------------------------------------------- /open/utilities/fields.py: -------------------------------------------------------------------------------- 1 | from django.db.models import CharField 2 | 3 | # simplify charfield because i always have to think what to put for these parameters 4 | # whereas it's almost always the same/similar to below 5 | DEFAULT_MODELS_CHAR_FIELD = CharField( 6 | max_length=300, blank=False, default="", null=False 7 | ) 8 | 9 | 10 | def create_django_choice_tuple_from_list(list_a): 11 | if list_a is None: 12 | return () 13 | 14 | tuples_list = [] 15 | for item in list_a: 16 | if isinstance(item, str): 17 | tuple_item_title = item.title() 18 | else: 19 | tuple_item_title = item 20 | 21 | tuple_item = (item, tuple_item_title) 22 | tuples_list.append(tuple_item) 23 | 24 | return tuple(tuples_list) 25 | -------------------------------------------------------------------------------- /open/utilities/importing_models.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import pkgutil 3 | 4 | 5 | def import_submodules(package, recursive=True): 6 | 7 | """ Import all submodules of a module, recursively, including subpackages 8 | 9 | # Ripped from StackOverflow 10 | https://stackoverflow.com/questions/3365740/how-to-import-all-submodules/25083161#25083161 11 | 12 | :param package: package (name or actual module) 13 | :type package: str | module 14 | :rtype: dict[str, types.ModuleType] 15 | """ 16 | if isinstance(package, str): 17 | package = importlib.import_module(package) 18 | results = {} 19 | for loader, name, is_pkg in pkgutil.walk_packages(package.__path__): 20 | full_name = package.__name__ + "." + name 21 | results[full_name] = importlib.import_module(full_name) 22 | if recursive and is_pkg: 23 | results.update(import_submodules(full_name)) 24 | return results 25 | -------------------------------------------------------------------------------- /open/utilities/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.db.models import DateTimeField, UUIDField, ForeignKey, CASCADE, TextField 4 | from django.utils import timezone 5 | from model_utils.models import TimeStampedModel 6 | from rest_framework.reverse import reverse 7 | 8 | from open.users.models import User 9 | 10 | 11 | class BaseModel(TimeStampedModel): 12 | # doing this lets you insert records and modify the timestamps of created 13 | created = DateTimeField(default=timezone.now, editable=False, blank=True) 14 | # modified is inherited from TimeStampedModel 15 | uuid = UUIDField(primary_key=False, default=uuid.uuid4, editable=False, unique=True) 16 | 17 | class Meta: 18 | abstract = True 19 | 20 | def __repr__(self): 21 | has_name_attr = getattr(self, "name", "") 22 | if has_name_attr: 23 | label = f"ID {self.id} | {self._meta.verbose_name.title()} - {self.name}" 24 | else: 25 | label = f"ID {self.id} | {self._meta.verbose_name.title()}" 26 | 27 | return label 28 | 29 | def __str__(self): 30 | return self.__repr__() 31 | 32 | @property 33 | def uuid_str(self): 34 | # i frequently get the str of uuid a lot 35 | return self.uuid.__str__() 36 | 37 | 38 | class BaseModelWithUserGeneratedContent(BaseModel): 39 | user = ForeignKey(User, null=False, blank=False, on_delete=CASCADE) 40 | notes = TextField(default="", blank=True) 41 | 42 | class Meta: 43 | abstract = True 44 | ordering = ["-id"] 45 | 46 | def get_update_url(self): 47 | instance_uuid = str(self.uuid) 48 | kwargs = {"uuid": instance_uuid} 49 | update_url = reverse(self.RESOURCE_NAME, kwargs=kwargs) 50 | return update_url 51 | -------------------------------------------------------------------------------- /open/utilities/profilers.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffshek/open/71a97bbf420b6c982e3056ff21f86765c500f218/open/utilities/profilers.py -------------------------------------------------------------------------------- /open/utilities/testing.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from rest_framework.test import APIRequestFactory, force_authenticate 4 | 5 | 6 | def generate_random_uuid_as_string(): 7 | generated_uuid = uuid.uuid4() 8 | return generated_uuid.__str__() 9 | 10 | 11 | def get_instance_uuid_as_string(instance): 12 | # typing this always is super annoying 13 | return instance.uuid.__str__() 14 | 15 | 16 | def create_api_request_context(url, user, data): 17 | """ 18 | Sometimes useful if you need to skip using an APIClient and directly test with a request 19 | 20 | There's an edge case where some DRF Fields think they should be required, but passes in tests, but not in production 21 | """ 22 | factory = APIRequestFactory() 23 | request = factory.post(url, data) 24 | request.user = user 25 | force_authenticate(request, user=user) 26 | context = {"request": request} 27 | return context 28 | -------------------------------------------------------------------------------- /production.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | django: &django 5 | build: 6 | context: . 7 | dockerfile: ./compose/production/django/Dockerfile 8 | image: gcr.io/proven-record-242223/open:latest 9 | env_file: 10 | - ./credentials/open/production/django.txt 11 | command: /start.sh 12 | volumes: 13 | - .:/app 14 | depends_on: 15 | - cloud-sql-proxy 16 | 17 | traefik: 18 | build: 19 | context: . 20 | dockerfile: ./compose/production/traefik/Dockerfile 21 | image: gcr.io/proven-record-242223/open_traefik:latest 22 | depends_on: 23 | - django 24 | ports: 25 | # SSL termination happens at the LB layer, all traffic here is http 26 | - "0.0.0.0:80:80" 27 | 28 | cloud-sql-proxy: 29 | image: gcr.io/cloudsql-docker/gce-proxy:1.11 30 | command: > 31 | /cloud_sql_proxy -instances=proven-record-242223:us-east1:open-b522b437=tcp:0.0.0.0:5432 32 | -credential_file /root/.config/senrigan_gcp_sql_access.json 33 | volumes: 34 | - ./credentials/open/production/:/root/.config 35 | ports: 36 | - 5432:5432 37 | 38 | celeryworker: 39 | image: gcr.io/proven-record-242223/open:latest 40 | env_file: 41 | - ./credentials/open/production/django.txt 42 | volumes: 43 | - .:/app 44 | command: /start-celeryworker.sh 45 | depends_on: 46 | - cloud-sql-proxy 47 | 48 | -------------------------------------------------------------------------------- /production_primary.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | # this file is the exact same (or should be) as production.yml except it includes celerybeat 4 | services: 5 | django: &django 6 | build: 7 | context: . 8 | dockerfile: ./compose/production/django/Dockerfile 9 | image: gcr.io/proven-record-242223/open:latest 10 | env_file: 11 | - ./credentials/open/production/django.txt 12 | command: /start.sh 13 | depends_on: 14 | - cloud-sql-proxy 15 | volumes: 16 | - .:/app 17 | 18 | traefik: 19 | build: 20 | context: . 21 | dockerfile: ./compose/production/traefik/Dockerfile 22 | image: gcr.io/proven-record-242223/open_traefik:latest 23 | depends_on: 24 | - django 25 | ports: 26 | # SSL termination happens at the LB layer, all traffic here is http 27 | - "0.0.0.0:80:80" 28 | 29 | cloud-sql-proxy: 30 | image: gcr.io/cloudsql-docker/gce-proxy:1.11 31 | command: > 32 | /cloud_sql_proxy -instances=proven-record-242223:us-east1:open-b522b437=tcp:0.0.0.0:5432 33 | -credential_file /root/.config/senrigan_gcp_sql_access.json 34 | volumes: 35 | - ./credentials/open/production/:/root/.config 36 | ports: 37 | - 5432:5432 38 | 39 | celeryworker: 40 | image: gcr.io/proven-record-242223/open:latest 41 | env_file: 42 | - ./credentials/open/production/django.txt 43 | volumes: 44 | - .:/app 45 | command: /start-celeryworker.sh 46 | depends_on: 47 | - cloud-sql-proxy 48 | 49 | celerybeat: 50 | image: gcr.io/proven-record-242223/open:latest 51 | env_file: 52 | - ./credentials/open/production/django.txt 53 | volumes: 54 | - .:/app 55 | command: /start-celerybeat.sh 56 | depends_on: 57 | - cloud-sql-proxy 58 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | aiocontextvars==0.2.2 2 | aiohttp==3.6.2 3 | aioredis==1.2.0 4 | argon2-cffi==19.1.0 # https://github.com/hynek/argon2_cffi 5 | asgiref==3.2.10 6 | asn1crypto==1.3.0 7 | async-timeout==3.0.1 8 | autobahn==19.9.3 9 | Automat==0.7.0 10 | celery==4.3.0 # https://github.com/celery/celery 11 | channels==2.4.0 12 | channels-redis==2.4.0 13 | constantly==15.1.0 14 | contextvars==2.4 15 | coreapi==2.3.3 # https://github.com/core-api/python-client 16 | cryptography==2.7 17 | daphne==2.5.0 18 | django==2.2.13 # https://www.djangoproject.com/ 19 | django-allauth==0.42.0 # https://github.com/pennersr/django-allauth 20 | django-anymail[mailgun]==7.0.0 # https://github.com/anymail/django-anymail 21 | django-cors-headers==3.1.0 22 | django-crispy-forms==1.7.2 # https://github.com/django-crispy-forms/django-crispy-forms 23 | django-environ==0.4.5 # https://github.com/joke2k/django-environ 24 | django-filter==2.3.0 25 | django-fsm==2.7.0 26 | django-model-utils==4.0.0 # https://github.com/jazzband/django-model-utils 27 | django-redis==4.12.1 # https://github.com/niwinz/django-redis 28 | django-render-block==0.7 29 | django-rest-auth==0.9.5 30 | django-storages[google]==1.9.1 # https://github.com/jschneier/django-storages 31 | django-templated-email==2.3.0 32 | djangorestframework==3.11.0 # https://github.com/encode/django-rest-framework 33 | flower==0.9.3 # https://github.com/mher/flower 34 | hiredis==1.0.1 35 | hyperlink==19.0.0 36 | idna-ssl==1.1.0 37 | immutables==0.14 38 | incremental==17.5.0 39 | # ERROR: channels-redis 2.4.0 has requirement msgpack~=0.6.0, but you'll have msgpack 1.0.0 which is incompatible. 40 | msgpack==0.6.0 41 | multidict==4.5.2 42 | numpy==1.19.1 43 | pandas==0.25.1 44 | Pillow==6.2.0 # https://github.com/python-pillow/Pillow 45 | profilehooks==1.11.0 46 | PyHamcrest==1.9.0 47 | python-coveralls==2.9.3 48 | python-slugify==4.0.1 # https://github.com/un33k/python-slugify 49 | pytz==2019.2 # https://github.com/stub42/pytz 50 | PyYAML==5.1.2 51 | redis==3.3.8 # https://github.com/antirez/redis 52 | sentry-asgi==0.2.0 53 | sentry-sdk==0.12.2 # https://github.com/getsentry/sentry-python 54 | SQLAlchemy==1.3.19 # needed for pandas to parse legacy data from database 55 | twisted==20.3.0 56 | txaio==18.8.1 57 | typing-extensions==3.7.4 58 | vine==1.3.0 59 | websocket-client==0.56.0 # need this for now to profile and find speedups 60 | websockets==8.0.2 61 | whitenoise==4.1.3 # https://github.com/evansd/whitenoise 62 | xlrd==1.2.0 63 | XlsxWriter==1.2.9 64 | yarl==1.3.0 65 | zope.interface==4.6.0 66 | tblib==1.7.0 # use for parallel traceback exceptions 67 | -------------------------------------------------------------------------------- /requirements/local.txt: -------------------------------------------------------------------------------- 1 | -r ./base.txt 2 | black==19.3b0 # https://github.com/ambv/black 3 | coverage==4.5.4 # https://github.com/nedbat/coveragepy 4 | django-coverage-plugin==1.6.0 # https://github.com/nedbat/django_coverage_plugin 5 | django-debug-toolbar==2.0 # https://github.com/jazzband/django-debug-toolbar 6 | django-extensions==3.0.2 # https://github.com/django-extensions/django-extensions 7 | django-test-plus==1.4.0 8 | factory-boy==2.12.0 # https://github.com/FactoryBoy/factory_boy 9 | flake8==3.7.8 # https://github.com/PyCQA/flake8 10 | ipdb==0.13.3 # https://github.com/gotcha/ipdb 11 | mypy==0.720 # https://github.com/python/mypy 12 | psycopg2==2.8.3 --no-binary psycopg2 # https://github.com/psycopg/psycopg2 13 | pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery 14 | pylint-django==2.0.15 # https://github.com/PyCQA/pylint-django 15 | pytest==5.4.3 # https://github.com/pytest-dev/pytest 16 | pytest-django==3.5.1 # https://github.com/pytest-dev/pytest-django 17 | pytest-sugar==0.9.2 # https://github.com/Frozenball/pytest-sugar 18 | Sphinx==2.2.0 # https://github.com/sphinx-doc/sphinx 19 | Werkzeug==1.0.1 # pyup: < 0.15 # https://github.com/pallets/werkzeug 20 | -------------------------------------------------------------------------------- /requirements/production.txt: -------------------------------------------------------------------------------- 1 | -r ./base.txt 2 | Collectfast==1.0.0 # https://github.com/antonagestam/collectfast 3 | 4 | # Django 5 | # ------------------------------------------------------------------------------ 6 | 7 | gunicorn==20.0.4 # https://github.com/benoitc/gunicorn 8 | psycopg2==2.8.3 --no-binary psycopg2 # https://github.com/psycopg/psycopg2 9 | -------------------------------------------------------------------------------- /scripts/restore_db_from_snapshot.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | dropdb -U $POSTGRES_USER $POSTGRES_DB 4 | createdb -U $POSTGRES_USER $POSTGRES_DB 5 | psql -U $POSTGRES_USER $POSTGRES_DB < /app/snapshots/db_snapshot.sql 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules 4 | 5 | [pycodestyle] 6 | max-line-length = 120 7 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules 8 | 9 | [mypy] 10 | python_version = 3.6 11 | check_untyped_defs = True 12 | ignore_errors = False 13 | ignore_missing_imports = True 14 | strict_optional = True 15 | warn_unused_ignores = True 16 | warn_redundant_casts = True 17 | warn_unused_configs = True 18 | 19 | [mypy-*.migrations.*] 20 | # Django migrations should not produce any errors: 21 | ignore_errors = True 22 | --------------------------------------------------------------------------------