├── .bowerrc
├── .circleci
└── config.yml
├── .coveragerc
├── .editorconfig
├── .flake8
├── .gitignore
├── .isort.cfg
├── .stylintrc
├── Gulpfile.coffee
├── README.md
├── acc
├── __init__.py
├── apps.py
├── assets
│ ├── index.styl
│ ├── login.styl
│ └── subscriptions.styl
├── fixtures
│ └── me.jpg
├── migrations
│ └── __init__.py
├── pipelines.py
├── signals.py
├── static
│ ├── acc
│ │ ├── fancy-login.jpg
│ │ └── fancy-login@2x.jpg
│ ├── no-photo.jpg
│ └── no-photo@2x.jpg
├── templates
│ ├── acc
│ │ ├── greetings
│ │ │ ├── _base.html
│ │ │ ├── _partial
│ │ │ │ ├── briefs.html
│ │ │ │ └── curated-lessons.html
│ │ │ ├── _subscription.html
│ │ │ ├── classes-without-subscription.html
│ │ │ ├── empty.html
│ │ │ ├── out-of-classes.html
│ │ │ ├── subscription-active.html
│ │ │ ├── subscription-finished.html
│ │ │ ├── trial-scheduled.html
│ │ │ ├── trial-started.html
│ │ │ └── trial.html
│ │ ├── index.html
│ │ └── profile.html
│ ├── mail
│ │ └── service
│ │ │ └── new_user.html
│ └── registration
│ │ └── login.html
├── tests.py
├── urls.py
└── views.py
├── accounting
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_auto_20160925_0847.py
│ ├── 0003_auto_20161012_1551.py
│ └── __init__.py
├── models.py
├── signals.py
├── tasks.py
└── tests
│ ├── functional
│ ├── __init__.py
│ └── tests_signals.py
│ ├── integration
│ ├── __init__.py
│ └── tests_tasks.py
│ └── unit
│ ├── __init__.py
│ ├── tests_events.py
│ ├── tests_manager.py
│ └── tests_tasks.py
├── bower.json
├── build
├── css-vendor-files.json
├── download_geoip_db.sh
├── js-vendor-files.json
├── store-build-information.sh
└── strip-for-production.sh
├── circle.yml
├── coffeelint.json
├── crm
├── __init__.py
├── admin
│ ├── __init__.py
│ ├── companies.py
│ ├── customers.py
│ └── forms.py
├── apps.py
├── assets
│ ├── admin
│ │ └── css
│ │ │ ├── customer_notes.styl
│ │ │ └── customers_actions.styl
│ ├── customer_profile
│ │ ├── customer_form.coffee
│ │ └── customer_form.styl
│ └── issue
│ │ ├── issue_form-toggle.styl
│ │ ├── issue_form.coffee
│ │ └── issue_form.styl
├── fixtures
│ └── crm.yaml
├── forms.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_auto_20160701_0448.py
│ ├── 0003_auto_20160705_0602.py
│ ├── 0004_auto_20160705_0754.py
│ ├── 0005_auto_20160707_0716.py
│ ├── 0006_auto_20160714_1512.py
│ ├── 0007_auto_20160807_1102.py
│ ├── 0008_auto_20160811_1306.py
│ ├── 0009_auto_20160813_1830.py
│ ├── 0010_customer_ref.py
│ ├── 0011_customer_responsible.py
│ ├── 0011_customer_timezone.py
│ ├── 0012_companies.py
│ ├── 0013_auto_20160909_0601.py
│ ├── 0014_auto_20160916_1337.py
│ ├── 0014_merge.py
│ ├── 0015_customer_languages.py
│ ├── 0016_customernote.py
│ ├── 0017_auto_20160918_1312.py
│ ├── 0018_merge.py
│ ├── 0019_customer_profile_photo_cropping.py
│ ├── 0020_auto_20161001_1227.py
│ ├── 0021_issue.py
│ ├── 0022_auto_20161220_1234.py
│ ├── 0023_user_field_is_mandatory.py
│ └── __init__.py
├── models.py
├── signals.py
├── templates
│ ├── crm
│ │ ├── admin
│ │ │ └── customer_notes.html
│ │ ├── customer_form.html
│ │ ├── issue_form.html
│ │ └── issue_popup.html
│ └── mail
│ │ └── trial_lesson_added.html
├── templatetags
│ └── contact_us.py
├── tests
│ ├── __init__.py
│ ├── functional
│ │ ├── __init__.py
│ │ ├── tests_export_last_lessons.py
│ │ ├── tests_issues.py
│ │ ├── tests_mailchimp.py
│ │ └── tests_profile.py
│ ├── integration
│ │ ├── __init__.py
│ │ └── test_trial_user_flow.py
│ ├── smoke
│ │ ├── __init__.py
│ │ └── tests_greetings.py
│ └── unit
│ │ ├── __init__.py
│ │ ├── tests_customer.py
│ │ ├── tests_greetings.py
│ │ ├── tests_issues.py
│ │ ├── tests_templatetags.py
│ │ └── tests_trial.py
├── urls.py
└── views.py
├── dashboard.sublime-project
├── docs
├── Celery.md
└── Template-location.md
├── elk
├── .env.circle
├── __init__.py
├── admin
│ ├── __init__.py
│ ├── filters.py
│ ├── forms.py
│ ├── model_admin.py
│ └── widgets.py
├── api
│ ├── __init__.py
│ ├── fields.py
│ └── permissions.py
├── assets
│ ├── admin
│ │ ├── css
│ │ │ ├── common.styl
│ │ │ ├── date_range_filter.styl
│ │ │ ├── foreignkey.styl
│ │ │ ├── left_menu.styl
│ │ │ └── markdown_field.styl
│ │ └── js
│ │ │ ├── .gitkeep
│ │ │ ├── foreignkey.coffee
│ │ │ └── num_only.coffee
│ ├── css
│ │ ├── branding.styl
│ │ ├── common.styl
│ │ ├── flash_message.styl
│ │ ├── forms.styl
│ │ ├── header.styl
│ │ └── modal.styl
│ └── js
│ │ ├── buttons.coffee
│ │ ├── csrf.coffee
│ │ ├── datetime.coffee
│ │ ├── duration.coffee
│ │ ├── flash_messages.coffee
│ │ ├── mobile.coffee
│ │ ├── modal.coffee
│ │ ├── progressbar.coffee
│ │ ├── raven.coffee
│ │ └── rivets
│ │ ├── after.coffee
│ │ ├── rv-bs-hidden-if.coffee
│ │ └── selected.coffee
├── celery.py
├── context_processors.py
├── formats
│ ├── __init__.py
│ ├── en
│ │ ├── __init__.py
│ │ └── formats.py
│ └── ru
│ │ ├── __init__.py
│ │ └── formats.py
├── geoip.py
├── logging.py
├── middleware.py
├── settings.py
├── static
│ ├── .gitignore
│ ├── favicon.ico
│ ├── fonts
│ │ ├── FontAwesome.otf
│ │ ├── fontawesome-webfont.eot
│ │ ├── fontawesome-webfont.svg
│ │ ├── fontawesome-webfont.ttf
│ │ ├── fontawesome-webfont.woff
│ │ └── fontawesome-webfont.woff2
│ └── i
│ │ ├── elk-120x120-round.png
│ │ ├── elk-120x120.png
│ │ ├── elk-book-white.svg
│ │ ├── elk-book.svg
│ │ ├── elk-heart-white.svg
│ │ ├── elk-heart.svg
│ │ ├── loader.svg
│ │ └── skype.svg
├── templates
│ ├── admin
│ │ └── base_site.html
│ └── elk
│ │ ├── _partial
│ │ ├── flash_messages.html
│ │ └── logo.svg
│ │ ├── analytics.html
│ │ ├── analytics
│ │ └── metrika.html
│ │ ├── base.html
│ │ ├── boilerplate.html
│ │ └── header
│ │ ├── navbar.html
│ │ └── profile.html
├── templatetags
│ ├── __init__.py
│ ├── absolute_url.py
│ ├── custom_humanize.py
│ ├── flash_message.py
│ ├── navbar_tags.py
│ └── skype.py
├── tests
│ ├── __init__.py
│ ├── functional
│ │ ├── __init__.py
│ │ ├── tests_absolute_url.py
│ │ └── tests_middleware.py
│ └── unit
│ │ ├── __init__.py
│ │ ├── test_views.py
│ │ ├── tests_bundled_admin_logging.py
│ │ ├── tests_context_processors.py
│ │ ├── tests_date_utils.py
│ │ ├── tests_fixture_generator.py
│ │ ├── tests_geoip.py
│ │ └── tests_templatetags.py
├── urls.py
├── utils
│ ├── __init__.py
│ ├── date.py
│ ├── forms.py
│ └── testing.py
├── views.py
└── wsgi.py
├── extevents
├── __init__.py
├── fixtures
│ ├── event-overlap.ics
│ ├── no-endtime.ics
│ ├── recurring-without-timezone.ics
│ ├── recurring.ics
│ ├── simple-plus-recurring.ics
│ └── simple.ics
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_auto_20160912_1433.py
│ ├── 0003_googlecalendar_last_update.py
│ ├── 0004_auto_20160918_1335.py
│ ├── 0005_externalevent_parent.py
│ ├── 0006_auto_20160924_1318.py
│ ├── 0007_auto_20161220_1326.py
│ └── __init__.py
├── models.py
├── static
│ └── admin
│ │ └── calendar_admin.css
├── tasks.py
└── tests
│ ├── __init__.py
│ ├── functional
│ ├── __init__.py
│ ├── test_google_calendar.py
│ └── tests_eventsource.py
│ └── unit
│ ├── __init__.py
│ ├── tests_google_calendar.py
│ ├── tests_ical_core.py
│ ├── tests_manager.py
│ └── tests_safety.py
├── lessons
├── __init__.py
├── admin.py
├── api
│ ├── __init__.py
│ └── serializers.py
├── assets
│ └── lessons.styl
├── fixtures
│ ├── lessons-fedor.yaml
│ └── lessons.yaml
├── migrations
│ ├── 0001_squashed_0002_event.py
│ ├── 0002_auto_20160701_0524.py
│ ├── 0003_event_slots.py
│ ├── 0004_auto_20160713_1532.py
│ ├── 0005_auto_20160719_0735.py
│ ├── 0006_auto_20160722_1351.py
│ ├── 0007_auto_20160728_1337.py
│ ├── 0008_pairedlesson_host.py
│ ├── 0009_auto_20160919_1200.py
│ ├── 0009_language.py
│ ├── 0010_merge.py
│ ├── 0011_auto_20160926_1543.py
│ ├── 0012_triallesson.py
│ ├── 0013_lesson_photos.py
│ └── __init__.py
├── models.py
└── tests
│ ├── __init__.py
│ ├── functional
│ ├── __init__.py
│ └── tests_api.py
│ └── unit
│ ├── __init__.py
│ ├── tests_api.py
│ └── tests_lessons.py
├── mailer
├── __init__.py
├── ical.py
├── migrations
│ └── __init__.py
├── owl.py
├── tasks.py
├── templates
│ └── mailer
│ │ ├── _signature.html
│ │ └── test.html
└── tests
│ ├── tests_ical.py
│ └── tests_owl.py
├── manage.py
├── market
├── __init__.py
├── admin
│ ├── __init__.py
│ ├── actions.py
│ ├── classes.py
│ ├── components.py
│ └── subscriptions.py
├── apps.py
├── assets
│ ├── cancel_popup
│ │ ├── cancel_popup.coffee
│ │ └── style.styl
│ ├── customer_lessons
│ │ └── customer_lessons.styl
│ ├── lessons_starting_soon
│ │ └── lessons_starting_soon.styl
│ ├── next_lesson
│ │ └── style.styl
│ ├── schedule_popup
│ │ ├── controller.coffee
│ │ ├── model.coffee
│ │ ├── popup-mobile-scroll.coffee
│ │ └── style.styl
│ ├── subsription_status
│ │ └── style.styl
│ └── timeline_entry_popup
│ │ ├── timeline_entry_popup.coffee
│ │ └── timeline_entry_popup.styl
├── auto_schedule.py
├── exceptions.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_auto_20160818_1546.py
│ ├── 0003_auto_20160929_0355.py
│ ├── 0004_auto_20161005_1552.py
│ ├── 0005_remove_class_lesson_id.py
│ ├── 0006_auto_20161117_1139.py
│ ├── 0007_subscription_duration.py
│ ├── 0008_subscription_first_lesson_date.py
│ ├── 0009_DefaultBuyPriceIzZero.py
│ └── __init__.py
├── models.py
├── signals.py
├── sortinghat.py
├── templates
│ ├── mail
│ │ └── class
│ │ │ ├── student
│ │ │ ├── cancelled.html
│ │ │ └── scheduled.html
│ │ │ └── teacher
│ │ │ ├── cancelled.html
│ │ │ └── scheduled.html
│ └── market
│ │ ├── _partial
│ │ ├── lessons_starting_soon.html
│ │ ├── next_lesson.html
│ │ └── subscription_status.html
│ │ ├── cancel_form.html
│ │ ├── cancel_popup
│ │ ├── index.html
│ │ └── sorry.html
│ │ ├── customer_lessons.html
│ │ ├── schedule_form.html
│ │ ├── schedule_popup
│ │ └── schedule_popup.html
│ │ ├── timeline_entry_form.html
│ │ └── timeline_entry_popup
│ │ └── timeline_entry_popup.html
├── templatetags
│ └── market
│ │ ├── __init__.py
│ │ └── schedule_popup.py
├── tests
│ ├── AutoSchedule
│ │ ├── __init__.py
│ │ └── tests_unit.py
│ ├── SortingHat
│ │ ├── __init__.py
│ │ ├── tests_functional.py
│ │ └── tests_unit.py
│ ├── __init__.py
│ ├── functional
│ │ ├── __init__.py
│ │ ├── tests_buy_subscription.py
│ │ ├── tests_customer_lesson_list.py
│ │ ├── tests_schedule.py
│ │ ├── tests_scheduling_popup_api.py
│ │ ├── tests_subscription_status.py
│ │ ├── tests_timeline_entry_popup.py
│ │ └── tests_user_cancellation.py
│ └── unit
│ │ ├── __init__.py
│ │ ├── tests_buyable_product.py
│ │ ├── tests_class.py
│ │ ├── tests_manager.py
│ │ ├── tests_schedule.py
│ │ ├── tests_signals.py
│ │ └── tests_subscription.py
├── urls.py
└── views.py
├── media
└── .gitignore
├── package.json
├── payments
├── __init__.py
├── assets
│ ├── stripe.coffee
│ └── stripe.styl
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_auto_20161018_1229.py
│ └── __init__.py
├── models.py
├── stripe.py
├── templates
│ └── payments
│ │ ├── _partial
│ │ ├── processing-popup.html
│ │ └── stripe.html
│ │ ├── result_base.html
│ │ ├── result_failure.html
│ │ ├── single_lesson_success.html
│ │ └── subscription_success.html
├── templatetags
│ ├── __init__.py
│ └── stripe.py
├── tests
│ ├── __init__.py
│ ├── functional
│ │ ├── __init__.py
│ │ ├── tests_payment_form.py
│ │ ├── tests_processing.py
│ │ └── tests_purchase.py
│ └── unit
│ │ ├── __init__.py
│ │ └── tests_stripe.py
├── urls.py
└── views.py
├── products
├── __init__.py
├── admin.py
├── apps.py
├── fixtures
│ └── products.yaml
├── migrations
│ ├── 0001_squashed_0004_auto_20160701_0401.py
│ ├── 0002_simplesubscription.py
│ ├── 0003_tiers.py
│ ├── 0004_auto_20161011_1305.py
│ ├── 0005_singlelessonproduct.py
│ ├── 0006_remove_tier_paypal_button_id.py
│ ├── 0007_auto_20161107_0952.py
│ └── __init__.py
├── models.py
└── tests
│ ├── __init__.py
│ ├── functional
│ ├── __init__.py
│ └── tests_shipment.py
│ └── unit
│ ├── __init__.py
│ ├── tests_products.py
│ └── tests_tiers.py
├── requirements.txt
├── teachers
├── __init__.py
├── admin
│ ├── __init__.py
│ ├── absences.py
│ └── teachers.py
├── api
│ ├── __init__.py
│ ├── serializers.py
│ └── viewsets.py
├── assets
│ ├── admin
│ │ └── css
│ │ │ └── working_hours.styl
│ └── css
│ │ ├── teacher_detail.styl
│ │ └── teachers.styl
├── fixtures
│ └── teachers.yaml
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_auto_20160715_1257.py
│ ├── 0003_auto_20160718_1230.py
│ ├── 0004_teacher_acceptable_lessons.py
│ ├── 0005_teacher_description.py
│ ├── 0006_teacher_announce.py
│ ├── 0007_auto_20160802_1459.py
│ ├── 0008_auto_20160809_1816.py
│ ├── 0009_auto_20160813_1302.py
│ ├── 0010_teacher_active.py
│ ├── 0011_absence.py
│ ├── 0012_auto_20160910_1235.py
│ ├── 0013_auto_20160925_1001.py
│ ├── 0014_auto_20160925_1134.py
│ ├── 0015_auto_20161008_1522.py
│ ├── 0016_teacher_teacher_avatar_cropping.py
│ ├── 0017_remove_teacher_description.py
│ ├── 0018_teacher_title.py
│ ├── 0019_teacher_ordering.py
│ └── __init__.py
├── models.py
├── slot_list.py
├── templates
│ └── teachers
│ │ ├── _partial
│ │ ├── _teacher.html
│ │ └── active_teachers.html
│ │ ├── teacher_detail.html
│ │ └── teacher_list.html
├── tests
│ ├── __init__.py
│ ├── functional
│ │ ├── __init__.py
│ │ ├── tests_autoschedule.py
│ │ ├── tests_teacher.py
│ │ └── tests_teacher_views.py
│ └── unit
│ │ ├── __init__.py
│ │ ├── tests_manager.py
│ │ ├── tests_slots.py
│ │ ├── tests_teacher.py
│ │ └── tests_working_hours.py
├── urls.py
└── views.py
└── timeline
├── __init__.py
├── api
├── __init__.py
├── serializers.py
└── viewsets.py
├── apps.py
├── assets
├── calendar
│ ├── calendar.coffee
│ └── calendar.styl
├── entry_detail
│ ├── card.coffee
│ └── style.styl
└── entry_form
│ ├── controller.coffee
│ ├── model.coffee
│ └── styles.styl
├── exceptions.py
├── forms.py
├── migrations
├── 0001_initial.py
├── 0002_entry_allow_overlap.py
├── 0003_auto_20160719_0702.py
├── 0004_entry_allow_besides_working_hours.py
├── 0005_auto_20160801_1117.py
├── 0006_entry_is_finished.py
├── 0007_entry_allow_when_teacher_is_busy.py
├── 0008_entry_allow_when_teacher_has_external_events.py
├── 0009_auto_20161023_1606.py
├── 0010_remove_entry_active.py
├── 0011_unique_lesson_type.py
├── 0012_ordering.py
└── __init__.py
├── models.py
├── signals.py
├── tasks.py
├── templates
├── mail
│ └── class
│ │ ├── student
│ │ └── starting.html
│ │ └── teacher
│ │ └── starting.html
└── timeline
│ ├── calendar.html
│ ├── entry
│ └── card.html
│ └── entry_form.html
├── templatetags
├── __init__.py
└── format_entry_date.py
├── tests
├── __init__.py
├── functional
│ ├── __init__.py
│ ├── tests_api.py
│ ├── tests_card.py
│ ├── tests_crud.py
│ ├── tests_tasks.py
│ └── tests_validation.py
├── integration
│ ├── __init__.py
│ └── tests_dangerous_cancellation.py
└── unit
│ ├── __init__.py
│ ├── tests_cancellation_methods.py
│ ├── tests_entry.py
│ ├── tests_form_context.py
│ ├── tests_manager.py
│ └── tests_validation.py
├── urls.py
└── views.py
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "elk/static/vendor/",
3 | "json": false
4 | }
5 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | omit =
3 | */venv/*
4 | */migrations/*
5 | */tests.py
6 | */__init__.py
7 | */virtualenvs/*
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | indent_size = 4
3 | indent_style = space
4 | [*.coffee]
5 | indent_size = 2
6 | [*.yml]
7 | indent_size = 2
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore = E501, E265
3 | exclude = .git,venv,migrations
4 | max-line-length = 160
--------------------------------------------------------------------------------
/.isort.cfg:
--------------------------------------------------------------------------------
1 | [settings]
2 | line_length = 119
--------------------------------------------------------------------------------
/.stylintrc:
--------------------------------------------------------------------------------
1 | {
2 | "sortOrder": "none"
3 | }
--------------------------------------------------------------------------------
/acc/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/acc/__init__.py
--------------------------------------------------------------------------------
/acc/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class AccConfig(AppConfig):
5 | name = 'acc'
6 |
7 | def ready(self):
8 | import acc.signals # NOQA
9 |
--------------------------------------------------------------------------------
/acc/assets/index.styl:
--------------------------------------------------------------------------------
1 | .homepage-big-blue-button
2 | margin-top: 25px
3 | small
4 | display: block
5 | margin-top: 5px
6 |
7 | @media (max-width: 768px)
8 | .homepage-disclaimer
9 | display: none
10 |
11 | section.active-teachers
12 | margin-top: 50px
13 |
14 | h3
15 | margin-bottom: 30px
16 |
--------------------------------------------------------------------------------
/acc/assets/login.styl:
--------------------------------------------------------------------------------
1 | @import 'nib'
2 |
3 | .login-jumbotron
4 | display: flex
5 | align-items: center
6 | height: 100vh
7 |
8 | &__benefit
9 | margin-bottom: 35px
10 |
11 | &__message
12 | relative: top -50px
13 |
14 | &__buttons
15 | relative: top -5px
16 |
17 | h1, h5, p
18 | color: #333300
19 |
20 | a+a
21 | margin-left: 5px
22 |
23 | .staff-login
24 | position: fixed
25 | top: 97vh
26 | left: 98vw
27 | opacity: .3
28 |
29 | body.fancy-login
30 | background-image: url(static + 'acc/fancy-login.jpg')
31 | @media all and (-webkit-min-device-pixel-ratio: 1.5)
32 | background-image: url(static + 'acc/fancy-login@2x.jpg')
33 | // there is a bug in nib, see https://github.com/tj/nib/issues/255
34 | background-size: cover
35 |
--------------------------------------------------------------------------------
/acc/fixtures/me.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/acc/fixtures/me.jpg
--------------------------------------------------------------------------------
/acc/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/acc/migrations/__init__.py
--------------------------------------------------------------------------------
/acc/signals.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.dispatch import Signal, receiver
3 |
4 | from mailer.owl import Owl
5 |
6 | new_user_registered = Signal(providing_args=['user', 'whom_to_notify']) # class is just scheduled
7 |
8 |
9 | @receiver(new_user_registered, dispatch_uid='new_user_notify')
10 | def new_user_notify(sender, **kwargs):
11 | whom_to_notify = kwargs.get('whom', settings.SUPPORT_EMAIL)
12 |
13 | user = kwargs['user']
14 | owl = Owl(
15 | template='mail/service/new_user.html',
16 | ctx={
17 | 'user': user,
18 | },
19 | to=[whom_to_notify],
20 | )
21 | owl.send()
22 |
--------------------------------------------------------------------------------
/acc/static/acc/fancy-login.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/acc/static/acc/fancy-login.jpg
--------------------------------------------------------------------------------
/acc/static/acc/fancy-login@2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/acc/static/acc/fancy-login@2x.jpg
--------------------------------------------------------------------------------
/acc/static/no-photo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/acc/static/no-photo.jpg
--------------------------------------------------------------------------------
/acc/static/no-photo@2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/acc/static/no-photo@2x.jpg
--------------------------------------------------------------------------------
/acc/templates/acc/greetings/_subscription.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
4 |
7 |
8 | Our ELK Academy subscriptions include 10 lessons.
9 | Individual lessons last 30 minutes, and lessons in groups or pairs last 45 minutes.
10 |
11 |
12 | Subscription price is {{ product1_tier.name }}.
13 |
14 |
15 |
16 |
17 | {% include 'acc/greetings/_partial/briefs.html' %}
18 |
19 | {% include 'acc/greetings/_partial/curated-lessons.html' %}
20 |
--------------------------------------------------------------------------------
/acc/templates/acc/greetings/classes-without-subscription.html:
--------------------------------------------------------------------------------
1 | {% extends 'acc/greetings/_base.html' %}
2 |
--------------------------------------------------------------------------------
/acc/templates/acc/greetings/empty.html:
--------------------------------------------------------------------------------
1 | {% extends 'acc/greetings/_subscription.html' %}
2 |
--------------------------------------------------------------------------------
/acc/templates/acc/greetings/out-of-classes.html:
--------------------------------------------------------------------------------
1 | {% extends 'acc/greetings/_subscription.html' %}
2 |
--------------------------------------------------------------------------------
/acc/templates/acc/greetings/subscription-active.html:
--------------------------------------------------------------------------------
1 | {% extends 'acc/greetings/_base.html' %}
2 |
3 | {% block footer %}
4 | {% include 'market/_partial/lessons_starting_soon.html' %}
5 | {% include 'market/_partial/subscription_status.html' %}
6 | {% endblock%}
7 |
--------------------------------------------------------------------------------
/acc/templates/acc/greetings/subscription-finished.html:
--------------------------------------------------------------------------------
1 | {% extends 'acc/greetings/_subscription.html' %}
2 |
--------------------------------------------------------------------------------
/acc/templates/acc/greetings/trial-scheduled.html:
--------------------------------------------------------------------------------
1 | {% extends 'acc/greetings/_base.html' %}
2 | {% load skype_chat from skype %}
3 |
4 | {% block title %}
5 | {% with next_class=request.user.crm.classes.nearest_scheduled %}
6 | You've scheduled a lesson with {{ next_class.timeline.teacher.user.crm.full_name }}
7 | {% endwith %}
8 | {% endblock %}
9 |
10 | {% block lead %}
11 | {% with next_class=request.user.crm.classes.nearest_scheduled %}
12 |
13 | The lesson will start at {{ next_class.timeline.start | date:"SHORT_DATETIME_FORMAT" }}.
14 |
15 | {% endwith %}
16 | {% endblock %}
17 |
18 | {% block message %}
19 | {% with next_class=request.user.crm.classes.nearest_scheduled %}
20 |
21 | {{ next_class.timeline.teacher.user.crm.first_name }}'s skype is {% skype_chat next_class.timeline.teacher.user.crm %}. Don't forget to eat something delicious before the class!
22 |
23 | {% endwith %}
24 | {% endblock %}
25 |
26 | {% block disclaimer %}
27 | {% endblock %}
28 |
29 | {% block next_lesson %}
30 | {% endblock %}
31 |
32 | {% block call_to_action %}
33 | {% endblock %}
34 |
--------------------------------------------------------------------------------
/acc/templates/acc/greetings/trial-started.html:
--------------------------------------------------------------------------------
1 | {% extends 'acc/greetings/_subscription.html' %}
2 |
--------------------------------------------------------------------------------
/acc/templates/acc/greetings/trial.html:
--------------------------------------------------------------------------------
1 | {% extends 'acc/greetings/_base.html' %}
2 |
3 |
4 | {% block title %}
5 | {{ request.user.first_name | capfirst }}, your trial lesson is ready
6 | {% endblock %}
7 |
8 | {% block lead %}
9 |
10 |
11 | it with your curator at any time.
12 |
13 | {% endblock %}
14 |
15 | {% block message %}
16 |
17 | We've just powered up your trial lesson. To schedule, press the big button out there (↓↓) and choose your curator. If you don't know which curator to choose, check out our blog .
18 |
19 | {% endblock %}
20 |
21 | {% block disclaimer %}
22 | {% endblock %}
23 |
24 | {% block next_lesson %}
25 | {% endblock %}
26 |
--------------------------------------------------------------------------------
/acc/templates/acc/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'elk/base.html' %}
2 |
3 | {% block content %}
4 | {% include 'acc/greetings/'|add:GREETING|add:'.html'%}
5 | {% endblock %}
6 |
--------------------------------------------------------------------------------
/acc/templates/acc/profile.html:
--------------------------------------------------------------------------------
1 | {% extends 'elk/base.html' %}
2 |
3 | {% block content %}
4 |
11 | {% block form %}
12 | {% endblock %}
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/acc/templates/mail/service/new_user.html:
--------------------------------------------------------------------------------
1 | {% extends "mail_templated/base.tpl" %}
2 | {% load i18n %}
3 |
4 | {% block subject %}
5 | Dashboard: {{ user.crm.full_name }}, a new student!
6 | {% endblock %}
7 |
8 | {% block body %}
9 | {% trans 'Hi there!' %}
10 |
11 | {% trans "We've got a registered student via" %} {{ user.crm.source }} — {{ user.crm.full_name }}.
12 |
13 | {% include 'mailer/_signature.html' %}
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/acc/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import include, url
2 |
3 | from acc import views
4 |
5 | urlpatterns = [
6 | url(r'profile/$', views.CustomerProfile.as_view(), name='profile'),
7 | url('', include('django.contrib.auth.urls')),
8 | url('', include('social.apps.django_app.urls', namespace='social')),
9 | ]
10 |
--------------------------------------------------------------------------------
/accounting/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = 'accounting.apps.AccountingConfig'
2 |
--------------------------------------------------------------------------------
/accounting/admin.py:
--------------------------------------------------------------------------------
1 | from date_range_filter import DateRangeFilter
2 | from django.contrib import admin
3 |
4 | from accounting.models import Event as AccEvent
5 | from elk.admin import ModelAdmin
6 |
7 |
8 | @admin.register(AccEvent)
9 | class AccountingEventAdmin(ModelAdmin):
10 | list_display = ('time', 'teacher', 'customers', 'event_type')
11 | list_filter = (
12 | ('timestamp', DateRangeFilter),
13 | ('teacher', admin.RelatedOnlyFieldListFilter),
14 | 'event_type',
15 | )
16 | readonly_fields = ('time', 'teacher', 'event_type', 'customers')
17 | exclude = ('originator_id', 'originator_type')
18 |
19 | def has_add_permission(self, *args):
20 | return False
21 |
22 | def has_delete_permission(self, *args):
23 | return False
24 |
25 | def time(self, instance):
26 | return self._datetime(instance.originator_time)
27 |
28 | def customers(self, instance):
29 | return ', '.join(map(str, instance.originator_customers))
30 |
--------------------------------------------------------------------------------
/accounting/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class AccountingConfig(AppConfig):
5 | name = 'accounting'
6 |
7 | def ready(self):
8 | import accounting.signals # noqa
9 |
--------------------------------------------------------------------------------
/accounting/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('teachers', '0012_auto_20160910_1235'),
11 | ('contenttypes', '0002_remove_content_type_name'),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name='Event',
17 | fields=[
18 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
19 | ('event_type', models.CharField(choices=[('class', 'Completed class'), ('customer_inspired_cancellation', 'Customer inspired cancellation')], max_length=140)),
20 | ('timestamp', models.DateTimeField(auto_now_add=True)),
21 | ('originator_id', models.PositiveIntegerField()),
22 | ('originator_type', models.ForeignKey(to='contenttypes.ContentType')),
23 | ('teacher', models.ForeignKey(related_name='accounting_events', to='teachers.Teacher')),
24 | ],
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/accounting/migrations/0002_auto_20160925_0847.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('accounting', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterUniqueTogether(
15 | name='event',
16 | unique_together=set([('teacher', 'originator_type', 'originator_id')]),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/accounting/migrations/0003_auto_20161012_1551.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('accounting', '0002_auto_20160925_0847'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterUniqueTogether(
15 | name='event',
16 | unique_together=set([]),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/accounting/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/accounting/migrations/__init__.py
--------------------------------------------------------------------------------
/accounting/signals.py:
--------------------------------------------------------------------------------
1 | from django.dispatch import receiver
2 |
3 | from accounting.models import Event as AccEvent
4 | from market.signals import class_cancelled
5 |
6 |
7 | @receiver(class_cancelled, dispatch_uid='account_class_cancellation')
8 | def account_class_cancellation(sender, **kwargs):
9 | if kwargs['src'] != 'customer':
10 | return
11 |
12 | cancelled_class = kwargs['instance']
13 | ev = AccEvent(
14 | originator=cancelled_class,
15 | teacher=cancelled_class.timeline.teacher,
16 | event_type='customer_inspired_cancellation',
17 | )
18 | ev.save()
19 |
--------------------------------------------------------------------------------
/accounting/tasks.py:
--------------------------------------------------------------------------------
1 | from accounting.models import Event as AccEvent
2 | from elk.celery import app as celery
3 | from elk.logging import logger
4 | from timeline.models import Entry as TimelineEntry
5 |
6 |
7 | @celery.task
8 | def bill_timeline_entries():
9 | for entry in TimelineEntry.objects.to_be_marked_as_finished().filter(taken_slots__gte=1):
10 | entry.is_finished = True
11 | entry.save()
12 |
13 | if not AccEvent.objects.by_originator(entry).count():
14 | ev = AccEvent(
15 | teacher=entry.teacher,
16 | originator=entry,
17 | event_type='class',
18 | )
19 | ev.save()
20 | else:
21 | logger.warning('Tried to bill already billed timeline entry')
22 |
--------------------------------------------------------------------------------
/accounting/tests/functional/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/accounting/tests/functional/__init__.py
--------------------------------------------------------------------------------
/accounting/tests/integration/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/accounting/tests/integration/__init__.py
--------------------------------------------------------------------------------
/accounting/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/accounting/tests/unit/__init__.py
--------------------------------------------------------------------------------
/accounting/tests/unit/tests_manager.py:
--------------------------------------------------------------------------------
1 | from mixer.backend.django import mixer
2 |
3 | from accounting.models import Event as AccEvent
4 | from elk.utils.testing import TestCase, create_teacher
5 | from timeline.models import Entry as TimelineEntry
6 |
7 |
8 | class TestEventManager(TestCase):
9 | @classmethod
10 | def setUpTestData(cls):
11 | cls.teacher = create_teacher()
12 |
13 | def test_find_events_by_originator_ok(self):
14 | originator = mixer.blend(TimelineEntry, teacher=self.teacher)
15 |
16 | mixer.blend(AccEvent, originator=originator, teacher=self.teacher)
17 |
18 | found = AccEvent.objects.by_originator(originator)
19 | self.assertEqual(found.count(), 1)
20 |
21 | def test_find_events_by_originator_fail(self):
22 | originator1 = mixer.blend(TimelineEntry, teacher=self.teacher)
23 | originator2 = mixer.blend(TimelineEntry, teacher=self.teacher)
24 |
25 | mixer.blend(AccEvent, originator=originator1, teacher=self.teacher)
26 |
27 | found = AccEvent.objects.by_originator(originator2)
28 | self.assertEqual(found.count(), 0)
29 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "elk-dashboard",
3 | "dependencies": {
4 | "jquery": "~2.1.4",
5 | "bootstrap": "^3.3.6",
6 | "bootstrap-social": "^5.0.0",
7 | "moment": "~2.14.1",
8 | "fullcalendar": "~2.9.0",
9 | "sprintfjs": "~1.2.3",
10 | "jquery-timepicker": "jonthornton/jquery-timepicker#~1.11.1",
11 | "bootstrap-datepicker": "~1.6.1",
12 | "bower": "*",
13 | "install": "^1.0.4",
14 | "bootstrap-select": "~1.10.0",
15 | "rivets": "~0.9.0",
16 | "microevent.js": "f213/microevent.js",
17 | "js-cookie": "^2.1.3",
18 | "font-awesome": "^4.7.0",
19 | "select2": "^4.0.3",
20 | "raven-js": "^3.9.1"
21 | },
22 | "private": "true",
23 | "resolutions": {
24 | "font-awesome": "^4.7.0"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/build/css-vendor-files.json:
--------------------------------------------------------------------------------
1 | [
2 | "bootstrap/dist/css/bootstrap.min.css",
3 | "font-awesome/css/font-awesome.min.css",
4 | "bootstrap-datepicker/dist/css/bootstrap-datepicker3.min.css",
5 | "jquery-timepicker/jquery.timepicker.css",
6 | "bootstrap-select/dist/css/bootstrap-select.min.css"
7 | ]
8 |
--------------------------------------------------------------------------------
/build/download_geoip_db.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # Downloads maxmind database from http://dev.maxmind.com/geoip/legacy/geolite/
4 | #
5 |
6 | cd geolite
7 |
8 | wget -N http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz
9 |
10 | gunzip -kqf GeoLite2-City.mmdb.gz
11 |
--------------------------------------------------------------------------------
/build/js-vendor-files.json:
--------------------------------------------------------------------------------
1 | [
2 | "jquery/dist/jquery.min.js",
3 | "sprintfjs/sprintf.js",
4 | "moment/min/moment.min.js",
5 | "rivets/dist/rivets.bundled.min.js",
6 | "bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js",
7 | "jquery-timepicker/jquery.timepicker.min.js",
8 | "bootstrap-select/dist/js/bootstrap-select.min.js",
9 | "js-cookie/src/js.cookie.js",
10 | "microevent.js/microevent.js",
11 | "raven-js/dist/raven.min.js",
12 | "bootstrap/dist/js/bootstrap.js"
13 | ]
14 |
--------------------------------------------------------------------------------
/build/store-build-information.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | git rev-parse HEAD > ./elk/static/revision.txt
4 |
5 | F='./elk/static/build.txt'
6 |
7 | echo "branch: $CIRCLE_BRANCH" > $F
8 | echo "commit: $CIRCLE_SHA1" >> $F
9 | echo "build: $CIRCLE_BUILD_NUM" >> $F
10 |
--------------------------------------------------------------------------------
/build/strip-for-production.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # This script stips all files, not needed in production environment.
4 | #
5 | #
6 |
7 | rm .elk/.env
8 |
9 | rm -Rf .git
10 |
11 | # delete all test files
12 | rm -Rf */tests
13 | find . -type f -name 'tests_*' -delete
14 |
15 | # delete Stylus and Coffeescript sources
16 | find . -type f -name '*.coffee' -delete
17 | find . -type f -name '*.styl' -delete
18 | rm -Rf package.json bower.json
19 |
20 | # documentation is not needed on the production!
21 | rm -Rf *.md
--------------------------------------------------------------------------------
/coffeelint.json:
--------------------------------------------------------------------------------
1 | {
2 | "max_line_length" : {
3 | "level": "warn",
4 | "value": "119"
5 | }
6 | }
--------------------------------------------------------------------------------
/crm/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = 'crm.apps.CRMConfig'
2 |
--------------------------------------------------------------------------------
/crm/admin/__init__.py:
--------------------------------------------------------------------------------
1 | from . import customers # noqa
2 | from . import companies # noqa
3 |
--------------------------------------------------------------------------------
/crm/admin/companies.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from crm.models import Company
4 | from elk.admin import ModelAdmin
5 |
6 |
7 | @admin.register(Company)
8 | class CompanyAdmin(ModelAdmin):
9 | pass
10 |
--------------------------------------------------------------------------------
/crm/admin/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from suit.widgets import SuitDateWidget
3 |
4 | from elk.admin.forms import ActionFormWithParams
5 |
6 |
7 | class CustomerActionForm(ActionFormWithParams):
8 | start = forms.DateField(widget=SuitDateWidget(attrs={'class': 'customer-action__date-selector'}))
9 | end = forms.DateField(widget=SuitDateWidget(attrs={'class': 'customer-action__date-selector'}))
10 |
--------------------------------------------------------------------------------
/crm/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class CRMConfig(AppConfig):
5 | name = 'crm'
6 | verbose_name = 'CRM'
7 |
8 | def ready(self):
9 | import crm.signals # NOQA
10 |
--------------------------------------------------------------------------------
/crm/assets/admin/css/customer_notes.styl:
--------------------------------------------------------------------------------
1 | @import 'nib'
2 |
3 | .customer_notes
4 | margin-top: 40px
5 | h1
6 | margin-bottom 10px
7 | margin-left 0
8 |
9 | &__note + &__note
10 | margin-top 30px
11 |
12 | &__text
13 | max-width 590px
14 |
15 | &__meta, &__text
16 | display inline-block
17 | vertical-align top
18 |
19 | &__meta
20 | p
21 | margin-bottom 0
22 |
23 | &__meta
24 | font-size 11px
25 |
26 | &__text
27 | margin-left 5px
28 | font-size 14px
29 |
30 | .vLargeTextField
31 | min-width 450px
32 |
33 | &__existing
34 | margin-bottom 30px
35 |
36 | &__add
37 | @media print
38 | display none
39 |
40 | &__btn
41 | margin-top 5px
42 | relative: left 197px
43 |
--------------------------------------------------------------------------------
/crm/assets/admin/css/customers_actions.styl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/crm/assets/admin/css/customers_actions.styl
--------------------------------------------------------------------------------
/crm/assets/customer_profile/customer_form.coffee:
--------------------------------------------------------------------------------
1 | $frm = $ '.customer-profile form'
2 | $submit = $ 'input[type=submit]', $frm
3 | $birthday = $ 'input[name=birthday]', $frm
4 | $birthday_label = $ '.customer-profile__birthday', $frm
5 |
6 | $('input[required]', $frm).on 'keyup', (e) ->
7 | $parent = $(e.target).parents '.form-group'
8 |
9 | if $frm[0].checkValidity()
10 | $submit.removeClass 'disabled'
11 | $parent.removeClass 'has-error'
12 | else
13 | $submit.addClass 'disabled'
14 | $parent.addClass 'has-error'
15 |
16 | .keyup()
17 |
18 | $birthday.datepicker
19 | autoclose: true,
20 | startView: 'years',
21 | endDate: '-15y',
22 | todayHighlight: false
23 |
24 | .on 'changeDate', (e) ->
25 | date = moment e.date
26 | $birthday_label.text date.format 'll'
27 |
28 |
29 |
30 | $('.change-birthday').on 'click', (e) ->
31 | $birthday.datepicker('show')
32 | e.preventDefault()
33 |
--------------------------------------------------------------------------------
/crm/assets/customer_profile/customer_form.styl:
--------------------------------------------------------------------------------
1 | .customer-profile
2 | max-width: 320px
3 |
4 | &__intro
5 | margin-bottom: 30px
6 | display: table
7 | border-spacing: 0 5px
8 |
9 | .field
10 | display: table-row
11 |
12 | label, span
13 | display: table-cell
14 |
15 | span
16 | padding-left: 15px
17 |
18 | label:after
19 | content: ': '
20 |
21 | &__birthday
22 | font-style: inherit
23 | margin-right: 5px
24 |
25 | &__form
26 | margin-bottom: 30px
27 | height: 100px
28 | min-height: 100px
29 |
30 | display: table
31 | border-spacing: 0 10px
32 | width: 100%
33 |
34 | .form-group
35 | display: table-row
36 | height: 40px
37 |
38 | label, .form-control
39 | display: table-cell
40 | vertical-align: middle
41 |
42 | label
43 | position: relative
44 | top: -2px
45 |
46 | .need-skype
47 | position: absolute
48 | margin-left: 15px
49 | padding-top: 5px
50 |
--------------------------------------------------------------------------------
/crm/assets/issue/issue_form-toggle.styl:
--------------------------------------------------------------------------------
1 |
2 | .issue-form-toggle
3 | @media (max-width: 768px)
4 | display: none
5 |
6 | position: fixed
7 | bottom: 0
8 | right: 60px
9 |
10 | border-radius: 5px 5px 0 0
11 | background-color: #222222
12 |
13 | cursor: pointer
14 |
15 | .fa
16 | min-width: 14px // to avoid flashing
17 | margin-left: 2px
18 |
19 | &__open
20 | display: block
21 | padding: 5px 10px
22 | color: #cccccc
23 | &:hover, &:focus
24 | text-decoration: none
25 | outline: none
26 | &:hover
27 | color: #ffffff
28 | &:focus
29 | color: #cccccc
30 |
--------------------------------------------------------------------------------
/crm/assets/issue/issue_form.coffee:
--------------------------------------------------------------------------------
1 | class IssueController
2 | constructor: (@form, @body) ->
3 | @still = true
4 | @submitting = false
5 | @submitted = false
6 | @submit_disabled = true
7 |
8 | @body.on 'keyup', () =>
9 | @update()
10 |
11 | @form.on 'submit', () =>
12 | @submit()
13 | return false
14 |
15 | @update()
16 |
17 | update: () ->
18 | @msg = @body.val()
19 | @submit_disabled = if @msg.length > 0 then false else true
20 |
21 | submit: () ->
22 | @still = false
23 | @submitting = true
24 | @submit_disabled = true
25 | $.post '/crm/issue/', {body: @msg}, () =>
26 | @submitting = false
27 | @submitted = true
28 |
29 |
30 | $('.issue-form').on 'shown.bs.modal', () ->
31 | $form = $ 'form', this
32 | $body = $ 'textarea[name="body"]', $form
33 | c = new IssueController(
34 | form = $form
35 | body = $body
36 | )
37 | rivets.bind $form, {c: c}
38 | $body.focus()
39 |
--------------------------------------------------------------------------------
/crm/assets/issue/issue_form.styl:
--------------------------------------------------------------------------------
1 | .issue-form
2 | height: 420px
3 | .modal-header, .modal-body, .form-group
4 | padding-bottom: 0
5 | margin-bottom: 0
6 | margin-top: 0
7 |
8 | .modal-header, .modal-footer
9 | border: 0
10 | h3
11 | margin: 0
12 |
13 | .modal-footer
14 | padding-top: 0
15 |
16 | &__body, .pretty-loader
17 | min-height: 125px
18 |
19 |
20 | .modal-body
21 | min-height: 155px
22 |
23 | &__thanks
24 | h3
25 | margin-top: 5px
26 | p
27 | margin-bottom: 0
28 |
--------------------------------------------------------------------------------
/crm/fixtures/crm.yaml:
--------------------------------------------------------------------------------
1 | - model: auth.user
2 | pk: 1
3 | fields:
4 | password: pbkdf2_sha256$24000$ri9yLfnl0Uhj$gd5V+zlABd6OsekQucih3RbyLlI73dF0gLJAAIkLpdI=
5 | last_login: "2016-06-24T08:50:47+00:00"
6 | is_superuser: true
7 | username: admin
8 | first_name: 'Fedor'
9 | last_name: 'Borshev'
10 | email: f@f213.in
11 | is_staff: true
12 | is_active: true
13 | date_joined: "2016-06-23T13:58:08+00:00"
14 | groups: []
15 | user_permissions: []
16 | - model: crm.customer
17 | pk: 1
18 | fields: {user: 1, source: 'internal',
19 | date_arrived: '2016-06-23T13:59:30+03:00', country: RU, starting_level: A1, current_level: A2}
20 |
--------------------------------------------------------------------------------
/crm/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 | from crm.models import Customer
4 | from elk.utils.date import common_timezones
5 |
6 |
7 | class CustomerProfileForm(forms.ModelForm):
8 | timezone = forms.ChoiceField(
9 | choices=common_timezones(),
10 | widget=forms.Select(
11 | attrs={
12 | 'class': 'form-control selectpicker customer-profile__timezone fadein',
13 | 'data-live-search': 'true'
14 | }
15 | ),
16 | )
17 |
18 | class Meta:
19 | model = Customer
20 | fields = ('timezone', 'birthday', 'skype')
21 |
--------------------------------------------------------------------------------
/crm/migrations/0004_auto_20160705_0754.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('crm', '0003_auto_20160705_0602'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='customer',
16 | name='profile_photo',
17 | field=models.ImageField(null=True, upload_to='profiles/'),
18 | ),
19 | migrations.AlterField(
20 | model_name='customer',
21 | name='source',
22 | field=models.CharField(max_length=140, verbose_name='Customer source', default='internal'),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/crm/migrations/0005_auto_20160707_0716.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('crm', '0004_auto_20160705_0754'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='customer',
16 | name='linkedin',
17 | field=models.CharField(blank=True, max_length=140, verbose_name='Linkedin username'),
18 | ),
19 | migrations.AddField(
20 | model_name='customer',
21 | name='native_language',
22 | field=models.CharField(blank=True, max_length=140, null=True),
23 | ),
24 | migrations.AddField(
25 | model_name='customer',
26 | name='profession',
27 | field=models.CharField(blank=True, max_length=140, null=True),
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/crm/migrations/0006_auto_20160714_1512.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('crm', '0005_auto_20160707_0716'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='RegisteredCustomer',
16 | fields=[
17 | ],
18 | options={
19 | 'verbose_name': 'Student',
20 | 'proxy': True,
21 | },
22 | bases=('crm.customer',),
23 | ),
24 | migrations.AlterModelOptions(
25 | name='customer',
26 | options={'verbose_name': 'Lead'},
27 | ),
28 | migrations.AlterField(
29 | model_name='customer',
30 | name='profile_photo',
31 | field=models.ImageField(null=True, upload_to='profiles/', blank=True),
32 | ),
33 | migrations.AlterField(
34 | model_name='customer',
35 | name='source',
36 | field=models.CharField(default='internal', max_length=140),
37 | ),
38 | ]
39 |
--------------------------------------------------------------------------------
/crm/migrations/0007_auto_20160807_1102.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('crm', '0006_auto_20160714_1512'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='customer',
16 | name='cancellation_streak',
17 | field=models.SmallIntegerField(verbose_name='Cancelled lesson streak', default=0),
18 | ),
19 | migrations.AddField(
20 | model_name='customer',
21 | name='max_cancellation_count',
22 | field=models.SmallIntegerField(verbose_name='Maximum allowed lessons to cancel', default=7),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/crm/migrations/0008_auto_20160811_1306.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('crm', '0007_auto_20160807_1102'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterModelOptions(
15 | name='customer',
16 | options={'verbose_name': 'CRM Profile'},
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/crm/migrations/0009_auto_20160813_1830.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('crm', '0008_auto_20160811_1306'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='customer',
16 | name='current_level',
17 | field=models.CharField(blank=True, max_length=2, null=True, choices=[('A1', 'A1'), ('B1', 'B1'), ('C1', 'C1'), ('A2', 'A2'), ('B2', 'B2'), ('C2', 'C2'), ('A3', 'A3'), ('B3', 'B3'), ('C3', 'C3')]),
18 | ),
19 | migrations.AlterField(
20 | model_name='customer',
21 | name='starting_level',
22 | field=models.CharField(blank=True, max_length=2, null=True, choices=[('A1', 'A1'), ('B1', 'B1'), ('C1', 'C1'), ('A2', 'A2'), ('B2', 'B2'), ('C2', 'C2'), ('A3', 'A3'), ('B3', 'B3'), ('C3', 'C3')]),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/crm/migrations/0010_customer_ref.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('crm', '0009_auto_20160813_1830'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='customer',
16 | name='ref',
17 | field=models.CharField(verbose_name='Referal code', max_length=140, blank=True),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/crm/migrations/0011_customer_responsible.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import django.db.models.deletion
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('teachers', '0010_teacher_active'),
12 | ('crm', '0010_customer_ref'),
13 | ]
14 |
15 | operations = [
16 | migrations.AddField(
17 | model_name='customer',
18 | name='responsible',
19 | field=models.ForeignKey(related_name='patronized_customers', to='teachers.Teacher', on_delete=django.db.models.deletion.SET_NULL, null=True, blank=True),
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/crm/migrations/0011_customer_timezone.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import timezone_field.fields
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('crm', '0010_customer_ref'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='customer',
17 | name='timezone',
18 | field=timezone_field.fields.TimeZoneField(default='Europe/Moscow'),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/crm/migrations/0012_companies.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import django.db.models.deletion
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('crm', '0011_customer_responsible'),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name='Company',
17 | fields=[
18 | ('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
19 | ('name', models.CharField(max_length=140)),
20 | ('legal_name', models.CharField(max_length=140)),
21 | ],
22 | options={
23 | 'verbose_name_plural': 'companies',
24 | },
25 | ),
26 | migrations.DeleteModel(
27 | name='RegisteredCustomer',
28 | ),
29 | migrations.AddField(
30 | model_name='customer',
31 | name='company',
32 | field=models.ForeignKey(to='crm.Company', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customers', blank=True),
33 | ),
34 | ]
35 |
--------------------------------------------------------------------------------
/crm/migrations/0013_auto_20160909_0601.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('crm', '0012_companies'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterModelOptions(
15 | name='customer',
16 | options={'verbose_name': 'Profile'},
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/crm/migrations/0014_auto_20160916_1337.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('crm', '0013_auto_20160909_0601'),
11 | ]
12 |
13 | operations = [
14 | migrations.RenameField(
15 | model_name='customer',
16 | old_name='responsible',
17 | new_name='curator',
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/crm/migrations/0014_merge.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('crm', '0013_auto_20160909_0601'),
11 | ('crm', '0011_customer_timezone'),
12 | ]
13 |
14 | operations = [
15 | ]
16 |
--------------------------------------------------------------------------------
/crm/migrations/0015_customer_languages.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('lessons', '0009_language'),
11 | ('crm', '0014_auto_20160916_1337'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='customer',
17 | name='languages',
18 | field=models.ManyToManyField(to='lessons.Language'),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/crm/migrations/0016_customernote.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('teachers', '0010_teacher_active'),
11 | ('crm', '0015_customer_languages'),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name='CustomerNote',
17 | fields=[
18 | ('id', models.AutoField(verbose_name='ID', serialize=False, primary_key=True, auto_created=True)),
19 | ('timestamp', models.DateTimeField(auto_now_add=True)),
20 | ('text', models.TextField()),
21 | ('customer', models.ForeignKey(to='crm.Customer', related_name='notes')),
22 | ('teacher', models.ForeignKey(to='teachers.Teacher', related_name='customer_notes')),
23 | ],
24 | options={
25 | 'verbose_name': 'Note',
26 | 'verbose_name_plural': 'Customer notes',
27 | },
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/crm/migrations/0017_auto_20160918_1312.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('crm', '0016_customernote'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='customer',
16 | name='languages',
17 | field=models.ManyToManyField(to='lessons.Language', blank=True),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/crm/migrations/0018_merge.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('crm', '0017_auto_20160918_1312'),
11 | ('crm', '0014_merge'),
12 | ]
13 |
14 | operations = [
15 | ]
16 |
--------------------------------------------------------------------------------
/crm/migrations/0019_customer_profile_photo_cropping.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import image_cropping.fields
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('crm', '0018_merge'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='customer',
17 | name='profile_photo_cropping',
18 | field=image_cropping.fields.ImageRatioField('image', '80x80', verbose_name='profile photo cropping', free_crop=False, size_warning=False, hide_image_field=False, allow_fullsize=False, adapt_rotation=False, help_text=None),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/crm/migrations/0020_auto_20161001_1227.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import image_cropping.fields
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('crm', '0019_customer_profile_photo_cropping'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='customer',
17 | name='phone',
18 | field=models.CharField(blank=True, verbose_name='Phone number', max_length=15),
19 | ),
20 | migrations.AlterField(
21 | model_name='customer',
22 | name='profile_photo_cropping',
23 | field=image_cropping.fields.ImageRatioField('profile_photo', '80x80', hide_image_field=False, free_crop=False, adapt_rotation=False, verbose_name='profile photo cropping', help_text=None, allow_fullsize=False, size_warning=False),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/crm/migrations/0021_issue.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('crm', '0020_auto_20161001_1227'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='Issue',
16 | fields=[
17 | ('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
18 | ('customer', models.ForeignKey(to='crm.Customer', related_name='issues')),
19 | ('body', models.TextField()),
20 | ],
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/crm/migrations/0022_auto_20161220_1234.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.conf import settings
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('crm', '0021_issue'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='customer',
17 | name='user',
18 | field=models.OneToOneField(blank=True, related_name='crm', to=settings.AUTH_USER_MODEL, null=True),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/crm/migrations/0023_user_field_is_mandatory.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.conf import settings
5 | from django.db import migrations, models
6 |
7 |
8 | def drop_customer_profiles_without_user(apps, schema_editor):
9 | Customer = apps.get_model('crm.Customer')
10 | Customer.objects.filter(user__isnull=True).delete()
11 |
12 |
13 | class Migration(migrations.Migration):
14 |
15 | dependencies = [
16 | ('crm', '0022_auto_20161220_1234'),
17 | ]
18 |
19 | operations = [
20 | migrations.RunSQL('SET CONSTRAINTS ALL IMMEDIATE'),
21 | migrations.RunPython(drop_customer_profiles_without_user),
22 | migrations.AlterField(
23 | model_name='customer',
24 | name='user',
25 | field=models.OneToOneField(to=settings.AUTH_USER_MODEL, related_name='crm'),
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/crm/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/crm/migrations/__init__.py
--------------------------------------------------------------------------------
/crm/signals.py:
--------------------------------------------------------------------------------
1 | from django.apps import apps
2 | from django.contrib.auth.models import User
3 | from django.db.models.signals import post_save
4 | from django.dispatch import Signal, receiver
5 |
6 | from mailer.owl import Owl
7 |
8 | trial_lesson_added = Signal()
9 |
10 |
11 | @receiver(trial_lesson_added, dispatch_uid='notify_new_customer_about_trial_lesson')
12 | def notify_new_customer_about_trial_lesson(sender, **kwargs):
13 | owl = Owl(
14 | template='mail/trial_lesson_added.html',
15 | ctx={
16 | 'c': sender,
17 | },
18 | to=[sender.user.email],
19 | timezone=sender.timezone,
20 | )
21 | owl.send()
22 |
23 |
24 | @receiver(post_save, sender=User, dispatch_uid='create_profile_for_new_users')
25 | def create_profile_for_new_users(sender, **kwargs):
26 | if not kwargs['created']:
27 | return
28 |
29 | user = kwargs['instance']
30 | Customer = apps.get_model('crm.Customer')
31 | try:
32 | if user.crm is not None:
33 | return
34 | except Customer.DoesNotExist:
35 | pass
36 |
37 | Customer.objects.create(user=user)
38 |
--------------------------------------------------------------------------------
/crm/templates/crm/issue_popup.html:
--------------------------------------------------------------------------------
1 | {% load contact_us %}
2 |
3 | {% contact_us 'Ask us a question ' 'issue-form-toggle__open' %}
4 |
5 |
--------------------------------------------------------------------------------
/crm/templates/mail/trial_lesson_added.html:
--------------------------------------------------------------------------------
1 | {% extends "mail_templated/base.tpl" %}
2 |
3 | {% load absolute_url %}
4 |
5 | {% block subject %}
6 | Hey, {{ c.first_name | capfirst }}, go ahead and schedule your free first lesson!
7 | {% endblock %}
8 |
9 | {% block body %}
10 | Dear {{ c.first_name | capfirst }},
11 |
12 | Welcome aboard!
13 |
14 | We've just powered on your first trial lesson. You can plan it in the dashboard at {% absolute_url "home" %}#schedule.
15 |
16 | We look forward to getting to meet you at your first lesson!
17 |
18 | Dream BIG and make it happen
19 | ELK Academy
20 | Education + Love = Knowledge
21 | {% endblock %}
22 |
--------------------------------------------------------------------------------
/crm/templatetags/contact_us.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.utils.html import format_html
3 |
4 | register = template.Library()
5 |
6 |
7 | @register.simple_tag
8 | def contact_us(text='Contact us', classes=''):
9 | return format_html('{} ', classes, text)
10 |
--------------------------------------------------------------------------------
/crm/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/crm/tests/__init__.py
--------------------------------------------------------------------------------
/crm/tests/functional/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/crm/tests/functional/__init__.py
--------------------------------------------------------------------------------
/crm/tests/integration/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/crm/tests/integration/__init__.py
--------------------------------------------------------------------------------
/crm/tests/smoke/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/crm/tests/smoke/__init__.py
--------------------------------------------------------------------------------
/crm/tests/smoke/tests_greetings.py:
--------------------------------------------------------------------------------
1 | from collections import OrderedDict
2 |
3 | from crm.models import Customer
4 | from elk.utils.testing import ClientTestCase
5 |
6 |
7 | class TestGreetingsSmoke(ClientTestCase):
8 | def test_all_available_greetings(self):
9 | """
10 | Check if all greeting types has templates for the homepage
11 | """
12 | greetings = OrderedDict(Customer.GREETINGS)
13 | for greeting_type in greetings.keys():
14 | response = self.c.get('/?greeting=%s' % greeting_type)
15 | self.assertEqual(response.status_code, 200)
16 |
--------------------------------------------------------------------------------
/crm/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/crm/tests/unit/__init__.py
--------------------------------------------------------------------------------
/crm/tests/unit/tests_templatetags.py:
--------------------------------------------------------------------------------
1 | from django.template import Context, Template
2 |
3 | from elk.utils.testing import TestCase
4 |
5 |
6 | class TestContactUsTemplateTag(TestCase):
7 | def test_default(self):
8 | tpl = Template("{% load contact_us %}{% contact_us %}")
9 | html = tpl.render(Context({}))
10 |
11 | self.assertIn('drop a line ', html)
18 |
19 | def test_classes(self):
20 | tpl = Template("{% load contact_us %}{% contact_us 'drop a line' 'btn btn-lg' %}")
21 | html = tpl.render(Context({}))
22 |
23 | self.assertIn('class="btn btn-lg"', html)
24 |
--------------------------------------------------------------------------------
/crm/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url
2 | from django.contrib.auth.decorators import login_required
3 |
4 | from crm import views
5 |
6 | urlpatterns = [
7 | url('^issue/$', login_required(views.IssueCreate.as_view()), name='issue_create'),
8 | url(r'^mailchimp_csv/(?P[\d,]+)$', views.mailchimp_csv, name='mailchimp_csv'),
9 | url(r'^export_last_lessons/(?P[\d,]+)/start/(?P[\d-]+)/end/(?P[\d-]+)/$', views.export_last_lessons, name='export_last_lessons'),
10 | ]
11 |
--------------------------------------------------------------------------------
/dashboard.sublime-project:
--------------------------------------------------------------------------------
1 | {
2 | "folders":
3 | [
4 | {
5 | "file_exclude_patterns":
6 | [
7 | ".*",
8 | "manage.py",
9 | "celerybeat-*",
10 | "coffeelint.json",
11 | "dashboard.sublime-*"
12 | ],
13 | "folder_exclude_patterns":
14 | [
15 | ".deploy",
16 | "build",
17 | "static",
18 | "media",
19 | "geolite"
20 | ],
21 | "path": "."
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/docs/Celery.md:
--------------------------------------------------------------------------------
1 | # Using Celery
2 |
3 | We use [Celery](http://www.celeryproject.org) instead of cron for periodic tasks scheduling.
4 |
5 | ## Configuring and running on a development machine
6 |
7 | In production environment, Celery uses Redis as storage for tasks. If you want to try some other configuration on your machine (you really shouldn't), configure `.env`:
8 | ```ini
9 | CELERY_BROKER_URL='redis://localhost:6379'
10 | CELERY_RESULT_BACKEND='redis://localhost:6379'
11 | ```
12 |
13 | To run Celery on your machine, cd to the project root&virtualenv and run:
14 | ```sh
15 | celery -A elk worker -B
16 | ```
17 |
18 | `-B` here starts the Celery heartbeat process in the same instance.
19 |
20 | ## Defining a task
21 |
22 | Create a file named `cron.py` in your application, and follow Celery [documentation](http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#entries) to define a task.
23 |
24 | Then, add a line to `settings.py`:
25 | ```python
26 |
27 | CELERYBEAT_SCHEDULE = {
28 | 'check_classes_that_will_start_soon': {
29 | 'task': 'market.tasks.notify_15min_to_class',
30 | 'schedule': timedelta(minutes=1),
31 | },
32 |
33 | ```
34 |
--------------------------------------------------------------------------------
/elk/.env.circle:
--------------------------------------------------------------------------------
1 | DEBUG=on
2 | SECRET_KEY=test100500
3 | DATABASE_URL=postgres://ubuntu:@127.0.0.1:5432/circle_test
4 | CACHE_URL=dummycache://
5 | STATIC_URL=/static/
6 | STATIC_ROOT=/tmp/static
7 | MEDIA_ROOT=/tmp/media
8 | MEDIA_URL=/media/
9 |
10 | SOCIAL_AUTH_FACEBOOK_KEY=''
11 | SOCIAL_AUTH_FACEBOOK_SECRET=''
12 |
13 | SOCIAL_AUTH_GOOGLE_OAUTH2_KEY=''
14 | SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET=''
15 | TIME_ZONE='US/Eastern'
16 |
17 | EMAIL_HOST=127.0.0.1
18 | EMAIL_PORT=1025
19 | EMAIL_BACKEND=django.core.mail.backends.dummy.EmailBackend
20 | EMAIL_ASYNC=False
21 | MAILGUN_API_KEY=key-ra4dae3Ahs1oajiophup0chahpai8ooj
22 | MAILGUN_SENDER_DOMAIN=sandbox,oophouta4Pifeiji3ceitie5yeme7iud.mailgun.org
23 |
24 | EMAIL_NOTIFICATIONS_FROM='Fedor Borshev '
25 |
26 | CELERY_BROKER_URL='redis://localhost:6379'
27 | CELERY_RESULT_BACKEND='redis://localhost:6379'
28 |
29 | SENTRY_DSN=https://Ooshei9Iu7Eorenu8phahmijaihohdi6:Ooshei9Iu7Eorenu8phahmijaihohdi6@sentry.io/101101
30 | GROOVE_API_TOKEN=100500
31 |
32 | STRIPE_API_KEY=sk_test_aethoh6nairaWo
33 | STRIPE_PK=pk_test_quees3un8Yeing
34 |
--------------------------------------------------------------------------------
/elk/__init__.py:
--------------------------------------------------------------------------------
1 | from .celery import app as celery # noqa
2 |
--------------------------------------------------------------------------------
/elk/admin/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Abstract admin classes. All your ModelAdmins should be subclasses from this Modeladmin
3 | """
4 | from django.contrib import admin
5 |
6 | from .model_admin import * # noqa
7 |
8 |
9 | admin.site.disable_action('delete_selected') # disable delete selected action site-wide
10 |
--------------------------------------------------------------------------------
/elk/admin/filters.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 |
4 | class BooleanFilter(admin.SimpleListFilter):
5 | """
6 | Abstract base class for simple boolean filter in admin. You should define only
7 | `title`, unique `parameter_name` and two methods: `t` and `f`, returning a queryset
8 | when filter is set to True and False respectively:
9 |
10 | class HasClassesFilter(BooleanFilter):
11 | title = _('Has classes')
12 | parameter_name = 'has_classes'
13 |
14 | def t(self, request, queryset):
15 | return queryset.filter(classes__isnull=False).distinct('pk')
16 |
17 | def n(self, request, queryset):
18 | return queryset.filter(classes__isnull=True)
19 |
20 | """
21 | def lookups(self, request, model_admin):
22 | return (
23 | ('t', 'Yes'),
24 | ('f', 'No'),
25 | )
26 |
27 | def queryset(self, request, queryset):
28 | if not self.value():
29 | return queryset
30 | else:
31 | if self.value() == 't':
32 | return self.t(request, queryset)
33 | else:
34 | return self.f(request, queryset)
35 |
--------------------------------------------------------------------------------
/elk/admin/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.contrib.admin.helpers import ActionForm
3 | from django.utils.translation import ugettext_lazy as _
4 |
5 |
6 | class ActionFormWithParams(ActionForm):
7 | """
8 | Use this form when you are redefining an ActionForm
9 | in order to add some action parameters.
10 | """
11 | action = forms.ChoiceField(label=_('Action:'), widget=forms.widgets.Select(attrs={'class': 'action_selector'}))
12 |
--------------------------------------------------------------------------------
/elk/admin/widgets.py:
--------------------------------------------------------------------------------
1 | from django.forms.widgets import Select
2 |
3 |
4 | class ForeignKeyWidget(Select):
5 | """
6 | Fancy select widget based on Select2 (https://select2.github.io/)
7 | """
8 | def __init__(self, attrs={}, **kwargs):
9 |
10 | classes = attrs.get('class', '')
11 | classes = 'foreign_key ' + classes
12 |
13 | attrs['class'] = classes
14 |
15 | super().__init__(attrs, **kwargs)
16 |
--------------------------------------------------------------------------------
/elk/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/elk/api/__init__.py
--------------------------------------------------------------------------------
/elk/api/fields.py:
--------------------------------------------------------------------------------
1 | from django_markdown.utils import markdown
2 | from rest_framework import serializers
3 |
4 |
5 | class MarkdownField(serializers.Field):
6 | """
7 | Return HTML rendered from Django-markdown (https://github.com/sv0/django-markdown-app)
8 | """
9 | def to_representation(self, obj):
10 | return markdown(obj)
11 |
--------------------------------------------------------------------------------
/elk/api/permissions.py:
--------------------------------------------------------------------------------
1 | from rest_framework import permissions
2 |
3 |
4 | class StaffMemberRequiredPermission(permissions.BasePermission):
5 | def has_permission(self, request, view):
6 | return request.user.is_staff
7 |
--------------------------------------------------------------------------------
/elk/assets/admin/css/common.styl:
--------------------------------------------------------------------------------
1 | @import 'nib'
2 | a.skype
3 | &:before
4 | relative: top 3px
5 | display: inline-block
6 | content: '\a0'
7 | width: 12px
8 | height: 12px
9 | background-image: url(static + 'i/skype.svg')
10 | background-repeat: no-repeat
11 | background-size: contain
12 | margin-right: 1px
13 |
14 | .control-group.error, .control-group.warning, .control-group
15 | .control-label, input, .add-on
16 | transition: color .5s
17 | transition: border-color .5s
18 | transition: background-color .5s
19 |
20 | .actions
21 | .datetimeshortcuts // hide the annoying 'today' link in action forms that use SuitDateWidget for date input
22 | margin-left: 5px
23 | a:first-of-type
24 | display: none
25 |
26 | a
27 | padding: 5px 5px !important
28 |
29 | .vDateField.input-small
30 | width: 105px !important
31 |
32 | .action_selector // add a padding to selector in actions forms with parameters, see elk/admin/forms.py
33 | margin-right: 30px
--------------------------------------------------------------------------------
/elk/assets/admin/css/date_range_filter.styl:
--------------------------------------------------------------------------------
1 | .date_range_filter
2 | margin-left 10px
3 |
--------------------------------------------------------------------------------
/elk/assets/admin/css/foreignkey.styl:
--------------------------------------------------------------------------------
1 | @import 'nib'
2 |
3 | .select2-selection:focus
4 | outline: none !important
5 | border-color: rgba(82, 168, 236, .8)
6 | box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6)
7 |
8 | .select2-search__field
9 | height: 30px !important
10 |
11 | .select2-results__option
12 | padding-left: 10px
13 |
14 | .select2-results__message
15 | opacity: .8
16 |
--------------------------------------------------------------------------------
/elk/assets/admin/css/left_menu.styl:
--------------------------------------------------------------------------------
1 | @import 'nib'
2 |
3 | // This styles do fix non-perfect django-suit icons position in the left menu
4 |
5 | .left-nav
6 | >ul>li>a>i
7 | relative: top -1px
8 | margin-right 7px
9 |
10 | .icon-lock
11 | relative: top -2px
12 |
13 | .icon-home
14 | relative: left 1px
15 |
--------------------------------------------------------------------------------
/elk/assets/admin/css/markdown_field.styl:
--------------------------------------------------------------------------------
1 | .markItUp
2 | width 580px !important
3 |
4 | .markItUpEditor
5 | width 570px !important
6 |
--------------------------------------------------------------------------------
/elk/assets/admin/js/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/elk/assets/admin/js/.gitkeep
--------------------------------------------------------------------------------
/elk/assets/admin/js/foreignkey.coffee:
--------------------------------------------------------------------------------
1 | # Fancy selector on all foreignkey selectors
2 | # https://select2.github.io/
3 |
4 | $('select.foreign_key').select2()
5 |
--------------------------------------------------------------------------------
/elk/assets/admin/js/num_only.coffee:
--------------------------------------------------------------------------------
1 | # Simple plugin to allow input only of numbers.
2 | # Take a look at the last line — it's the usage example.
3 |
4 | $.fn.numonly = () ->
5 |
6 | blink = ($el) -> # blink with error
7 | $group = $el.parents('.control-group.form-row')
8 | $group.addClass 'error'
9 | window.setTimeout () ->
10 | $group.removeClass 'error'
11 | , 100
12 |
13 | $(this).on 'keypress', (e) ->
14 | if $.inArray(e.keyCode, [46, 8, 9, 27, 13, 110]) > 0
15 | return
16 |
17 | if not e.key?
18 | return
19 | if e.key.match /Shift|Control|Alt|Meta/
20 | return
21 |
22 | if e.key.match /^\d|\:$/
23 | return
24 |
25 | blink $ this
26 |
27 | e.preventDefault()
28 |
29 |
30 | $('input.numonly').numonly()
31 |
--------------------------------------------------------------------------------
/elk/assets/css/branding.styl:
--------------------------------------------------------------------------------
1 | .btn-primary
2 | background-color: #262262
3 |
4 | outline: none !important
5 | &:focus
6 | background-color: #262262
7 |
8 | &:hover
9 | background-color: #2d4373
10 | transition: background-color .1s
11 |
12 | &:not(:hover)
13 | transition: background-color .2s
14 |
15 | &.disabled, &[disabled]
16 | opacity: .35
17 | &:hover
18 | background-color: #262262
19 |
20 | body
21 | font-family: 'Montserrat'
22 | line-height: 25px
23 |
24 | label
25 | font-weight: normal
26 |
27 | h1, h2, h3, h4, h5, h6
28 | font-family: 'Roboto Slab'
29 | font-weight: 700
30 |
31 | h1
32 | line-height: 45px
33 |
34 | p.lead
35 | font-family: 'Roboto Slab'
36 | line-height: 35px
37 |
38 | p
39 | margin-bottom: 15px
40 |
41 | h1
42 | margin-bottom: 20px
43 |
44 | h2
45 | margin-bottom: 15px
46 |
47 | .page-header
48 | margin: 40px 0
49 |
--------------------------------------------------------------------------------
/elk/assets/css/flash_message.styl:
--------------------------------------------------------------------------------
1 | .flash-message
2 | position: fixed
3 | top: 40px
4 | right: 30px
5 | opacity: .8
6 |
--------------------------------------------------------------------------------
/elk/assets/css/forms.styl:
--------------------------------------------------------------------------------
1 | .bootstrap-select
2 | .dropdown-toggle:focus
3 | outline: none !important
4 |
5 | li.no-results
6 | opacity: .5
7 |
8 | .dropdown-backdrop
9 | display: none
10 |
11 | .btn-primary, .btn-default
12 | transition: opacity .1s ease-in // for styling bootstrap 'enabled' and 'disabled' classes
13 |
14 | label
15 | font-weight: normal
16 |
--------------------------------------------------------------------------------
/elk/assets/css/header.styl:
--------------------------------------------------------------------------------
1 | @import 'nib'
2 |
3 | .navbar-profile
4 | &__photo
5 | position: relative
6 | display: inline-block
7 | margin-right: 10px
8 | color: #262262
9 | img
10 | margin-right: 5px
11 |
12 | &.active
13 | color: #333 // bootstrap body color
14 | pointer-events: none
15 | .profile-needs-updating
16 | display: none
17 |
18 | .profile-needs-updating
19 | absolute: left 50px top 2px
20 | background-color: red
21 | min-height: 8px
22 | min-width: 8px
23 | border-radius: 8px
24 |
25 | a:hover
26 | text-decoration: none
27 |
28 |
29 | .elk-logo
30 | padding: 4px
31 | margin-right: 30px
32 | svg
33 | height: 100%
34 |
35 | .profile-needs-updating
36 | position: relative
37 | min-height: 10px
38 | min-width: 10px
39 |
--------------------------------------------------------------------------------
/elk/assets/css/modal.styl:
--------------------------------------------------------------------------------
1 | // some default bootstrap modal improvements
2 |
3 |
4 | .modal-header
5 | button.close
6 | margin-top: -7px // sorry, but bootstrap does that
7 |
8 | span
9 | font-size: 30px
10 |
--------------------------------------------------------------------------------
/elk/assets/js/buttons.coffee:
--------------------------------------------------------------------------------
1 | $('button[data-url]').click ->
2 | window.location.href = $(this).attr 'data-url'
3 |
--------------------------------------------------------------------------------
/elk/assets/js/csrf.coffee:
--------------------------------------------------------------------------------
1 | #
2 | # Take django's CSRF cookie and append it to every ajax POST request with same origin
3 | #
4 | csrftoken = Cookies.get 'csrftoken'
5 |
6 | $.ajaxSetup
7 | beforeSend: (xhr, settings) ->
8 | if settings.type is 'POST' and not this.crossDomain
9 | xhr.setRequestHeader 'X-CSRFToken', csrftoken
10 |
--------------------------------------------------------------------------------
/elk/assets/js/datetime.coffee:
--------------------------------------------------------------------------------
1 | #
2 | # jQuery methods for 3 fields:
3 | # - $('#field').applyTimePicker() based on https://github.com/jonthornton/jquery-timepicker
4 | # - $('#field').applyDatepicker() based on https://github.com/eternicode/bootstrap-datepicker
5 | # - $('#field').applyDurationSelector() based on own simple plugin (see duration.coffee)
6 | #
7 |
8 | $.fn.applyTimepicker = () ->
9 | $(this).timepicker
10 | timeFormat: 'H:i',
11 | scrollDefault: 'now'
12 |
13 | $.fn.applyDatePicker = () ->
14 | $(this).datepicker
15 | autoclose: true,
16 | startDate: Date(),
17 | todayBtn: 'linked',
18 | todayHighlight: true
19 |
20 | $.fn.applyDurationSelector = () ->
21 | $(this).duration()
22 |
--------------------------------------------------------------------------------
/elk/assets/js/duration.coffee:
--------------------------------------------------------------------------------
1 | #
2 | # Simple plugin for duration input fields
3 | #
4 | $.fn.duration = () ->
5 | $field = $(this)
6 | format = () ->
7 | d = $field.val()
8 | d = d.replace /:\d*$/, '' if d.match /^\d+:\d+:/
9 | d = d.replace /^0:/, '00:'
10 | $field.val d
11 |
12 | $field.on 'change', format
13 | format()
14 |
15 | $field.on 'keyup', (e) ->
16 | if $field.val().match /^\d{2}$/
17 | if e.keyCode isnt 8
18 | $field.val($field.val() + ':')
19 |
20 | if $field.val().match /^\d{3}$/
21 | d = $field.val()
22 | a = d.split ''
23 | $field.val(a[0] + a[1] + ':' + a[2])
24 |
--------------------------------------------------------------------------------
/elk/assets/js/flash_messages.coffee:
--------------------------------------------------------------------------------
1 | $(document).on 'ready', () ->
2 | window.setTimeout () ->
3 | $('.flash-message.alert').alert 'close'
4 | , 2000
5 |
--------------------------------------------------------------------------------
/elk/assets/js/mobile.coffee:
--------------------------------------------------------------------------------
1 | if window.matchMedia('(max-width: 767px)').matches
2 | $('body').addClass 'mobile'
3 | else
4 | $('body').addClass 'desktop'
5 |
--------------------------------------------------------------------------------
/elk/assets/js/modal.coffee:
--------------------------------------------------------------------------------
1 | #
2 | # System-wide defaults for bootstrap modal windows:
3 | # — Close on ESC
4 | # — Close on press on any button with type = cancel
5 | #
6 |
7 | $('.modal').on 'show.bs.modal', () ->
8 | $modal = $ this
9 |
10 | $(document).on 'keyup', (e) -> # close popup on ESC
11 | return if e.keyCode isnt 27
12 | $(document).off 'keyup'
13 | $modal.modal 'hide'
14 |
15 | $('button[type=reset]', $modal).on 'click', () -> # close popup on every button with type=cancel
16 | $modal.modal 'hide'
17 |
--------------------------------------------------------------------------------
/elk/assets/js/progressbar.coffee:
--------------------------------------------------------------------------------
1 | # Automatically set width of all bootstrap progressbars on the page
2 | # https://getbootstrap.com/components/#progress
3 |
4 | $.fn.update_bootstrap_progressbar = () ->
5 | $bar = $ this
6 | max = parseInt $bar.attr 'aria-valuemax'
7 | now = parseInt $bar.attr 'aria-valuenow'
8 | width = Math.round now / max * 100
9 |
10 | $bar.css 'width', width + '%'
11 |
12 | $('.progress .progress-bar').each () ->
13 | $(this).update_bootstrap_progressbar()
14 |
--------------------------------------------------------------------------------
/elk/assets/js/raven.coffee:
--------------------------------------------------------------------------------
1 | #
2 | # Init sentry.io frontend-error logging
3 | #
4 |
5 | raven = Raven.config Project.helpers.raven_dsn()
6 | raven.install()
--------------------------------------------------------------------------------
/elk/assets/js/rivets/after.coffee:
--------------------------------------------------------------------------------
1 | rivets.binders.after = (el, value) ->
2 | $(el).after value
3 |
--------------------------------------------------------------------------------
/elk/assets/js/rivets/rv-bs-hidden-if.coffee:
--------------------------------------------------------------------------------
1 | rivets.binders['hidden-if'] = (el, value) ->
2 | $el = $ el
3 | if not value
4 | $el.removeClass 'hidden'
5 | else
6 | $el.addClass 'hidden'
7 |
8 | rivets.binders['hidden-unless'] = (el, value) ->
9 | $el = $ el
10 | if value
11 | $el.removeClass 'hidden'
12 | else
13 | $el.addClass 'hidden'
14 |
--------------------------------------------------------------------------------
/elk/assets/js/rivets/selected.coffee:
--------------------------------------------------------------------------------
1 | rivets.binders.selected = (el, value) ->
2 | el.selected = 'selected' if value
3 |
--------------------------------------------------------------------------------
/elk/celery.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import os
4 |
5 | from celery import Celery
6 | from django.conf import settings # noqa
7 |
8 | # set the default Django settings module for the 'celery' program.
9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'elk.settings')
10 |
11 |
12 | app = Celery('elk')
13 |
14 | # Using a string here means the worker will not have to
15 | # pickle the object when using Windows.
16 | app.config_from_object('django.conf:settings')
17 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
18 |
--------------------------------------------------------------------------------
/elk/context_processors.py:
--------------------------------------------------------------------------------
1 | from django.apps import apps
2 | from django.conf import settings
3 |
4 |
5 | def support_email(request):
6 | return {
7 | 'SUPPORT_EMAIL': settings.SUPPORT_EMAIL,
8 | }
9 |
10 |
11 | def stripe_pk(request):
12 | return {
13 | 'STRIPE_PK': settings.STRIPE_PK,
14 | }
15 |
16 |
17 | def greeting(request):
18 | if request.user is None or request.user.id is None:
19 | return {}
20 |
21 | greeting = request.GET.get('greeting', request.user.crm.get_greeting_type())
22 |
23 | Customer = apps.get_model('crm.Customer')
24 |
25 | try:
26 | greeting = Customer.clean_greeting(greeting)
27 | except ValueError:
28 | greeting = Customer.clean_greeting('empty')
29 |
30 | return {
31 | 'GREETING': greeting
32 | }
33 |
34 |
35 | def revision(request):
36 | return {'REVISION': settings.VERSION}
37 |
--------------------------------------------------------------------------------
/elk/formats/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/elk/formats/__init__.py
--------------------------------------------------------------------------------
/elk/formats/en/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/elk/formats/en/__init__.py
--------------------------------------------------------------------------------
/elk/formats/en/formats.py:
--------------------------------------------------------------------------------
1 | # Please don't forget to use non-breakable space (mac: option + space) where possible.
2 |
3 | SHORT_DATE_FORMAT = 'D, M d'
4 | SHORT_DATETIME_FORMAT = 'M d, h:i A'
5 | TIME_FORMAT = 'h:i a'
6 |
--------------------------------------------------------------------------------
/elk/formats/ru/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/elk/formats/ru/__init__.py
--------------------------------------------------------------------------------
/elk/formats/ru/formats.py:
--------------------------------------------------------------------------------
1 | # Please don't forget to use non-breakable space (mac: option + space) where possible.
2 |
3 | SHORT_DATE_FORMAT = 'D, d E'
4 |
--------------------------------------------------------------------------------
/elk/geoip.py:
--------------------------------------------------------------------------------
1 | from os import path
2 |
3 | import geoip2.database
4 | from django.conf import settings
5 | from timezonefinder import TimezoneFinder
6 |
7 |
8 | class GeoIP():
9 | def __init__(self, ip):
10 | filename = path.join(settings.GEOIP_PATH, 'GeoLite2-City.mmdb')
11 | self.geoip = geoip2.database.Reader(filename)
12 | self._response = self.geoip.city(ip)
13 |
14 | @property
15 | def timezone(self):
16 | if self._response.location.time_zone is not None:
17 | return self._response.location.time_zone
18 |
19 | if self.lat is not None and self.lng is not None:
20 | tf = TimezoneFinder()
21 | return tf.timezone_at(
22 | lat=self.lat,
23 | lng=self.lng,
24 | )
25 |
26 | @property
27 | def country(self):
28 | return self._response.country.iso_code
29 |
30 | @property
31 | def city(self):
32 | return self._response.city.name
33 |
34 | @property
35 | def lat(self):
36 | return self._response.location.latitude
37 |
38 | @property
39 | def lng(self):
40 | return self._response.location.longitude
41 |
--------------------------------------------------------------------------------
/elk/logging.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from django.contrib import admin
4 | from django.contrib.contenttypes.models import ContentType
5 |
6 |
7 | def _get_logger():
8 | return logging.getLogger('app')
9 |
10 |
11 | class logger():
12 | @staticmethod
13 | def warning(*args, **kwargs):
14 | kwargs['exc_info'] = True
15 | _get_logger().warning(*args, **kwargs)
16 |
17 | @staticmethod
18 | def error(*args, **kwargs):
19 | kwargs['exc_info'] = True
20 | _get_logger().error(*args, **kwargs)
21 |
22 |
23 | def write_admin_log_entry(user, object, action_flag=None, msg='Changed'):
24 | """
25 | Create a django bundled admin log entry
26 | """
27 | if action_flag is None:
28 | action_flag = admin.models.CHANGE
29 |
30 | entry = admin.models.LogEntry(
31 | user=user,
32 | object_id=object.pk,
33 | content_type=ContentType.objects.get_for_model(object), # move it away from this function if you experience problems
34 | object_repr=str(object),
35 | action_flag=action_flag,
36 | change_message=msg,
37 | )
38 | entry.save()
39 |
--------------------------------------------------------------------------------
/elk/static/.gitignore:
--------------------------------------------------------------------------------
1 | bower_components
--------------------------------------------------------------------------------
/elk/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/elk/static/favicon.ico
--------------------------------------------------------------------------------
/elk/static/fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
1 | ../vendor/font-awesome/fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/elk/static/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
1 | ../vendor/font-awesome/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/elk/static/fonts/fontawesome-webfont.svg:
--------------------------------------------------------------------------------
1 | ../vendor/font-awesome/fonts/fontawesome-webfont.svg
--------------------------------------------------------------------------------
/elk/static/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
1 | ../vendor/font-awesome/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/elk/static/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
1 | ../vendor/font-awesome/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/elk/static/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
1 | ../vendor/font-awesome/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/elk/static/i/elk-120x120-round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/elk/static/i/elk-120x120-round.png
--------------------------------------------------------------------------------
/elk/static/i/elk-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/elk/static/i/elk-120x120.png
--------------------------------------------------------------------------------
/elk/static/i/elk-book-white.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/elk/static/i/elk-book.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/elk/static/i/elk-heart-white.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/elk/static/i/elk-heart.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/elk/static/i/loader.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/elk/templates/admin/base_site.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/base.html'%}
2 | {% load admin_static %}
3 |
4 | {% block extrastyle %}
5 |
6 |
7 |
8 | {% endblock %}
9 |
10 | {% block suit_jquery %}
11 |
12 |
13 |
17 | {% endblock %}
18 |
19 | {% block extrajs %}
20 |
21 |
22 | {% endblock %}
23 |
--------------------------------------------------------------------------------
/elk/templates/elk/_partial/flash_messages.html:
--------------------------------------------------------------------------------
1 | {% load flash_message %}
2 | {% if messages %}
3 | {% for msg in messages %}
4 | {% flash_message msg msg.tags %}
5 | {% endfor %}
6 | {% endif %}
7 |
--------------------------------------------------------------------------------
/elk/templates/elk/analytics.html:
--------------------------------------------------------------------------------
1 | {% include 'elk/analytics/metrika.html' %}
2 |
--------------------------------------------------------------------------------
/elk/templates/elk/analytics/metrika.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/elk/templates/elk/base.html:
--------------------------------------------------------------------------------
1 | {% extends 'elk/boilerplate.html' %}
2 | {% load staticfiles %}
3 | {% load raven %}
4 |
5 | {% block css %}
6 | {% endblock %}
7 |
8 | {% block body-class %}
9 | {% endblock %}
10 |
11 | {% block navbar %}
12 | {% include 'elk/header/navbar.html' %}
13 | {% endblock %}
14 |
15 | {% block container %}
16 | {% include "elk/_partial/flash_messages.html" %}
17 |
18 | {% block content %}
19 | {% endblock %}
20 |
21 |
22 | {% include "market/schedule_form.html" %}
23 | {% include "market/timeline_entry_form.html" %}
24 | {% include "market/cancel_form.html" %}
25 | {% include "crm/issue_form.html" %}
26 | {% include "crm/issue_popup.html" %}
27 |
28 | {% endblock %}
29 |
30 | {% block basejs %}
31 |
35 | {% endblock %}
36 |
37 | {% block js %} {# pages can require custom javascript files defining this block #}
38 | {% endblock %}
39 |
--------------------------------------------------------------------------------
/elk/templates/elk/header/profile.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load navbar_tags %}
3 | {% load staticfiles %}
4 |
5 |
18 |
--------------------------------------------------------------------------------
/elk/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/elk/templatetags/__init__.py
--------------------------------------------------------------------------------
/elk/templatetags/absolute_url.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import urljoin
2 |
3 | from django import template
4 | from django.conf import settings
5 | from django.template.defaulttags import URLNode, url
6 |
7 | register = template.Library()
8 |
9 |
10 | class AbsoluteURLNode(URLNode):
11 | """
12 | Thanks to http://felecan.com/2013/django-templatetag-absolute-full-url-domain-path/
13 | """
14 | def render(self, context):
15 | path = super().render(context)
16 |
17 | if self.asvar:
18 | context[self.asvar] = urljoin(settings.ABSOLUTE_HOST, context[self.asvar])
19 | return ''
20 | else:
21 | return urljoin(settings.ABSOLUTE_HOST, path)
22 |
23 |
24 | @register.tag
25 | def absolute_url(parser, token, node_cls=AbsoluteURLNode):
26 | """
27 | Just like {% url %} but adds the domain from settings.ABSOLUTE_HOST.
28 | """
29 | node_instance = url(parser, token)
30 |
31 | return node_cls(
32 | view_name=node_instance.view_name,
33 | args=node_instance.args,
34 | kwargs=node_instance.kwargs,
35 | asvar=node_instance.asvar
36 | )
37 |
--------------------------------------------------------------------------------
/elk/templatetags/custom_humanize.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.contrib.humanize.templatetags import humanize
3 | from django.utils.translation import pgettext
4 |
5 | register = template.Library()
6 |
7 |
8 | @register.filter
9 | def naturaltime(value):
10 | """
11 | Custom wrapper over django.contrib.humanize.naturaltime
12 | """
13 | time = humanize.naturaltime(value)
14 | time = time.replace(' ' + pgettext('naturaltime', 'from now'), '')
15 | return time
16 |
--------------------------------------------------------------------------------
/elk/templatetags/flash_message.py:
--------------------------------------------------------------------------------
1 | from django import template
2 |
3 | register = template.Library()
4 |
5 | DJANGO_BOOTSTRAP_ALERT_LEVEL_MAPPING = {
6 | 'error': 'danger',
7 | #'django': 'bootstrap'
8 | }
9 |
10 |
11 | def map_django_alert_level_to_bootstrap(tags):
12 | result_tags = []
13 | for tag in tags.split(' '):
14 | if tag in DJANGO_BOOTSTRAP_ALERT_LEVEL_MAPPING:
15 | tag = DJANGO_BOOTSTRAP_ALERT_LEVEL_MAPPING[tag]
16 |
17 | result_tags.append('alert-' + tag)
18 |
19 | return ' '.join(result_tags)
20 |
21 |
22 | @register.simple_tag
23 | def flash_message(msg, tags='info'):
24 | """
25 | Display an autoclosing flash message
26 | """
27 | tags = map_django_alert_level_to_bootstrap(tags)
28 | return """
29 |
30 |
31 | ×
32 |
33 | {msg}
34 |
35 | """.format(
36 | tags=tags,
37 | msg=msg,
38 | )
39 |
--------------------------------------------------------------------------------
/elk/templatetags/navbar_tags.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from django import template
4 |
5 | register = template.Library()
6 |
7 |
8 | @register.simple_tag
9 | def is_active(request, pattern):
10 | pattern = pattern.replace('__username__', request.user.username)
11 | if re.search(pattern, request.path):
12 | return 'active'
13 | return ''
14 |
--------------------------------------------------------------------------------
/elk/templatetags/skype.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.utils.html import format_html
3 |
4 | register = template.Library()
5 |
6 |
7 | @register.simple_tag
8 | def skype_chat(crm):
9 | if not crm or not len(crm.skype):
10 | return ''
11 |
12 | return _skype_link(crm.skype, 'chat')
13 |
14 |
15 | @register.simple_tag
16 | def skype_call(crm):
17 | if not crm or not len(crm.skype):
18 | return ''
19 |
20 | return _skype_link(crm.skype, 'call')
21 |
22 |
23 | def _skype_link(skype_username, action='chat'):
24 | return format_html('{} ', action, skype_username, action, skype_username)
25 |
--------------------------------------------------------------------------------
/elk/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/elk/tests/__init__.py
--------------------------------------------------------------------------------
/elk/tests/functional/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/elk/tests/functional/__init__.py
--------------------------------------------------------------------------------
/elk/tests/functional/tests_absolute_url.py:
--------------------------------------------------------------------------------
1 | from django.template import Context, Template
2 | from django.test import override_settings
3 |
4 | from elk.utils.testing import TestCase
5 |
6 |
7 | class TestAbsoluteUrlGenerator(TestCase):
8 | TEMPLATE = Template("{% load absolute_url %} {% absolute_url 'timeline:entry_card' username='user' pk=100500 %}")
9 |
10 | @override_settings(ABSOLUTE_HOST='https://a.app')
11 | def test_host_appending(self):
12 | rendered = self.TEMPLATE.render(Context({}))
13 | self.assertIn('https://a.app/timeline/user/100500/card/', rendered)
14 |
--------------------------------------------------------------------------------
/elk/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/elk/tests/unit/__init__.py
--------------------------------------------------------------------------------
/elk/tests/unit/test_views.py:
--------------------------------------------------------------------------------
1 | from elk.utils.testing import ClientTestCase, create_teacher
2 |
3 |
4 | class TestLoginRequiredView(ClientTestCase):
5 | def setUp(self):
6 | for i in range(0, 5):
7 | create_teacher()
8 |
9 | def test_login_ok(self):
10 | response = self.c.get('/teachers/') # this view is built from LoginRequiredListView
11 | self.assertEqual(response.status_code, 200)
12 |
13 | def test_login_required(self):
14 | self.c.logout()
15 | response = self.c.get('/teachers/') # this view is built from LoginRequiredListView
16 |
17 | self.assertRedirectsPartial(response, '/accounts/login')
18 |
--------------------------------------------------------------------------------
/elk/tests/unit/tests_bundled_admin_logging.py:
--------------------------------------------------------------------------------
1 | from django.apps import apps
2 | from django.contrib.admin.models import LogEntry
3 |
4 | from elk.logging import write_admin_log_entry
5 | from elk.utils.testing import TestCase, create_customer
6 | from lessons import models as lessons
7 |
8 |
9 | class TestBundledAdminLogging(TestCase):
10 | fixtures = ['lessons']
11 |
12 | def setUp(self):
13 | self.customer = create_customer()
14 |
15 | def test_write_admin_log_entry(self):
16 | Class = apps.get_model('market.Class')
17 | c = Class(
18 | customer=self.customer,
19 | lesson_type=lessons.OrdinaryLesson.get_contenttype()
20 | )
21 |
22 | user = create_customer().user
23 | write_admin_log_entry(user, c, msg='Testing')
24 |
25 | log_entry = LogEntry.objects.first()
26 |
27 | self.assertEqual(log_entry.change_message, 'Testing')
28 | self.assertEqual(log_entry.user, user)
29 |
--------------------------------------------------------------------------------
/elk/tests/unit/tests_date_utils.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from elk.utils.date import common_timezones, day_range
4 | from elk.utils.testing import TestCase
5 |
6 |
7 | class TestDateUtils(TestCase):
8 | def test_day_range(self):
9 | r = day_range('2016-02-28')
10 | self.assertEquals(r, ('2016-02-28 00:00:00', '2016-02-28 23:59:59'))
11 |
12 | def test_day_range_for_datetime(self):
13 | r = day_range(datetime(2016, 2, 28))
14 | self.assertEquals(r, ('2016-02-28 00:00:00', '2016-02-28 23:59:59'))
15 |
16 | def test_common_timezones(self):
17 | a = list(common_timezones())
18 | self.assertGreater(len(a), 32)
19 |
--------------------------------------------------------------------------------
/elk/tests/unit/tests_fixture_generator.py:
--------------------------------------------------------------------------------
1 | from django.apps import apps
2 |
3 | from elk.utils.testing import TestCase, create_customer, create_teacher
4 |
5 |
6 | class TestFixtures(TestCase):
7 | """
8 | Test if my fixtures helper generates fixtures with correct relations
9 | """
10 | def test_create_customer(self):
11 | Customer = apps.get_model('crm.customer')
12 | customer = create_customer()
13 |
14 | self.assertEquals(Customer.objects.get(user__username=customer.user.username), customer)
15 |
16 | def test_create_teacher(self):
17 | Teacher = apps.get_model('teachers.teacher')
18 | teacher = create_teacher()
19 |
20 | t = Teacher.objects.get(user__username=teacher.user.username)
21 | self.assertEqual(t, teacher)
22 | self.assertIsNotNone(t.user.crm)
23 | self.assertTrue(t.user.is_staff)
24 |
25 | def test_create_teacher_all_lessons(self):
26 | teacher = create_teacher()
27 | allowed_lessons = teacher.allowed_lessons.all()
28 | self.assertGreater(allowed_lessons.count(), 0)
29 | self.assertEqual(allowed_lessons[0].app_label, 'lessons')
30 |
--------------------------------------------------------------------------------
/elk/tests/unit/tests_geoip.py:
--------------------------------------------------------------------------------
1 | from unittest import skip
2 | from unittest.mock import MagicMock
3 |
4 | from elk.geoip import GeoIP
5 | from elk.utils.testing import TestCase
6 |
7 |
8 | @skip('Skipping country tests cuz we dont need to download geolite in this environment')
9 | class TestGeoIp(TestCase):
10 | def test_init(self):
11 | g = GeoIP('71.192.161.223')
12 | self.assertIsNotNone(g._response)
13 |
14 | def test_properties(self):
15 | g = GeoIP('77.37.209.115')
16 | self.assertEqual(g.country, 'RU')
17 | self.assertEqual(g.city, 'Moscow')
18 | self.assertEqual(g.timezone, 'Europe/Moscow')
19 | self.assertEqual(g.lat, 55.7527)
20 | self.assertEqual(g.lng, 37.6172)
21 |
22 | def test_timezone_tzwhere(self):
23 | """
24 | When tz can't be determined by geolite, it should be guessed by coordinates
25 | """
26 | g = GeoIP('77.37.209.115')
27 | g._response.location = MagicMock()
28 | g._response.location.time_zone = None
29 | g._response.location.longitude = 37.61
30 | g._response.location.latitude = 55.75
31 |
32 | self.assertEqual(g.timezone, 'Europe/Moscow')
33 |
--------------------------------------------------------------------------------
/elk/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/elk/utils/__init__.py
--------------------------------------------------------------------------------
/elk/utils/date.py:
--------------------------------------------------------------------------------
1 | """
2 | Shortcuts for dealing with timedelta in format, unsderstandable by django
3 | models.
4 | """
5 | from datetime import date, datetime, time, timedelta
6 |
7 | import pytz
8 |
9 |
10 | def minute_till_midnight(date):
11 | return datetime.combine(date, time.max)
12 |
13 |
14 | def minute_after_midnight(date):
15 | return datetime.combine(date + timedelta(days=1), time.min)
16 |
17 |
18 | def day_range(d):
19 | """
20 | Return a day range for model query — a tuple with start of the day and end of the day
21 | """
22 | if isinstance(d, date):
23 | d = d.strftime('%Y-%m-%d')
24 |
25 | return (
26 | d + ' 00:00:00',
27 | d + ' 23:59:59',
28 | )
29 |
30 |
31 | def common_timezones():
32 | """
33 | List of common timezones
34 |
35 | Excludes some rare and unneeded to the app timezones, like Asia or Antarctica ones
36 | """
37 | for tz in pytz.common_timezones:
38 | if tz.startswith('Europe/') or tz.startswith('US/'):
39 | yield (tz, tz)
40 |
41 | yield ('UTC', 'UTC')
42 |
--------------------------------------------------------------------------------
/elk/utils/forms.py:
--------------------------------------------------------------------------------
1 | from django.http import JsonResponse
2 |
3 |
4 | class AjaxResponseMixin():
5 | """
6 | Mixin to add AJAX support to a form.
7 | Must be used with an object-based FormView (e.g. CreateView)
8 | https://docs.djangoproject.com/en/dev/topics/class-based-views/generic-editing/#ajax-example
9 | """
10 | def get_success_url(self):
11 | return '/'
12 |
13 | def form_invalid(self, form):
14 | return JsonResponse(form.errors, status=400)
15 |
16 | def form_valid(self, form):
17 | super().form_valid(form)
18 |
19 | data = {
20 | 'pk': self.object.pk,
21 | }
22 | return JsonResponse(data)
23 |
--------------------------------------------------------------------------------
/elk/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for elk project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "elk.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/extevents/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/extevents/__init__.py
--------------------------------------------------------------------------------
/extevents/fixtures/recurring-without-timezone.ics:
--------------------------------------------------------------------------------
1 | BEGIN:VCALENDAR
2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN
3 | VERSION:2.0
4 | CALSCALE:GREGORIAN
5 | METHOD:PUBLISH
6 | X-WR-CALNAME:Мейн
7 | X-WR-TIMEZONE:Europe/Moscow
8 | BEGIN:VTIMEZONE
9 | TZID:Europe/Moscow
10 | X-LIC-LOCATION:Europe/Moscow
11 | BEGIN:STANDARD
12 | TZOFFSETFROM:+0300
13 | TZOFFSETTO:+0300
14 | TZNAME:MSK
15 | DTSTART:19700101T000000
16 | END:STANDARD
17 | END:VTIMEZONE
18 | BEGIN:VEVENT
19 | DTSTART;TZID=Europe/Moscow:20230911T210000
20 | DTEND;TZID=Europe/Moscow:20230911T220000
21 | RRULE:FREQ=WEEKLY;BYDAY=SU;UNTIL=20320911
22 | DTSTAMP:20160911T173649Z
23 | UID:q60ak05pj2t108bjpcl3ogrf5k@google.com
24 | CREATED:20160911T173548Z
25 | DESCRIPTION:This is a repeated event\, i've created for testing
26 | LAST-MODIFIED:20160911T173548Z
27 | LOCATION:
28 | SEQUENCE:0
29 | STATUS:CONFIRMED
30 | SUMMARY:Repeated event
31 | TRANSP:OPAQUE
32 | END:VEVENT
33 | END:VCALENDAR
34 |
--------------------------------------------------------------------------------
/extevents/fixtures/recurring.ics:
--------------------------------------------------------------------------------
1 | BEGIN:VCALENDAR
2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN
3 | VERSION:2.0
4 | CALSCALE:GREGORIAN
5 | METHOD:PUBLISH
6 | X-WR-CALNAME:Мейн
7 | X-WR-TIMEZONE:Europe/Moscow
8 | BEGIN:VTIMEZONE
9 | TZID:Europe/Moscow
10 | X-LIC-LOCATION:Europe/Moscow
11 | BEGIN:STANDARD
12 | TZOFFSETFROM:+0300
13 | TZOFFSETTO:+0300
14 | TZNAME:MSK
15 | DTSTART:19700101T000000
16 | END:STANDARD
17 | END:VTIMEZONE
18 | BEGIN:VEVENT
19 | DTSTART;TZID=Europe/Moscow:20230911T210000
20 | DTEND;TZID=Europe/Moscow:20230911T220000
21 | RRULE:FREQ=WEEKLY;BYDAY=SU
22 | DTSTAMP:20160911T173649Z
23 | UID:q60ak05pj2t108bjpcl3ogrf5k@google.com
24 | CREATED:20160911T173548Z
25 | DESCRIPTION:This is a repeated event\, i've created for testing
26 | LAST-MODIFIED:20160911T173548Z
27 | LOCATION:
28 | SEQUENCE:0
29 | STATUS:CONFIRMED
30 | SUMMARY:Repeated event
31 | TRANSP:OPAQUE
32 | END:VEVENT
33 | END:VCALENDAR
34 |
--------------------------------------------------------------------------------
/extevents/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('teachers', '0012_auto_20160910_1235'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='GoogleCalendar',
16 | fields=[
17 | ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)),
18 | ('url', models.URLField()),
19 | ('active', models.BooleanField(default=True)),
20 | ('teacher', models.ForeignKey(related_name='google_calendars', to='teachers.Teacher')),
21 | ],
22 | options={
23 | 'abstract': False,
24 | },
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/extevents/migrations/0003_googlecalendar_last_update.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import datetime
5 |
6 | from django.db import migrations, models
7 | from django.utils.timezone import utc
8 |
9 |
10 | class Migration(migrations.Migration):
11 |
12 | dependencies = [
13 | ('extevents', '0002_auto_20160912_1433'),
14 | ]
15 |
16 | operations = [
17 | migrations.AddField(
18 | model_name='googlecalendar',
19 | name='last_update',
20 | field=models.DateTimeField(auto_now=True, default=datetime.datetime(2016, 9, 13, 18, 38, 49, 622910, tzinfo=utc)),
21 | preserve_default=False,
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/extevents/migrations/0004_auto_20160918_1335.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('extevents', '0003_googlecalendar_last_update'),
11 | ]
12 |
13 | operations = [
14 | migrations.RenameField(
15 | model_name='externalevent',
16 | old_name='ext_src_id',
17 | new_name='src_id',
18 | ),
19 | migrations.RenameField(
20 | model_name='externalevent',
21 | old_name='ext_src_type',
22 | new_name='src_type',
23 | ),
24 | migrations.AlterUniqueTogether(
25 | name='externalevent',
26 | unique_together=set([('teacher', 'src_type', 'src_id', 'start', 'end')]),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/extevents/migrations/0005_externalevent_parent.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('extevents', '0004_auto_20160918_1335'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='externalevent',
16 | name='parent',
17 | field=models.ForeignKey(to='extevents.ExternalEvent', null=True, blank=True),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/extevents/migrations/0006_auto_20160924_1318.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('extevents', '0005_externalevent_parent'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterUniqueTogether(
15 | name='externalevent',
16 | unique_together=set([]),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/extevents/migrations/0007_auto_20161220_1326.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('extevents', '0006_auto_20160924_1318'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='externalevent',
16 | name='description',
17 | field=models.TextField(),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/extevents/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/extevents/migrations/__init__.py
--------------------------------------------------------------------------------
/extevents/static/admin/calendar_admin.css:
--------------------------------------------------------------------------------
1 | .field-updated {
2 | white-space: nowrap;
3 | }
4 | .dynamic-google_calendars input[name^="google_calendars"] {
5 | min-width: 450px;
6 | }
7 |
--------------------------------------------------------------------------------
/extevents/tasks.py:
--------------------------------------------------------------------------------
1 | from elk.celery import app as celery
2 | from extevents.models import GoogleCalendar
3 |
4 |
5 | @celery.task
6 | def update_google_calendars():
7 | for calendar in GoogleCalendar.objects.active():
8 | calendar.poll()
9 | calendar.update()
10 |
--------------------------------------------------------------------------------
/extevents/tests/functional/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/extevents/tests/functional/__init__.py
--------------------------------------------------------------------------------
/extevents/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/extevents/tests/unit/__init__.py
--------------------------------------------------------------------------------
/extevents/tests/unit/tests_ical_core.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | from icalendar.prop import vRecur
4 |
5 | from elk.utils.testing import TestCase, create_teacher
6 | from extevents.models import GoogleCalendar
7 |
8 |
9 | class TestIcalGenericUtils(TestCase):
10 | def setUp(self):
11 | self.teacher = create_teacher()
12 | self.src = GoogleCalendar(
13 | teacher=self.teacher,
14 | url='http://testing'
15 | )
16 |
17 | def test_rrule_appends_timezone(self):
18 | rrule = vRecur(
19 | BYMONTHDAY=[3],
20 | FREQ=['MONTHLY'],
21 | UNTIL=[datetime.date(2016, 8, 31)]
22 | )
23 |
24 | s = self.src._build_generating_rule(rrule)
25 | self.assertIn('Z;', s) # should contain a UTC identifier
26 |
27 | def test_rrule_does_not_append_timezone_when_it_is_set(self):
28 | rrule = vRecur(
29 | BYMONTHDAY=[3],
30 | FREQ=['MONTHLY'],
31 | UNTIL=[self.tzdatetime('UTC', 2016, 8, 31, 15, 0)]
32 | )
33 | s = self.src._build_generating_rule(rrule)
34 | self.assertIn('Z;', s) # should contain a UTC identifier
35 |
--------------------------------------------------------------------------------
/extevents/tests/unit/tests_manager.py:
--------------------------------------------------------------------------------
1 | from mixer.backend.django import mixer
2 |
3 | from extevents.models import ExternalEvent
4 | from extevents.tests import GoogleCalendarTestCase
5 |
6 |
7 | class TestExternalEventManager(GoogleCalendarTestCase):
8 | def test_by_src(self):
9 | for i in range(0, 10):
10 | mixer.blend(ExternalEvent, teacher=self.teacher, src=self.src)
11 |
12 | self.assertEqual(ExternalEvent.objects.by_src(self.src).count(), 10)
13 |
--------------------------------------------------------------------------------
/lessons/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/lessons/__init__.py
--------------------------------------------------------------------------------
/lessons/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from elk.admin import ModelAdmin
4 | from lessons import models
5 |
6 |
7 | @admin.register(models.Language)
8 | class LanguageAdmin(ModelAdmin):
9 | pass
10 |
11 |
12 | class HostedLessonAdmin(ModelAdmin):
13 | """
14 | Abstract admin for the lessons.
15 | """
16 | list_display = ('host', '__str__', 'duration')
17 | list_filter = (
18 | ('host', admin.RelatedOnlyFieldListFilter),
19 | )
20 |
21 | def get_queryset(self, request):
22 | """
23 | Hide lessons without host from administrators. Lessons without host are used
24 | for subscriptions.
25 | """
26 | return super().get_queryset(request).filter(host__isnull=False)
27 |
28 |
29 | @admin.register(models.PairedLesson)
30 | class PairedLessonAdmin(HostedLessonAdmin):
31 | pass
32 |
33 |
34 | @admin.register(models.HappyHour)
35 | class HappyHourAdmin(HostedLessonAdmin):
36 | pass
37 |
38 |
39 | @admin.register(models.MasterClass)
40 | class MasterClassAdmin(HostedLessonAdmin):
41 | pass
42 |
--------------------------------------------------------------------------------
/lessons/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/lessons/api/__init__.py
--------------------------------------------------------------------------------
/lessons/assets/lessons.styl:
--------------------------------------------------------------------------------
1 | img.lesson
2 | height: 200px
3 | width: 200px
4 | border-radius: 5px
--------------------------------------------------------------------------------
/lessons/fixtures/lessons-fedor.yaml:
--------------------------------------------------------------------------------
1 | - fields: {active: 1, announce: This is Fedor's master class, description: This is
2 | Fedor's master class, duration: '00:30:00', host: 1, internal_name: Fedor's
3 | master class, name: Fedor's master class, slots: 8}
4 | model: lessons.masterclass
5 | pk: 501
6 |
--------------------------------------------------------------------------------
/lessons/migrations/0002_auto_20160701_0524.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('lessons', '0001_squashed_0002_event'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterModelOptions(
15 | name='happyhour',
16 | options={'verbose_name': 'Happy Hour'},
17 | ),
18 | migrations.AlterModelOptions(
19 | name='lessonwithnative',
20 | options={'verbose_name_plural': 'Curated lessons with native speaker', 'verbose_name': 'Curataed lesson with native speaker'},
21 | ),
22 | migrations.AlterModelOptions(
23 | name='masterclass',
24 | options={'verbose_name_plural': 'Master Classes', 'verbose_name': 'Master Class'},
25 | ),
26 | migrations.AlterModelOptions(
27 | name='pairedlesson',
28 | options={'verbose_name': 'Paired Lesson'},
29 | ),
30 | ]
31 |
--------------------------------------------------------------------------------
/lessons/migrations/0003_event_slots.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('lessons', '0002_auto_20160701_0524'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='event',
16 | name='slots',
17 | field=models.SmallIntegerField(default=1),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/lessons/migrations/0004_auto_20160713_1532.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.conf import settings
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12 | ('lessons', '0003_event_slots'),
13 | ]
14 |
15 | operations = [
16 | migrations.RemoveField(
17 | model_name='event',
18 | name='host',
19 | ),
20 | migrations.RemoveField(
21 | model_name='event',
22 | name='lesson_type',
23 | ),
24 | migrations.AddField(
25 | model_name='happyhour',
26 | name='host',
27 | field=models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True, related_name='+'),
28 | ),
29 | migrations.AddField(
30 | model_name='masterclass',
31 | name='host',
32 | field=models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True, related_name='+'),
33 | ),
34 | migrations.DeleteModel(
35 | name='Event',
36 | ),
37 | ]
38 |
--------------------------------------------------------------------------------
/lessons/migrations/0005_auto_20160719_0735.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('lessons', '0004_auto_20160713_1532'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='happyhour',
16 | name='host',
17 | field=models.ForeignKey(to='teachers.Teacher', related_name='+', null=True),
18 | ),
19 | migrations.AlterField(
20 | model_name='masterclass',
21 | name='host',
22 | field=models.ForeignKey(to='teachers.Teacher', related_name='+', null=True),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/lessons/migrations/0006_auto_20160722_1351.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('lessons', '0005_auto_20160719_0735'),
11 | ('teachers', '0005_teacher_description'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterModelOptions(
16 | name='lessonwithnative',
17 | options={'verbose_name_plural': 'Native speakers', 'verbose_name': 'Native speaker session'},
18 | ),
19 | migrations.AlterModelOptions(
20 | name='ordinarylesson',
21 | options={'verbose_name': 'Curated session'},
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/lessons/migrations/0008_pairedlesson_host.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('teachers', '0009_auto_20160813_1302'),
11 | ('lessons', '0007_auto_20160728_1337'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='pairedlesson',
17 | name='host',
18 | field=models.ForeignKey(related_name='+', null=True, to='teachers.Teacher'),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/lessons/migrations/0009_auto_20160919_1200.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('lessons', '0008_pairedlesson_host'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterModelOptions(
15 | name='lessonwithnative',
16 | options={'verbose_name_plural': 'Native speaker sessions', 'verbose_name': 'Native speaker'},
17 | ),
18 | migrations.AlterModelOptions(
19 | name='ordinarylesson',
20 | options={'verbose_name_plural': 'Single lessons', 'verbose_name': 'Single session'},
21 | ),
22 | migrations.AlterModelOptions(
23 | name='pairedlesson',
24 | options={'verbose_name_plural': 'Paired lessons', 'verbose_name': 'Paired session'},
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/lessons/migrations/0009_language.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('lessons', '0008_pairedlesson_host'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='Language',
16 | fields=[
17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
18 | ('name', models.CharField(max_length=140)),
19 | ],
20 | options={'ordering': ['name']},
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/lessons/migrations/0010_merge.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('lessons', '0009_language'),
11 | ('lessons', '0009_auto_20160919_1200'),
12 | ]
13 |
14 | operations = [
15 | ]
16 |
--------------------------------------------------------------------------------
/lessons/migrations/0011_auto_20160926_1543.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('lessons', '0010_merge'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterModelOptions(
15 | name='ordinarylesson',
16 | options={'verbose_name': 'Single lesson', 'verbose_name_plural': 'Single lessons'},
17 | ),
18 | migrations.AlterModelOptions(
19 | name='pairedlesson',
20 | options={'verbose_name': 'Paired lesson', 'verbose_name_plural': 'Paired lessons'},
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/lessons/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/lessons/migrations/__init__.py
--------------------------------------------------------------------------------
/lessons/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/lessons/tests/__init__.py
--------------------------------------------------------------------------------
/lessons/tests/functional/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/lessons/tests/functional/__init__.py
--------------------------------------------------------------------------------
/lessons/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/lessons/tests/unit/__init__.py
--------------------------------------------------------------------------------
/mailer/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/mailer/__init__.py
--------------------------------------------------------------------------------
/mailer/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/mailer/migrations/__init__.py
--------------------------------------------------------------------------------
/mailer/tasks.py:
--------------------------------------------------------------------------------
1 | from elk.celery import app as celery
2 |
3 |
4 | @celery.task
5 | def send_email(owl):
6 | owl.msg.send()
7 |
--------------------------------------------------------------------------------
/mailer/templates/mailer/_signature.html:
--------------------------------------------------------------------------------
1 | Take care, yours ELK Academy.
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/mailer/templates/mailer/test.html:
--------------------------------------------------------------------------------
1 | {% extends "mail_templated/base.tpl" %}
2 | {% load humanize %}
3 |
4 | {% block subject %}
5 | Проверка сабжекта для {{username}}
6 | {% endblock %}
7 |
8 | {% block html %}
9 | Hi {{ full_name }},
10 |
11 | You just signed up for my website, using:
12 |
13 | username {{ username }}
14 | join date {{ register_date }}
15 | Current time {{ time|date:"d.m.Y H:i" }}
16 | Current time in russian (should be in english) {{ time|naturalday }}
17 |
18 |
19 |
20 | Thanks, you rock!
21 | {% endblock %}
22 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "elk.settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/market/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = 'market.apps.MarketConfig'
2 |
--------------------------------------------------------------------------------
/market/admin/__init__.py:
--------------------------------------------------------------------------------
1 | from . import subscriptions # noqa
2 | from . import classes # noqa
3 |
--------------------------------------------------------------------------------
/market/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class MarketConfig(AppConfig):
5 | name = 'market'
6 | verbose_name = 'Market'
7 |
--------------------------------------------------------------------------------
/market/assets/cancel_popup/cancel_popup.coffee:
--------------------------------------------------------------------------------
1 | #
2 | # For working with this plugin, control should define `data-dismiss-after-cancellation`
3 | # with a selector of element, which to remove after successful unscheduling
4 | #
5 |
6 | $('.class_cancel').on 'click', (e) ->
7 | $btn = $ e.target
8 |
9 | class_id = $btn.data 'class-id'
10 | $popup = $ '.class_cancel-popup-container'
11 |
12 |
13 | $popup.load "/market/cancel/#{class_id}/popup", () ->
14 | $popup.modal 'show'
15 |
16 | # request the server when user has confirmed actual cancellation
17 | $('button[data-class-cancellation-url]').on 'click', (e) ->
18 | $this = $ e.target
19 |
20 | $this.button 'loading'
21 |
22 | url = $this.data 'class-cancellation-url'
23 | $.getJSON url, (response) ->
24 | $popup.modal 'hide'
25 | $this.button 'reset'
26 |
27 | $('.dismiss-after-class-cancellation').remove()
28 |
29 | # reset 'Plan another lesson' to 'Plan a lesson'
30 | $('.homepage-big-blue-button .load_schedule_popup').html 'Plan a lesson'
31 | # sorry :(
32 |
33 | e.preventDefault()
34 |
--------------------------------------------------------------------------------
/market/assets/cancel_popup/style.styl:
--------------------------------------------------------------------------------
1 | .class_cancel_popup
2 | &__submit
3 | min-width: 140px
4 |
--------------------------------------------------------------------------------
/market/assets/customer_lessons/customer_lessons.styl:
--------------------------------------------------------------------------------
1 | table.customer-lessons
2 | margin-top: 30px
3 |
--------------------------------------------------------------------------------
/market/assets/lessons_starting_soon/lessons_starting_soon.styl:
--------------------------------------------------------------------------------
1 | .lessons-starting-soon // container
2 | margin-top: 40px
3 | h2
4 | margin-bottom: 30px
5 |
6 | .lesson-starting-soon
7 | display: inline-block
8 | margin-right: 50px
9 | min-height: 380px
10 | vertical-align: top
11 |
12 | img
13 | margin-bottom: 10px
14 |
15 | &__title
16 | margin-bottom: 0
17 |
18 | &__author
19 | opacity: .5
20 | margin-bottom: 5px
21 |
22 | &__timeslots
23 | list-style-type: none
24 | padding: 0
--------------------------------------------------------------------------------
/market/assets/next_lesson/style.styl:
--------------------------------------------------------------------------------
1 | @import nib
2 | .next_class
3 | &__actions
4 | margin-top: 5px
5 |
6 | &__msg, &__sign
7 | display: inline
8 |
9 | &__msg
10 | margin-bottom: 5px
11 |
12 | &__skype
13 | margin-top: 10px
14 |
15 | &__sign
16 | color: #3c763d // 'success' color from bootstrap
17 | absolute: left -4px top 4px;
18 |
19 | @media (max-width: 768px)
20 | display: block
21 | margin-top: 25px
22 | padding-top: 25px
23 | border-top: 1px solid rgba(0, 0, 0, .1)
24 | &__sign
25 | display: none
26 |
--------------------------------------------------------------------------------
/market/assets/schedule_popup/popup-mobile-scroll.coffee:
--------------------------------------------------------------------------------
1 | #
2 | # Spies the scroll of scheduling popup
3 | # Hides filters when the position X is reached
4 | #
5 |
6 | $('.schedule-popup-container').on 'show.bs.modal', () ->
7 | if $('body').hasClass 'desktop'
8 | return
9 |
10 | $('.schedule-popup__content').on 'scroll', () ->
11 | $content = $ this
12 | $filters = $content.siblings('.schedule-popup__filters').find '.lesson_type'
13 |
14 | position = $content.scrollTop()
15 |
16 | if position > 150 and $filters.css('display') isnt 'none'
17 | $filters.css 'display', 'none'
18 | else if position < 20
19 | $filters.css 'display', 'block'
20 |
--------------------------------------------------------------------------------
/market/assets/subsription_status/style.styl:
--------------------------------------------------------------------------------
1 | .subscription-status
2 | td:first-child
3 | width: 1%
4 | white-space: nowrap
5 | padding-right: 10px
6 | text-align: right
7 | p:after
8 | content: ':'
9 |
10 | td.&__title
11 | padding-right: 5px
12 |
13 | td.&__status
14 | padding-left: 0
15 |
16 | &__progress
17 | max-width: 200px
18 |
19 | p
20 | margin: 0
21 | .progress
22 | margin-bottom: 0
23 |
--------------------------------------------------------------------------------
/market/assets/timeline_entry_popup/timeline_entry_popup.coffee:
--------------------------------------------------------------------------------
1 | $('.load-timeline-entry-popup').on 'click', (e) ->
2 | e.preventDefault()
3 |
4 | timeline_entry_id = $(this).data 'entry-id'
5 |
6 | $popup = $ '.timeline-entry-popup-container'
7 |
8 | $popup.load "/market/schedule/#{timeline_entry_id}", () ->
9 | $popup.modal 'show'
--------------------------------------------------------------------------------
/market/assets/timeline_entry_popup/timeline_entry_popup.styl:
--------------------------------------------------------------------------------
1 | @import 'nib'
2 |
3 | .timeline-entry-popup
4 | .modal-body
5 | min-height: 350px
6 |
7 | &__lesson-photo
8 | float: left
9 | margin-right: 25px
10 |
11 | &__head
12 | margin-bottom: 20px
13 |
14 | &__start,
15 | &__students
16 | margin-bottom: 0
17 |
18 | &__author
19 | relative: top -5px
20 | margin-bottom: 5px
21 | img, p
22 | display: inline
23 |
24 | p
25 | margin-left: 5px
26 |
27 | &__start
28 | font-weight: bold
29 |
--------------------------------------------------------------------------------
/market/exceptions.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import ValidationError
2 |
3 |
4 | class CannotBeScheduled(Exception):
5 | """
6 | Indicates a situation when trying to schedule lesson that does not suite
7 | to a timeline entry
8 | """
9 | pass
10 |
11 |
12 | class AutoScheduleExpcetion(ValidationError):
13 | """
14 | Indicates error in AutoSchedule, when teacher is not available
15 | """
16 | pass
17 |
--------------------------------------------------------------------------------
/market/migrations/0002_auto_20160818_1546.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('market', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterModelOptions(
15 | name='subscription',
16 | options={'ordering': ('buy_date',)},
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/market/migrations/0004_auto_20161005_1552.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('market', '0003_auto_20160929_0355'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='class',
16 | name='active',
17 | field=models.SmallIntegerField(choices=[(0, 'Inactive'), (1, 'Active')], db_index=True, default=1),
18 | ),
19 | migrations.AlterField(
20 | model_name='class',
21 | name='is_fully_used',
22 | field=models.BooleanField(db_index=True, default=False),
23 | ),
24 | migrations.AlterField(
25 | model_name='subscription',
26 | name='active',
27 | field=models.SmallIntegerField(choices=[(0, 'Inactive'), (1, 'Active')], db_index=True, default=1),
28 | ),
29 | migrations.AlterField(
30 | model_name='subscription',
31 | name='is_fully_used',
32 | field=models.BooleanField(db_index=True, default=False),
33 | ),
34 | ]
35 |
--------------------------------------------------------------------------------
/market/migrations/0005_remove_class_lesson_id.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('market', '0004_auto_20161005_1552'),
11 | ]
12 |
13 | operations = [
14 | migrations.RemoveField(
15 | model_name='class',
16 | name='lesson_id',
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/market/migrations/0006_auto_20161117_1139.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('market', '0005_remove_class_lesson_id'),
11 | ]
12 |
13 | operations = [
14 | migrations.RemoveField(
15 | model_name='class',
16 | name='active',
17 | ),
18 | migrations.RemoveField(
19 | model_name='subscription',
20 | name='active',
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/market/migrations/0007_subscription_duration.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from datetime import timedelta
5 |
6 | from django.db import migrations, models
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | ('market', '0006_auto_20161117_1139'),
13 | ]
14 |
15 | operations = [
16 | migrations.AddField(
17 | model_name='subscription',
18 | name='duration',
19 | field=models.DurationField(editable=False, default=timedelta(days=7 * 6)),
20 | preserve_default=False,
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/market/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/market/migrations/__init__.py
--------------------------------------------------------------------------------
/market/templates/mail/class/student/cancelled.html:
--------------------------------------------------------------------------------
1 | {% extends "mail_templated/base.tpl" %}
2 | {% load humanize %}
3 |
4 | {% block subject %}
5 | Your {{ c.timeline.event_title }} is cancelled :(
6 | {% endblock %}
7 |
8 | {% block body %}
9 | Dear {{ c.customer.first_name | capfirst }},
10 |
11 | {% if src == 'customer' %}You've{% else %}We've{% endif %} cancelled your {{ c.timeline.event_title }}
12 | planned for {{ c.timeline.start | naturalday:'SHORT_DATE_FORMAT'}}, {{ c.timeline.start | time:'TIME_FORMAT'}}.
13 |
14 | {% if src != 'customer' %}Sorry for that :({% endif %}
15 |
16 | If you have any questions — simply reply to this message and we'll help you.
17 |
18 | {% include 'mailer/_signature.html' %}
19 | {% endblock %}
20 |
--------------------------------------------------------------------------------
/market/templates/mail/class/student/scheduled.html:
--------------------------------------------------------------------------------
1 | {% extends "mail_templated/base.tpl" %}
2 | {% load humanize %}
3 |
4 | {% block subject %}
5 | {{ c.customer.first_name | capfirst }}, {{ c.timeline.event_title }} is scheduled for {{ c.timeline.start | naturalday:'SHORT_DATE_FORMAT' }}, {{ c.timeline.start | time:'TIME_FORMAT'}}
6 | {% endblock %}
7 |
8 | {% block body %}
9 | Dear {{ c.customer.first_name | capfirst }},
10 |
11 | Just for you, we've scheduled {{ c.timeline.event_title }} for {{ c.timeline.start | naturalday:'SHORT_DATE_FORMAT'}}, {{ c.timeline.start | time:'TIME_FORMAT'}}
12 |
13 | {% include 'mailer/_signature.html' %}
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/market/templates/mail/class/teacher/cancelled.html:
--------------------------------------------------------------------------------
1 | {% extends "mail_templated/base.tpl" %}
2 | {% load humanize %}
3 |
4 | {% block subject %}
5 | CANCELLED: {{ c.customer.full_name }}
6 | {% endblock %}
7 |
8 | {% block body %}
9 | Hi, {{ c.timeline.teacher.user.crm.first_name | capfirst }}!
10 |
11 |
12 | {{ c.customer.full_name }} has cancelled his lesson planned for {{ c.timeline.start | naturalday:'SHORT_DATE_FORMAT' }}, {{ c.timeline.start | time:'TIME_FORMAT'}} :(
13 |
14 | Cancellation source: {{ src }}.
15 |
16 | {% include 'mailer/_signature.html' %}
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/market/templates/mail/class/teacher/scheduled.html:
--------------------------------------------------------------------------------
1 | {% extends "mail_templated/base.tpl" %}
2 | {% load humanize %}
3 |
4 | {% block subject %}
5 | {{ c.customer.full_name | capfirst}} scheduled a class for {{ c.timeline.start | naturalday:'SHORT_DATE_FORMAT' }} at {{ c.timeline.start | time:'TIME_FORMAT'}}
6 | {% endblock %}
7 |
8 | {% block body %}
9 | Hi, {{ c.timeline.teacher.user.crm.first_name | capfirst }}!
10 |
11 | {{ c.customer.full_name | capfirst}} have scheduled a {{ c.timeline.lesson.type_verbose_name }} with you for {{ c.timeline.start | naturalday:'SHORT_DATE_FORMAT' }}, {{ c.timeline.start | time:'TIME_FORMAT'}}
12 |
13 | {% include 'mailer/_signature.html' %}
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/market/templates/market/_partial/lessons_starting_soon.html:
--------------------------------------------------------------------------------
1 | {% if request.user.crm.classes.hosted_lessons_starting_soon %}
2 |
3 |
4 |
Interesting lessons free for booking
5 | {% for lesson in request.user.crm.classes.hosted_lessons_starting_soon %}
6 |
7 |
8 |
{{ lesson.name }}
9 |
{{ lesson.host.user.crm.full_name }}
10 |
11 | {% for timeline_entry in lesson.get_timeline_entries %}
12 |
15 | {% endfor %}
16 |
17 |
18 | {% endfor %}
19 |
20 |
21 | {% endif %}
--------------------------------------------------------------------------------
/market/templates/market/_partial/next_lesson.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load skype %}
3 | {% load contact_us %}
4 |
5 | {% with next_class=request.user.crm.classes.nearest_scheduled %}
6 | {% if next_class %}
7 |
8 |
9 |
{% trans "You are going to have a " %} {{ next_class.lesson_type | lower }}
10 | {% trans 'with' %} {{ next_class.timeline.teacher.user.crm.full_name }},
11 | {% trans 'starting at' %} {{ next_class.timeline.start | date:"SHORT_DATETIME_FORMAT" }} .
12 | If something goes wrong, {% contact_us 'contact us' 'pseudo class_reschedule' %} {% trans 'or' %}
13 | {% trans 'cancel' %} it.
14 |
15 |
{{ next_class.timeline.teacher.user.crm.first_name | capfirst }}'s skype is {% skype_chat next_class.timeline.teacher.user.crm %}.
16 |
17 | {% endif %}
18 | {% endwith %}
19 |
--------------------------------------------------------------------------------
/market/templates/market/cancel_form.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/market/templates/market/cancel_popup/index.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
21 |
--------------------------------------------------------------------------------
/market/templates/market/cancel_popup/sorry.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
20 |
--------------------------------------------------------------------------------
/market/templates/market/schedule_form.html:
--------------------------------------------------------------------------------
1 | {% if request.user.crm.can_schedule_classes %}
2 |
5 | {% endif %}
6 |
--------------------------------------------------------------------------------
/market/templates/market/timeline_entry_form.html:
--------------------------------------------------------------------------------
1 | {% if request.user.crm.can_schedule_classes %}
2 |
4 | {% endif %}
--------------------------------------------------------------------------------
/market/templatetags/market/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/market/templatetags/market/__init__.py
--------------------------------------------------------------------------------
/market/tests/AutoSchedule/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/market/tests/AutoSchedule/__init__.py
--------------------------------------------------------------------------------
/market/tests/SortingHat/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/market/tests/SortingHat/__init__.py
--------------------------------------------------------------------------------
/market/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/market/tests/__init__.py
--------------------------------------------------------------------------------
/market/tests/functional/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/market/tests/functional/__init__.py
--------------------------------------------------------------------------------
/market/tests/functional/tests_timeline_entry_popup.py:
--------------------------------------------------------------------------------
1 | from freezegun import freeze_time
2 | from mixer.backend.django import mixer
3 |
4 | from elk.utils.testing import ClientTestCase, create_teacher
5 | from lessons import models as lessons
6 | from timeline.models import Entry as TimelineEntry
7 |
8 |
9 | @freeze_time('2032-12-01 12:00')
10 | class TimelineEntryPopupTestCase(ClientTestCase):
11 | @classmethod
12 | def setUpTestData(cls):
13 | cls.host = create_teacher(works_24x7=True)
14 |
15 | cls.lesson = mixer.blend(lessons.MasterClass, host=cls.host, photo=mixer.RANDOM)
16 |
17 | cls.entry = mixer.blend(
18 | TimelineEntry,
19 | teacher=cls.host,
20 | lesson=cls.lesson,
21 | start=cls.tzdatetime(2032, 12, 5, 13, 00)
22 | )
23 |
24 | def test_popup_loading_fail(self):
25 | result = self.c.get('/market/schedule/100500/') # non-existant ID
26 | self.assertEqual(result.status_code, 404)
27 |
28 | def test_popup_loading_ok(self):
29 | result = self.c.get('/market/schedule/%d/' % self.entry.pk)
30 | self.assertEqual(result.status_code, 200)
31 |
--------------------------------------------------------------------------------
/market/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/market/tests/unit/__init__.py
--------------------------------------------------------------------------------
/market/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url
2 |
3 | from . import views
4 |
5 | urlpatterns = [
6 | url(r'mylessons/$', views.CustomerLessons.as_view(), name='customer_lessons'),
7 | url(r'(?P[\d\-]+)/type/(?P\d+)/teachers.json$', views.teachers, name='teachers'),
8 | url(r'(?P[\d\-]+)/type/(?P\d+)/lessons.json$', views.lessons, name='lessons'),
9 |
10 | url(regex=r'schedule/(?P\d+)/',
11 | view=views.TimelineEntryPopup.as_view(),
12 | name='timeline_entry_popup',
13 | ),
14 |
15 | url(regex=r'schedule/step2/teacher/(?P\d+)/(?P\d+)/(?P[\d-]+)/(?P[\d:]{5})/',
16 | view=views.step2,
17 | name='step2'
18 | ),
19 | url(regex=r'schedule/step1/',
20 | view=views.step1,
21 | name='step01'
22 | ),
23 | url(regex=r'cancel/(?P\d+)/popup/',
24 | view=views.cancel_popup,
25 | name='cancel_popup'
26 | ),
27 | url(regex=r'cancel/(?P\d+)/$',
28 | view=views.cancel,
29 | name='cancel',
30 | ),
31 | ]
32 |
--------------------------------------------------------------------------------
/media/.gitignore:
--------------------------------------------------------------------------------
1 | *
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "elk-dashboard",
3 | "version": "1.0.0",
4 | "private": "true",
5 | "description": "",
6 | "main": "Gulpfile.coffee",
7 | "dependencies": {
8 | "coffee-script": "^1.10.0",
9 | "gulp": "^3.9.1",
10 | "gulp-coffee": "^2.3.2",
11 | "gulp-coffeelint": "^0.6.0",
12 | "gulp-concat": "^2.6.0",
13 | "gulp-if": "^2.0.1",
14 | "gulp-load-plugins": "^1.2.4",
15 | "gulp-sourcemaps": "^1.6.0",
16 | "gulp-stylint": "^3.0.0",
17 | "gulp-stylus": "^2.5.0",
18 | "gulp-uglify": "^1.5.4",
19 | "gulp-uglifycss": "^1.0.6",
20 | "gulp-util": "^3.0.7",
21 | "lazypipe": "^1.0.1",
22 | "nib": "^1.1.0",
23 | "run-sequence": "^1.2.2",
24 | "stylus": "^0.54.5"
25 | },
26 | "devDependencies": {},
27 | "scripts": {
28 | "test": "echo \"No tests yet, sorry\" && exit 0"
29 | },
30 | "repository": {
31 | "type": "git",
32 | "url": "git+https://github.com/f213/elk-back-office.git"
33 | },
34 | "author": "Fedor Borshev (http://f213.in/)",
35 | "license": "MIT",
36 | "bugs": {
37 | "url": "https://github.com/f213/elk-back-office/issues"
38 | },
39 | "homepage": "https://github.com/f213/elk-back-office#readme"
40 | }
41 |
--------------------------------------------------------------------------------
/payments/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/payments/__init__.py
--------------------------------------------------------------------------------
/payments/assets/stripe.styl:
--------------------------------------------------------------------------------
1 | .payment-processing-popup
2 | h3
3 | margin 0
4 |
5 | .modal-header
6 | padding-bottom: 0
7 | border-bottom: none
8 |
9 | .progress
10 | margin-bottom: 0
11 |
--------------------------------------------------------------------------------
/payments/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/payments/migrations/__init__.py
--------------------------------------------------------------------------------
/payments/stripe.py:
--------------------------------------------------------------------------------
1 | import stripe
2 | from django.conf import settings
3 |
4 | STRIPE_CURRENCY_MULTIPLIERS = {
5 | """
6 | This is a list of multipliers, that convert moneyfield ammount to stripe ammount.
7 |
8 | Stripe accepts only smallest currency units — cents for dollars, копейки for rubles
9 | see https://support.stripe.com/questions/which-zero-decimal-currencies-does-stripe-support
10 | """
11 | 'JPY': 1,
12 | }
13 |
14 |
15 | def get_stripe_instance():
16 | """
17 | Return a pre-configured stripe instance
18 | """
19 | stripe.api_key = settings.STRIPE_API_KEY
20 |
21 | return stripe
22 |
23 |
24 | def stripe_amount(cost):
25 | """
26 | Returns a strip amount — smalles currency unit
27 | """
28 | multiplyer = STRIPE_CURRENCY_MULTIPLIERS.get(str(cost.currency), 100) # default multiplier is 100, 1 USD is 100 cents
29 |
30 | return int(cost.amount * multiplyer)
31 |
32 |
33 | def stripe_currency(cost):
34 | """
35 | Returns an ISO-4217 currency code, understandable by stripe
36 | """
37 |
38 | return str(cost.currency)
39 |
--------------------------------------------------------------------------------
/payments/templates/payments/_partial/processing-popup.html:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/payments/templates/payments/_partial/stripe.html:
--------------------------------------------------------------------------------
1 | {# For internal use. Please use templatetag, for usage examples see payments/tests/functional/tests_payment_form.py #}
2 | {{ caption }}
10 |
--------------------------------------------------------------------------------
/payments/templates/payments/result_base.html:
--------------------------------------------------------------------------------
1 | {% extends 'elk/base.html' %}
2 |
3 | {% block content %}
4 |
5 |
6 |
9 | {% block lead %}
10 |
Text lead.
11 | {% endblock %}
12 | {% block msg %}
13 |
If you beleave this is an error, drop us a like .
14 | {% endblock %}
15 |
16 |
17 | {% block call_to_action %}{% endblock %}
18 | {% endblock %}
19 |
--------------------------------------------------------------------------------
/payments/templates/payments/result_failure.html:
--------------------------------------------------------------------------------
1 | {% extends 'payments/result_base.html' %}
2 | {% load contact_us %}
3 |
4 | {% block title %}Payment error :-({% endblock %}
5 | {% block lead %}Payment is denied by payment system
{% endblock %}
6 |
7 | {% block msg %}
8 |
9 | All we know is that Stripe says: {{ msg | lower }} The best solution is to drop a line to us.
10 |
11 | {% endblock %}
12 |
13 | {% block call_to_action %}
14 |
20 | {% endblock %}
21 |
--------------------------------------------------------------------------------
/payments/templates/payments/single_lesson_success.html:
--------------------------------------------------------------------------------
1 | {% extends 'payments/result_base.html' %}
2 | {% load contact_us %}
3 |
4 | {% block title %}Thank you!{% endblock %}
5 |
6 | {% block lead %}
7 | Your lesson is now activated.
8 | {% endblock %}
9 |
10 | {% block msg %}
11 | Go to the home page and find your lesson. Don't hesitate to {% contact_us 'drop us a line' 'pseudo'%}
12 | if you have any questions.
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/payments/templates/payments/subscription_success.html:
--------------------------------------------------------------------------------
1 | {% extends 'payments/result_base.html' %}
2 | {% load contact_us %}
3 |
4 | {% block title %}Thank you!{% endblock %}
5 |
6 | {% block lead %}
7 | Your {{ product.name }} is now activated.
8 | {% endblock %}
9 |
10 | {% block msg %}
11 | Go to the home page and find your first lesson. Don't hesitate to {% contact_us 'drop us a line' 'pseudo'%}
12 | if you have any questions.
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/payments/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/payments/templatetags/__init__.py
--------------------------------------------------------------------------------
/payments/templatetags/stripe.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.conf import settings
3 | from django.contrib.contenttypes.models import ContentType
4 | from django.template.loader import get_template
5 |
6 | from payments.stripe import stripe_amount, stripe_currency
7 |
8 | register = template.Library()
9 |
10 |
11 | @register.simple_tag(takes_context=True)
12 | def stripe_form(context, caption, classes, *args, **kwargs):
13 | tpl = get_template('payments/_partial/stripe.html')
14 |
15 | ctx = _ctx(*args, **kwargs)
16 | ctx['csrf_token'] = context.get('csrf_token')
17 |
18 | ctx['caption'] = caption
19 | ctx['classes'] = classes
20 |
21 | return tpl.render(ctx)
22 |
23 |
24 | @register.simple_tag
25 | def stripe_processing_popup():
26 | tpl = get_template('payments/_partial/processing-popup.html')
27 | return tpl.render()
28 |
29 |
30 | def _ctx(product, cost, crm):
31 | return {
32 | 'stripe_pk': settings.STRIPE_PK,
33 | 'amount': str(cost.amount),
34 | 'stripe_amount': stripe_amount(cost),
35 | 'currency': stripe_currency(cost),
36 | 'product': product,
37 | 'product_type': ContentType.objects.get_for_model(product).pk,
38 | 'crm': crm,
39 | }
40 |
--------------------------------------------------------------------------------
/payments/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import MagicMock
2 | from stripe.error import CardError
3 |
4 |
5 | def patch_stripe(p, success=True):
6 | p.stripe = mock_stripe(success)
7 |
8 |
9 | def mock_stripe(success=True):
10 | stripe = MagicMock()
11 |
12 | if success:
13 | stripe.Charge.create = MagicMock(return_value=True)
14 | else:
15 | stripe.Charge.create = MagicMock(side_effect=CardError(message='testing', param='123', code='100500'))
16 |
17 | return stripe
18 |
--------------------------------------------------------------------------------
/payments/tests/functional/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/payments/tests/functional/__init__.py
--------------------------------------------------------------------------------
/payments/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/payments/tests/unit/__init__.py
--------------------------------------------------------------------------------
/payments/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url
2 |
3 | from payments import views
4 |
5 | urlpatterns = [
6 | url(r'process/', views.process, name='process'),
7 | url(r'(?P\d+)/(?P\d+)/success/', views.success, name='success'),
8 | url(r'(?P\d+)/(?P\d+)/failure/', views.failure, name='failure'),
9 | ]
10 |
--------------------------------------------------------------------------------
/products/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/products/__init__.py
--------------------------------------------------------------------------------
/products/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class ProductsConfig(AppConfig):
5 | name = 'products'
6 |
--------------------------------------------------------------------------------
/products/fixtures/products.yaml:
--------------------------------------------------------------------------------
1 | - fields:
2 | active: 1
3 | cost: '150.00'
4 | cost_currency: USD
5 | duration: 42 00:00:00
6 | happy_hours: [500]
7 | internal_name: ELK Base subscription
8 | lessons_with_native: [1000, 1001]
9 | master_classes: [500]
10 | name: ELK Base subscription
11 | ordinary_lessons: [1000, 1001, 1002, 1003, 1004]
12 | paired_lessons: [500]
13 | model: products.product1
14 | pk: 1
15 |
--------------------------------------------------------------------------------
/products/migrations/0004_auto_20161011_1305.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('products', '0003_tiers'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='tier',
16 | name='name',
17 | field=models.CharField(max_length=140, verbose_name='Tier name'),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/products/migrations/0006_remove_tier_paypal_button_id.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('products', '0005_singlelessonproduct'),
11 | ]
12 |
13 | operations = [
14 | migrations.RemoveField(
15 | model_name='tier',
16 | name='paypal_button_id',
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/products/migrations/0007_auto_20161107_0952.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations
5 |
6 |
7 | def rename_single_lesson_product(apps, schema_editor):
8 | SingleLessonProduct = apps.get_model('products.SingleLessonProduct')
9 | SingleLessonProduct.objects.update(
10 | name='Single lesson',
11 | internal_name='Single lesson product'
12 | )
13 |
14 |
15 | class Migration(migrations.Migration):
16 |
17 | dependencies = [
18 | ('products', '0006_remove_tier_paypal_button_id'),
19 | ]
20 |
21 | operations = [
22 | migrations.AlterModelOptions(
23 | name='singlelessonproduct',
24 | options={'verbose_name': 'Single lesson'},
25 | ),
26 | migrations.RunPython(rename_single_lesson_product),
27 | ]
28 |
--------------------------------------------------------------------------------
/products/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/products/migrations/__init__.py
--------------------------------------------------------------------------------
/products/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/products/tests/__init__.py
--------------------------------------------------------------------------------
/products/tests/functional/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/products/tests/functional/__init__.py
--------------------------------------------------------------------------------
/products/tests/functional/tests_shipment.py:
--------------------------------------------------------------------------------
1 | from elk.utils.testing import TestCase, create_customer
2 | from products.models import Product1, SingleLessonProduct
3 |
4 |
5 | class TestShipment(TestCase):
6 | fixtures = ('products', 'lessons')
7 |
8 | def setUp(self):
9 | self.customer = create_customer()
10 |
11 | def test_subscription_shipment(self):
12 | product = Product1.objects.get(pk=1)
13 |
14 | product.ship(self.customer)
15 |
16 | shipped = self.customer.subscriptions.first()
17 |
18 | self.assertEqual(shipped.product, product)
19 |
20 | def test_single_lesson_shipment(self):
21 | product = SingleLessonProduct.objects.first() # should be created in migration
22 | self.assertIsNotNone(product.lesson_type)
23 |
24 | product.ship(self.customer)
25 |
26 | shipped = self.customer.classes.first()
27 | self.assertEqual(shipped.lesson_type, product.lesson_type)
28 |
--------------------------------------------------------------------------------
/products/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/products/tests/unit/__init__.py
--------------------------------------------------------------------------------
/teachers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/teachers/__init__.py
--------------------------------------------------------------------------------
/teachers/admin/__init__.py:
--------------------------------------------------------------------------------
1 | from . import absences # noqa
2 | from . import teachers # noqa
3 |
--------------------------------------------------------------------------------
/teachers/admin/absences.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.utils.translation import ugettext as _
3 |
4 | from elk.admin import ModelAdmin
5 | from teachers.models import Absence
6 |
7 |
8 | class TeacherFilter(admin.SimpleListFilter):
9 | title = _('Teacher')
10 | parameter_name = 'teacher'
11 |
12 | def lookups(self, request, model_admin):
13 | return (
14 | [i.teacher.pk, str(i.teacher)] for i in Absence.objects.distinct('teacher')
15 | )
16 |
17 | def queryset(self, request, queryset):
18 | if not self.value():
19 | return queryset
20 |
21 | return queryset.filter(teacher=self.value())
22 |
23 |
24 | @admin.register(Absence)
25 | class AbsenceAdmin(ModelAdmin):
26 | readonly_fields = ('is_approved',)
27 | list_display = ('teacher', 'type', 'start', 'end')
28 | list_filter = (TeacherFilter, 'type')
29 |
--------------------------------------------------------------------------------
/teachers/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/teachers/api/__init__.py
--------------------------------------------------------------------------------
/teachers/api/viewsets.py:
--------------------------------------------------------------------------------
1 | from django.contrib.contenttypes.models import ContentType
2 | from django.shortcuts import get_object_or_404
3 | from rest_framework import viewsets
4 | from rest_framework.decorators import detail_route
5 | from rest_framework.response import Response
6 |
7 | from lessons.api.serializers import factory as lesson_serializer_factory
8 | from teachers.api.serializers import TeacherSerializer
9 | from teachers.models import Teacher
10 |
11 |
12 | class TeacherViewSet(viewsets.ReadOnlyModelViewSet):
13 | queryset = Teacher.objects.all()
14 | serializer_class = TeacherSerializer
15 |
16 | @detail_route(methods=['GET'])
17 | def available_lessons(self, request, pk=None, format=None):
18 | teacher = self.get_object()
19 | lesson_type = get_object_or_404(ContentType, pk=request.GET['lesson_type'])
20 |
21 | available_lessons = []
22 | for lesson in teacher.available_lessons(lesson_type):
23 | Serializer = lesson_serializer_factory(lesson)
24 | available_lessons.append(
25 | Serializer(lesson).data
26 | )
27 |
28 | return Response(available_lessons)
29 |
--------------------------------------------------------------------------------
/teachers/assets/admin/css/working_hours.styl:
--------------------------------------------------------------------------------
1 |
2 | .dynamic-working_hours
3 | .field-start .datetimeshortcuts // hide the 'now' button when filling working hours.
4 | display none
5 |
6 | .field-end .datetimeshortcuts
7 | display: none
8 |
9 | .inline_label
10 | display: none !important
11 |
--------------------------------------------------------------------------------
/teachers/assets/css/teacher_detail.styl:
--------------------------------------------------------------------------------
1 | .teacher-detail
2 | &__intro
3 | margin-bottom: 50px
4 |
5 | &__timeslots
6 | >h2
7 | margin-bottom: 30px
8 |
9 | &__timeslot-group
10 | h3
11 | margin: 0
12 |
13 | .date, .slots
14 | padding-top: 10px
15 | vertical-align: top
16 | min-height: 130px
17 | border-bottom: solid 1px rgba(0, 0, 0, .3)
18 |
19 | .date
20 | min-width: 170px
21 |
22 |
23 | &__timeslot-group + &__timeslot-group
24 | margin-top: 20px
25 |
--------------------------------------------------------------------------------
/teachers/assets/css/teachers.styl:
--------------------------------------------------------------------------------
1 | @import 'nib'
2 |
3 | $size = 250px
4 | $hover = 270px
5 |
6 | img.teacher-face
7 | width: $size
8 | height: $size
9 |
10 | img.teacher-avatar
11 | width: 25px
12 | height: 25px
13 | border-radius: 50%
14 |
15 | .teacher-grid
16 | .teacher
17 | display: inline-block
18 | text-align: center
19 | vertical-align: top
20 | margin-right: 80px
21 |
22 | @media(min-width: 768px)
23 | &:nth-child(n+5)
24 | margin-top: 40px
25 |
26 | @media(max-width: 768px)
27 | &:nth-child(n+1)
28 | margin-top: 40px
29 |
30 | img
31 | display: block
32 | margin-bottom: 10px
33 |
34 | &-announce
35 | display: block
36 | width: 250px
37 | font-size: 12px
38 |
39 | a
40 |
41 | &:not(:hover) img
42 | transition: all .1s ease-in-out
43 |
44 | &:hover img
45 | // border: 1px solid #337ab7 // bootstrap hover color
46 | transform: scale(1.05)
47 |
--------------------------------------------------------------------------------
/teachers/fixtures/teachers.yaml:
--------------------------------------------------------------------------------
1 | - fields:
2 | name: admins
3 | permissions: []
4 | model: auth.group
5 | pk: 1
6 | - fields:
7 | name: teachers
8 | permissions: []
9 | model: auth.group
10 | pk: 2
11 |
--------------------------------------------------------------------------------
/teachers/migrations/0002_auto_20160715_1257.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import django.db.models.deletion
5 | from django.conf import settings
6 | from django.db import migrations, models
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | ('teachers', '0001_initial'),
13 | ]
14 |
15 | operations = [
16 | migrations.AlterModelOptions(
17 | name='workinghours',
18 | options={'verbose_name_plural': 'Working hours'},
19 | ),
20 | migrations.AlterField(
21 | model_name='teacher',
22 | name='user',
23 | field=models.OneToOneField(to=settings.AUTH_USER_MODEL, related_name='teacher_data', on_delete=django.db.models.deletion.PROTECT),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/teachers/migrations/0003_auto_20160718_1230.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('teachers', '0002_auto_20160715_1257'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='workinghours',
16 | name='weekday',
17 | field=models.IntegerField(verbose_name='Weekday', choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')]),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/teachers/migrations/0004_teacher_acceptable_lessons.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('contenttypes', '0002_remove_content_type_name'),
11 | ('teachers', '0003_auto_20160718_1230'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='teacher',
17 | name='acceptable_lessons',
18 | field=models.ManyToManyField(related_name='+', to='contenttypes.ContentType'),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/teachers/migrations/0005_teacher_description.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import django_markdown.models
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('teachers', '0004_teacher_acceptable_lessons'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='teacher',
17 | name='description',
18 | field=django_markdown.models.MarkdownField(default=''),
19 | preserve_default=False,
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/teachers/migrations/0006_teacher_announce.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import django_markdown.models
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('teachers', '0005_teacher_description'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='teacher',
17 | name='announce',
18 | field=django_markdown.models.MarkdownField(verbose_name='Short description', default=''),
19 | preserve_default=False,
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/teachers/migrations/0007_auto_20160802_1459.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('teachers', '0006_teacher_announce'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='teacher',
16 | name='acceptable_lessons',
17 | field=models.ManyToManyField(to='contenttypes.ContentType', blank=True, related_name='_teacher_acceptable_lessons_+'),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/teachers/migrations/0008_auto_20160809_1816.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('teachers', '0007_auto_20160802_1459'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='workinghours',
16 | name='end',
17 | field=models.TimeField(verbose_name='End hoour(EDT)'),
18 | ),
19 | migrations.AlterField(
20 | model_name='workinghours',
21 | name='start',
22 | field=models.TimeField(verbose_name='Start hour (EDT)'),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/teachers/migrations/0009_auto_20160813_1302.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('contenttypes', '0002_remove_content_type_name'),
11 | ('teachers', '0008_auto_20160809_1816'),
12 | ]
13 |
14 | operations = [
15 | migrations.RenameField(
16 | model_name='teacher',
17 | old_name='acceptable_lessons',
18 | new_name='allowed_lessons',
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/teachers/migrations/0010_teacher_active.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('teachers', '0009_auto_20160813_1302'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='teacher',
16 | name='active',
17 | field=models.IntegerField(default=1, choices=[(0, 'Inactive'), (1, 'Active')]),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/teachers/migrations/0011_absence.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('teachers', '0010_teacher_active'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='Absence',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, serialize=False, primary_key=True, verbose_name='ID')),
18 | ('type', models.CharField(choices=[('vacation', 'Vacation'), ('unpaid', 'Unpaid'), ('sick', 'Sick leave'), ('bonus', 'Bonus vacation'), ('srv', 'System-intiated vacation')], max_length=32, default='srv')),
19 | ('start', models.DateTimeField(verbose_name='Start')),
20 | ('end', models.DateTimeField(verbose_name='End')),
21 | ('add_date', models.DateTimeField(auto_now_add=True)),
22 | ('is_approved', models.BooleanField(default=True)),
23 | ('teacher', models.ForeignKey(to='teachers.Teacher', related_name='absences')),
24 | ],
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/teachers/migrations/0012_auto_20160910_1235.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('teachers', '0011_absence'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterModelOptions(
15 | name='teacher',
16 | options={'verbose_name': 'Teacher profile'},
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/teachers/migrations/0013_auto_20160925_1001.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('teachers', '0012_auto_20160910_1235'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='workinghours',
16 | name='end',
17 | field=models.TimeField(verbose_name='End hour (EDT)'),
18 | ),
19 | migrations.AlterField(
20 | model_name='workinghours',
21 | name='start',
22 | field=models.TimeField(verbose_name='Start hour (EDT)'),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/teachers/migrations/0014_auto_20160925_1134.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('teachers', '0013_auto_20160925_1001'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='teacher',
16 | name='announce',
17 | field=models.TextField(max_length=140),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/teachers/migrations/0015_auto_20161008_1522.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import image_cropping.fields
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('teachers', '0014_auto_20160925_1134'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='teacher',
17 | name='teacher_photo',
18 | field=models.ImageField(blank=True, upload_to='teachers/', null=True),
19 | ),
20 | migrations.AddField(
21 | model_name='teacher',
22 | name='teacher_photo_cropping',
23 | field=image_cropping.fields.ImageRatioField('teacher_photo', '500x500', adapt_rotation=False, verbose_name='teacher photo cropping', hide_image_field=False, size_warning=False, free_crop=False, help_text=None, allow_fullsize=False),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/teachers/migrations/0016_teacher_teacher_avatar_cropping.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import image_cropping.fields
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('teachers', '0015_auto_20161008_1522'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='teacher',
17 | name='teacher_avatar_cropping',
18 | field=image_cropping.fields.ImageRatioField('teacher_photo', '80x80', size_warning=False, hide_image_field=False, verbose_name='teacher avatar cropping', allow_fullsize=False, help_text=None, free_crop=False, adapt_rotation=False),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/teachers/migrations/0017_remove_teacher_description.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('teachers', '0016_teacher_teacher_avatar_cropping'),
11 | ]
12 |
13 | operations = [
14 | migrations.RemoveField(
15 | model_name='teacher',
16 | name='description',
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/teachers/migrations/0018_teacher_title.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('teachers', '0017_remove_teacher_description'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='teacher',
16 | name='title',
17 | field=models.TextField(max_length=32, blank=True),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/teachers/migrations/0019_teacher_ordering.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('teachers', '0018_teacher_title'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterModelOptions(
15 | name='teacher',
16 | options={'verbose_name': 'Teacher profile', 'ordering': ['user__last_name']},
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/teachers/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/teachers/migrations/__init__.py
--------------------------------------------------------------------------------
/teachers/slot_list.py:
--------------------------------------------------------------------------------
1 | from sortedcontainers import SortedList
2 |
3 |
4 | class SlotList(SortedList):
5 | """
6 | List os timeslots, serializers with two values: server and user time
7 | """
8 | pass
9 |
--------------------------------------------------------------------------------
/teachers/templates/teachers/_partial/_teacher.html:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/teachers/templates/teachers/_partial/active_teachers.html:
--------------------------------------------------------------------------------
1 | {% if active_teachers %}
2 |
3 |
4 |
Available now
5 | {% for teacher in active_teachers %}
6 | {% include 'teachers/_partial/_teacher.html' %}
7 | {% endfor %}
8 |
9 |
10 | {% endif %}
11 |
--------------------------------------------------------------------------------
/teachers/templates/teachers/teacher_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'elk/base.html' %}
2 |
3 | {% block content %}
4 |
7 |
8 |
9 | {% for teacher in object_list %}
10 | {% include 'teachers/_partial/_teacher.html' %}
11 | {% endfor %}
12 |
13 |
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/teachers/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/teachers/tests/__init__.py
--------------------------------------------------------------------------------
/teachers/tests/functional/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/teachers/tests/functional/__init__.py
--------------------------------------------------------------------------------
/teachers/tests/functional/tests_teacher.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 |
3 | from elk.utils.testing import TestCase, create_teacher
4 |
5 |
6 | class TestTeacherFunctional(TestCase):
7 | fixtures = ['teachers']
8 |
9 | @classmethod
10 | def setUpTestData(cls):
11 | cls.teacher = create_teacher(accepts_all_lessons=False)
12 |
13 | def test_automatic_group_assignment(self):
14 | """
15 | All newly created teachers should be members of 'teacher' permission group
16 | """
17 | self.assertEqual(self.teacher.user.groups.first().pk, settings.TEACHER_GROUP_ID)
18 |
--------------------------------------------------------------------------------
/teachers/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/teachers/tests/unit/__init__.py
--------------------------------------------------------------------------------
/teachers/tests/unit/tests_manager.py:
--------------------------------------------------------------------------------
1 | from elk.utils.testing import TestCase, create_teacher
2 | from lessons import models as lessons
3 | from teachers.models import Teacher
4 |
5 |
6 | class TeacherManagerTestCase(TestCase):
7 | def setUp(self):
8 | self.teacher = create_teacher(works_24x7=True, accepts_all_lessons=False)
9 |
10 | def test_filter_by_lesson_type_none(self):
11 | lesson_type = lessons.OrdinaryLesson.get_contenttype()
12 | res = Teacher.objects.by_lesson_type(lesson_type)
13 |
14 | self.assertEqual(len(res), 0)
15 |
16 | def test_filter_by_lesson_type_ok(self):
17 |
18 | lesson_type = lessons.OrdinaryLesson.get_contenttype()
19 | self.teacher.allowed_lessons.add(lesson_type)
20 |
21 | res = Teacher.objects.by_lesson_type(lesson_type)
22 |
23 | self.assertEqual(res.first(), self.teacher)
24 |
25 | def test_with_foto_none(self):
26 | self.teacher.teacher_photo = ''
27 | self.teacher.save()
28 |
29 | res = Teacher.objects.with_photos()
30 |
31 | self.assertEqual(len(res), 0)
32 |
33 | def test_with_foto_ok(self):
34 | res = Teacher.objects.with_photos()
35 |
36 | self.assertEqual(res.first(), self.teacher)
37 |
--------------------------------------------------------------------------------
/teachers/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url
2 |
3 | from . import views
4 |
5 | urlpatterns = [
6 | url(r'(?P.+)/', views.TeacherDetail.as_view(), name='detail'),
7 | url(r'$', views.TeacherList.as_view(), name='list')
8 | ]
9 |
--------------------------------------------------------------------------------
/teachers/views.py:
--------------------------------------------------------------------------------
1 | from elk.views import LoginRequiredDetailView, LoginRequiredListView
2 | from teachers.models import Teacher
3 |
4 |
5 | class TeacherDetail(LoginRequiredDetailView):
6 | model = Teacher
7 | slug_url_kwarg = 'username'
8 | slug_field = 'user__username'
9 |
10 | def get_context_data(self, **kwargs):
11 | ctx = super().get_context_data(**kwargs)
12 | ctx['timeslots'] = list(
13 | self.object.free_slots_for_dates(self.request.user.crm.classes.dates_for_planning())
14 | )
15 |
16 | return ctx
17 |
18 |
19 | class TeacherList(LoginRequiredListView):
20 | model = Teacher
21 | queryset = Teacher.objects.with_photos()
22 |
--------------------------------------------------------------------------------
/timeline/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = 'timeline.apps.TimelineConfig'
2 |
--------------------------------------------------------------------------------
/timeline/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/timeline/api/__init__.py
--------------------------------------------------------------------------------
/timeline/api/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 |
3 | from timeline.models import Entry as TimelineEntry
4 |
5 |
6 | class TimelineEntrySerializer(serializers.ModelSerializer):
7 | title = serializers.SerializerMethodField()
8 |
9 | class Meta:
10 | model = TimelineEntry
11 | fields = ('id', 'title', 'teacher', 'start', 'end', 'is_free', 'taken_slots', 'slots')
12 |
13 | def get_title(self, obj):
14 | return str(obj)
15 |
--------------------------------------------------------------------------------
/timeline/api/viewsets.py:
--------------------------------------------------------------------------------
1 | import django_filters
2 | from rest_framework import viewsets
3 |
4 | from elk.api.permissions import StaffMemberRequiredPermission
5 | from timeline.api.serializers import TimelineEntrySerializer
6 | from timeline.models import Entry as TimelineEntry
7 |
8 |
9 | class TimelineFilter(django_filters.rest_framework.FilterSet):
10 | teacher = django_filters.NumberFilter(name='teacher__id')
11 | start = django_filters.DateFromToRangeFilter()
12 |
13 | class Meta:
14 | model = TimelineEntry
15 | fields = ['teacher', 'start']
16 |
17 |
18 | class TimelineViewset(viewsets.ReadOnlyModelViewSet):
19 | queryset = TimelineEntry.objects.all().prefetch_related('lesson').order_by('start')
20 | serializer_class = TimelineEntrySerializer
21 | filter_class = TimelineFilter
22 |
23 | permission_classes = [StaffMemberRequiredPermission]
24 |
--------------------------------------------------------------------------------
/timeline/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class TimelineConfig(AppConfig):
5 | name = 'timeline'
6 |
7 | def ready(self):
8 | import timeline.signals # NOQA
9 |
--------------------------------------------------------------------------------
/timeline/assets/calendar/calendar.coffee:
--------------------------------------------------------------------------------
1 | $('.calendar__teacher-selector').on 'change', () ->
2 | # Replace window href on change of current teacher
3 |
4 | $this = $ this
5 | initial_username = $('option:first-child', $this).val()
6 | selected_username = $this.val()
7 | new_url = window.location.href.replace "/#{ initial_username }/", "/#{ selected_username }/"
8 | window.location.href = new_url
9 |
--------------------------------------------------------------------------------
/timeline/assets/calendar/calendar.styl:
--------------------------------------------------------------------------------
1 | @import 'nib'
2 |
3 | .calendar__teacher-selector
4 | relative: top -5px
5 | button.btn
6 | border: none
7 | font-size: 36px
8 | font-weight: 500
9 | line-height: 1.1 // like bootstrap's h1
10 | padding: 0
11 | width: auto
12 | &:focus
13 | outline: none !important
14 |
15 |
16 | span.filter-option
17 | border-bottom: dotted 1px
18 | outline: none
19 | font-weight: 700
20 |
21 | span.caret
22 | display: none
23 |
--------------------------------------------------------------------------------
/timeline/assets/entry_detail/card.coffee:
--------------------------------------------------------------------------------
1 | $selector = $ '.add-a-student__selector'
2 | $btn = $ '.add-a-student__add-btn'
3 | $selector.selectpicker()
4 |
5 | $selector.on 'change', () ->
6 | if $(this).val() and $(this).val().length
7 | $btn.removeClass 'disabled'
8 | else
9 | $btn.addClass 'disabled'
10 |
11 | $btn.on 'click', () ->
12 | window.location.href = $selector.val()
13 |
--------------------------------------------------------------------------------
/timeline/assets/entry_detail/style.styl:
--------------------------------------------------------------------------------
1 | .timeline-student-list
2 | max-width: 350px
3 |
4 | &__cnt
5 | width: 15px
6 |
7 | &__student
8 | white-space: nowrap
9 | text-align: left
10 |
11 | &__actions
12 | margin-left: 10px
13 |
14 | tr:hover &__actions
15 | display: inline
16 |
17 | tr:not(:hover) &__actions
18 | display: none
19 |
20 |
21 | .timeline-entry-info
22 | max-width: 350px
23 |
24 | .add-a-student
25 | max-width: 350px
26 | &__selector
27 | margin-right: 10px
28 |
--------------------------------------------------------------------------------
/timeline/exceptions.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import ValidationError # noqa
2 |
3 | from market.exceptions import AutoScheduleExpcetion # noqa
4 |
5 |
6 | class DoesNotFitWorkingHours(ValidationError):
7 | pass
8 |
--------------------------------------------------------------------------------
/timeline/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 | from timeline.models import Entry as TimelineEntry
4 |
5 |
6 | class EntryForm(forms.ModelForm):
7 | class Meta:
8 | fields = ('lesson_type', 'lesson_id', 'teacher', 'start')
9 | localized_fields = ('start',)
10 | model = TimelineEntry
11 | widgets = {
12 | 'start': forms.SplitDateTimeWidget(),
13 | 'lesson_id': forms.Select(), # populated by calendar.coffee
14 | 'teacher': forms.HiddenInput() # populated in the template
15 | }
16 |
--------------------------------------------------------------------------------
/timeline/migrations/0002_entry_allow_overlap.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('timeline', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='entry',
16 | name='allow_overlap',
17 | field=models.BooleanField(default=True),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/timeline/migrations/0003_auto_20160719_0702.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import django.db.models.deletion
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('timeline', '0002_entry_allow_overlap'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='entry',
17 | name='teacher',
18 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='timeline_entries', to='teachers.Teacher'),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/timeline/migrations/0004_entry_allow_besides_working_hours.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('timeline', '0003_auto_20160719_0702'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='entry',
16 | name='allow_besides_working_hours',
17 | field=models.BooleanField(default=True),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/timeline/migrations/0005_auto_20160801_1117.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('timeline', '0004_entry_allow_besides_working_hours'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterModelOptions(
15 | name='entry',
16 | options={'verbose_name': 'Planned class', 'verbose_name_plural': 'Planned classes', 'permissions': (('other_entries', "Can work with other's timeleine entries"),)},
17 | ),
18 | migrations.RemoveField(
19 | model_name='entry',
20 | name='customers',
21 | ),
22 | migrations.AlterField(
23 | model_name='entry',
24 | name='slots',
25 | field=models.SmallIntegerField(verbose_name='Student slots', default=1),
26 | ),
27 | migrations.AlterField(
28 | model_name='entry',
29 | name='taken_slots',
30 | field=models.SmallIntegerField(verbose_name='Students', default=0),
31 | ),
32 | ]
33 |
--------------------------------------------------------------------------------
/timeline/migrations/0006_entry_is_finished.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('timeline', '0005_auto_20160801_1117'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='entry',
16 | name='is_finished',
17 | field=models.BooleanField(default=False),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/timeline/migrations/0007_entry_allow_when_teacher_is_busy.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('timeline', '0006_entry_is_finished'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='entry',
16 | name='allow_when_teacher_is_busy',
17 | field=models.BooleanField(default=True),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/timeline/migrations/0008_entry_allow_when_teacher_has_external_events.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('timeline', '0007_entry_allow_when_teacher_is_busy'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='entry',
16 | name='allow_when_teacher_has_external_events',
17 | field=models.BooleanField(default=True),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/timeline/migrations/0009_auto_20161023_1606.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('timeline', '0008_entry_allow_when_teacher_has_external_events'),
11 | ]
12 |
13 | operations = [
14 | migrations.RemoveField(
15 | model_name='entry',
16 | name='allow_overlap',
17 | ),
18 | migrations.RemoveField(
19 | model_name='entry',
20 | name='allow_when_teacher_has_external_events',
21 | ),
22 | migrations.RemoveField(
23 | model_name='entry',
24 | name='allow_when_teacher_is_busy',
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/timeline/migrations/0010_remove_entry_active.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('timeline', '0009_auto_20161023_1606'),
11 | ]
12 |
13 | operations = [
14 | migrations.RemoveField(
15 | model_name='entry',
16 | name='active',
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/timeline/migrations/0012_ordering.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('timeline', '0011_unique_lesson_type'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterModelOptions(
15 | name='entry',
16 | options={'permissions': (('other_entries', "Can work with other's timeleine entries"),), 'verbose_name': 'Planned class', 'verbose_name_plural': 'Planned classes', 'ordering': ['start']},
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/timeline/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/timeline/migrations/__init__.py
--------------------------------------------------------------------------------
/timeline/tasks.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 |
3 | from elk.celery import app as celery
4 | from market.models import Class
5 | from timeline.signals import class_starting_student, class_starting_teacher
6 |
7 |
8 | @celery.task
9 | def notify_15min_to_class():
10 | for i in Class.objects.starting_soon(timedelta(minutes=30)).filter(pre_start_notifications_sent_to_teacher=False).distinct('timeline'):
11 | for other_class_with_the_same_timeline in Class.objects.starting_soon(timedelta(minutes=30)).filter(timeline=i.timeline):
12 | """
13 | Set all other starting classes as notified either.
14 | """
15 | other_class_with_the_same_timeline.pre_start_notifications_sent_to_teacher = True
16 | other_class_with_the_same_timeline.save()
17 | class_starting_teacher.send(sender=notify_15min_to_class, instance=i)
18 |
19 | for i in Class.objects.starting_soon(timedelta(minutes=30)).filter(pre_start_notifications_sent_to_student=False):
20 | i.pre_start_notifications_sent_to_student = True
21 | i.save()
22 | class_starting_student.send(sender=notify_15min_to_class, instance=i)
23 |
--------------------------------------------------------------------------------
/timeline/templates/mail/class/student/starting.html:
--------------------------------------------------------------------------------
1 | {% extends "mail_templated/base.tpl" %}
2 | {% load humanize %}
3 |
4 | {% block subject %}
5 | {{ c.customer.first_name | capfirst }}, your {{ c.timeline.lesson.type_verbose_name }} is about to start!
6 | {% endblock %}
7 |
8 | {% block body %}
9 | Dear {{ c.customer.first_name | capfirst }},
10 |
11 | It's a reminder about your {{ c.timeline.lesson.type_verbose_name }} with {{ c.timeline.teacher.user.first_name }}, which starts at {{ c.timeline.start | time:'TIME_FORMAT'}}.
12 |
13 | {% include 'mailer/_signature.html' %}
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/timeline/templates/mail/class/teacher/starting.html:
--------------------------------------------------------------------------------
1 | {% extends "mail_templated/base.tpl" %}
2 | {% load humanize %}
3 |
4 | {% block subject %}
5 | {{ c.customer.full_name | capfirst}} is about to arrive for {{ c.timeline.lesson.type_verbose_name }}
6 | {% endblock %}
7 |
8 | {% block body %}
9 | Hi, {{ c.timeline.teacher.user.crm.first_name | capfirst }}!
10 |
11 | {{ c.customer.full_name | capfirst}} is about to arrive for {{ c.timeline.lesson.type_verbose_name }} at {{ c.timeline.start | time:'TIME_FORMAT'}}.
12 |
13 | {% include 'mailer/_signature.html' %}
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/timeline/templates/timeline/calendar.html:
--------------------------------------------------------------------------------
1 | {% extends 'elk/base.html' %}
2 |
3 | {% load staticfiles %}
4 |
5 | {% block content %}
6 |
14 |
15 |
16 | {% endblock %}
17 |
18 | {% block css %}
19 |
20 | {% endblock %}
21 |
22 | {% block js %}
23 |
24 | {% endblock %}
25 |
--------------------------------------------------------------------------------
/timeline/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/timeline/templatetags/__init__.py
--------------------------------------------------------------------------------
/timeline/templatetags/format_entry_date.py:
--------------------------------------------------------------------------------
1 | from django import template
2 |
3 | register = template.Library()
4 |
5 |
6 | @register.filter
7 | def format_entry_date(fields):
8 | date = fields.field.widget.widgets[0]
9 | time = fields.field.widget.widgets[1]
10 |
11 | date.attrs['required'] = 'true'
12 | time.attrs['required'] = 'true'
13 |
14 | time.attrs['placeholder'] = '12:30'
15 |
16 | date.attrs['placeholder'] = 'mm/dd/yy'
17 | date.format = '%m/%d/%Y' # flex scope
18 |
19 | time.format = '%H:%M'
20 |
21 | date.attrs['class'] = 'form-control'
22 | time.attrs['class'] = 'form-control'
23 |
24 | # date.attrs['rv-value'] = 'model.start_date'
25 | # time.attrs['rv-value'] = 'model.start_time'
26 |
27 | return fields
28 |
--------------------------------------------------------------------------------
/timeline/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/timeline/tests/__init__.py
--------------------------------------------------------------------------------
/timeline/tests/functional/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/timeline/tests/functional/__init__.py
--------------------------------------------------------------------------------
/timeline/tests/integration/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/timeline/tests/integration/__init__.py
--------------------------------------------------------------------------------
/timeline/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/die-trying/django-celery/00ff8f8b4adbbcf49f23c3f84d689ca4727d85e6/timeline/tests/unit/__init__.py
--------------------------------------------------------------------------------