├── .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 | Schedule 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 |
5 |
6 | 9 |
10 |
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 | 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 | 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